Skip to content

Commit 7ac425e

Browse files
committed
Расширяемая фабрика и IoC
1 parent e5a5b98 commit 7ac425e

24 files changed

Lines changed: 1114 additions & 28 deletions

.pre-commit-config.yaml

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
default_stages: [commit]
1+
default_stages: [pre-commit]
22
repos:
33
- repo: https://github.com/pre-commit/pre-commit-hooks
4-
rev: v5.0.0
4+
rev: v6.0.0
55
hooks:
66
- id: end-of-file-fixer
77
- id: trailing-whitespace
8+
- id: check-added-large-files
89
- repo: https://github.com/Lucas-C/pre-commit-hooks
9-
rev: v1.5.5
10+
rev: v1.5.6
1011
hooks:
1112
- id: remove-crlf
1213
- id: remove-tabs
@@ -17,8 +18,3 @@ repos:
1718
- id: google-java-formatter
1819
- id: commitlint
1920
stages: [commit-msg]
20-
- repo: https://github.com/adrienverge/yamllint.git
21-
rev: v1.35.1
22-
hooks:
23-
- id: yamllint
24-
args: [ --strict, -c=.yamllint ]

.sdkmanrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@
99
#
1010

1111
java=jdk1.8.0_381
12-
gradle=8.10
12+
gradle=8.0

.yamllint

Lines changed: 0 additions & 17 deletions
This file was deleted.

app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ val fasterxml = "2.19.2"
3434
val guavaTableVersion = "33.0.0-jre"
3535
val log4jVersion = "2.20.0"
3636
val awaitilityVersion = "4.3.0"
37+
val mockitoVersion = "4.11.0"
3738

3839
dependencies {
3940
// Import the Spring Boot BOM using the platform() function (compatible Java 8)
@@ -54,6 +55,7 @@ dependencies {
5455
testImplementation("org.junit.jupiter:junit-jupiter")
5556
testImplementation("org.mockito:mockito-core")
5657
testImplementation("org.mockito:mockito-junit-jupiter")
58+
testImplementation("org.mockito:mockito-inline:$mockitoVersion")
5759
}
5860

5961
tasks.named<Test>("test") {
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package ru.otus.main_patterns.hw05;
2+
3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
5+
6+
/*-
7+
11. Расширяемая фабрика и IoC.
8+
Домашнее задание: Реализация IoC контейнера - частный случай расширяемой фабрики.
9+
10+
Цель: Реализовать собственный IoC контейнер, устойчивый к изменению требований.
11+
В результате выполнения домашнего задания Вы получите IoC, который можно будет использовать в качестве фасада в своих проектах.
12+
13+
Пошаговая инструкция выполнения домашнего задания:
14+
В игре "Космический бой" есть набор операций над игровыми объектами: движение по прямой, поворот, выстрел и т.д.
15+
При этом содержание этих команд может отличаться для разных игр, в зависимости от того, какие правила игры были выбраны пользователями.
16+
У каждой игры свой контекст(scope).
17+
Например, в одной игре пользователи могут ограничить запас каждого корабля некоторым количеством топлива,
18+
а в другой игре запретить поворачиваться кораблям по часовой стрелке и т.д.
19+
IoC может помочь в этом случае, скрыв детали в стратегии разрешения зависимости.
20+
21+
Например:
22+
Command command = IoC<Command>.resolve("двигаться прямо", obj);
23+
24+
,возвращает команду, которая чаще всего является макрокомандой и осуществляет один шаг движения по прямой.
25+
26+
Реализовать IoC контейнер, который:
27+
1. Разрешает зависимости с помощью метода, со следующей сигнатурой:
28+
T IoC.resolve(string key, params object[] args);
29+
Так реализуется параметрический полиморфизм в таких языках, как C++, C#, Java, Kotlin и др.
30+
Указание:
31+
Если язык программирования не поддерживает Generics, как, например, PHP, то запись Вам может быть незнакома.
32+
Тогда возвращайте просто ссылку на базовый класс.
33+
2. Регистрация зависимостей также происходит с помощью метода IoC.resolve(...):
34+
IoC.resolve("ioc.register", "aaa", (args) -> new A()).execute();
35+
3. Зависимости можно регистрировать в разных "scope-ах":
36+
IoC.resolve("ioc.scope.new", "scopeId").execute();
37+
IoC.resolve("ioc.scope.current", "scopeId").execute();
38+
39+
Указание: Если Ваш фреймворк допускает работу с многопоточным кодом, то для работы со scope-ами используйте ThreadLocal контейнер.
40+
41+
Критерии оценки:
42+
1. Интерфейс IoC устойчив к изменению требований.
43+
Оценка: 0 - 3 балла (0 - совсем не устойчив, 3 - преподаватель не смог построить ни одного контрпримера)
44+
2. IoC предоставляет ровно один метод для всех операций - 1 балл
45+
3. IoC предоставляет работу со scope-ами для предотвращения сильной связности - 2 балла.
46+
4. Реализованы модульные тесты - 2 балла
47+
5. Реализованы многопоточные тесты - 2 балла
48+
49+
Пример IoC контейнера:
50+
https://github.com/etyumentcev/appserver/tree/main/appserver
51+
52+
Scope = context
53+
54+
*/
55+
public class HW05 {
56+
private static final Logger logger = LoggerFactory.getLogger(HW05.class);
57+
58+
public static void main(String[] args) {
59+
logger.info(
60+
"Java version: {}, Java vendor: {}\nДомашнее задание: Реализация IoC контейнера",
61+
System.getProperty("java.version"),
62+
System.getProperty("java.vendor"));
63+
}
64+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package ru.otus.main_patterns.hw05.commands;
2+
3+
import ru.otus.main_patterns.hw05.interfaces.Command;
4+
5+
public class ClearCurrentScopeCommand implements Command {
6+
7+
@Override
8+
public void execute() {
9+
InitCommand.currentScopeThreadLocal.remove();
10+
}
11+
}
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package ru.otus.main_patterns.hw05.commands;
2+
3+
import java.util.HashMap;
4+
import java.util.Map;
5+
import java.util.concurrent.ConcurrentHashMap;
6+
import java.util.concurrent.atomic.AtomicBoolean;
7+
import java.util.function.BiFunction;
8+
import java.util.function.Function;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
11+
import ru.otus.main_patterns.hw05.core.IoC;
12+
import ru.otus.main_patterns.hw05.interfaces.Command;
13+
import ru.otus.main_patterns.hw05.interfaces.DependencyResolver;
14+
import ru.otus.main_patterns.hw05.scopes.DependencyResolverImpl;
15+
16+
/**
17+
* <a
18+
* href="https://github.com/etyumentcev/appserver/blob/main/appserver/scopes/InitCommand.cs">Example</a>
19+
*
20+
* <p>Scope(context) - это разные области видимости.
21+
*
22+
* <p>ThreadLocal — это специальный класс в Java, который позволяет создавать переменные, доступные
23+
* только для одного конкретного потока. Если обычная статическая переменная видна всем потокам
24+
* одинаково, то в случае с ThreadLocal у каждого потока будет своя собственная, изолированная копия
25+
* этой переменной. Изменение значения в одном потоке никак не повлияет на значение в другом.
26+
*
27+
* <p>ThreadLocal - это карта, где в качестве ключа выступает идентификатор текущего потока(Thread).
28+
* Можно по текущему потоку хранить какой-то тип данных.
29+
*
30+
* <p>В результате в каждый поток можно положить собственный scope(context). В итоге, когда мы будем
31+
* обращаться к этой стратегии(через метод IoC.resolve()), то мы сначала идем в ThreadLocal, смотрим
32+
* в текущем потоке какой текущий контекст(scope) и уже работаем с ним. В итоге получается, что в
33+
* каждом потоке могут быть как одинаковые, так и разные контексты. Т.е. Получаем, что благодаря
34+
* ThreadLocal, в одном потоке один контекст, а в другом потоке другой контекст. Т.е. Когда мы
35+
* выполняем метод IoC.resolve(), первое, что происходит - это мы пойдем в ThreadLocal и получим
36+
* текущий контекст. В каком бы потоке мы не вызывали мы всегда получим ссылку на тот контекст,
37+
* который актуален для этого потока. Таким образом мы получаем потокобезопасные реализации.
38+
* Стратегию проще всего представлять в виде лямбды функции - Function <Object[], Object>.
39+
*
40+
* <p>Инициализация многопоточного контейнера. Выполняется только один раз. Эта команда
41+
* инициализирует IoC контейнер для работы с несколькими потоками. Scope = Map<String,
42+
* Function<Object[], Object>>
43+
*
44+
* <p>При переопределении стратегии (через {@code "update.ioc.resolve.dependency.strategy"})
45+
* ожидается лямбда-функция типа:
46+
*
47+
* <pre>{@code
48+
* Function<BiFunction<String, Object[], Object>, BiFunction<String, Object[], Object>>
49+
* }</pre>
50+
*/
51+
public class InitCommand implements Command {
52+
public static final ThreadLocal<Object> currentScopeThreadLocal =
53+
ThreadLocal.withInitial(() -> null);
54+
// key = dependency name, value = strategy as lambda Function<Object[], Object>
55+
private static final ConcurrentHashMap<String, Function<Object[], Object>> scopesMap =
56+
new ConcurrentHashMap<>();
57+
private static final AtomicBoolean isAlreadyExecutesSuccessfully = new AtomicBoolean(false);
58+
59+
private static final Logger logger = LoggerFactory.getLogger(InitCommand.class);
60+
61+
/** - Выполняется один раз. */
62+
@Override
63+
public void execute() {
64+
logger.info("execute, isAlreadyExecutesSuccessfully: {}", isAlreadyExecutesSuccessfully);
65+
if (isAlreadyExecutesSuccessfully.get()) {
66+
return;
67+
}
68+
69+
// Root scope - главный контекст, в который мы складываем разные зависимости
70+
synchronized (scopesMap) {
71+
/*-
72+
SetCurrentScopeCommand - команда установит, в этом потоке, текущий контекст(смена контекста),
73+
который будет передан в качестве аргумента(args[0]).
74+
E.g. change scope:
75+
IoC.resolve<Command>("ioc.scope.current.set", otherScope).execute()
76+
*/
77+
scopesMap.putIfAbsent(
78+
"ioc.scope.current.set", (Object[] args) -> new SetCurrentScopeCommand(args[0]));
79+
scopesMap.putIfAbsent(
80+
"ioc.scope.current.clear", (Object[] args) -> new ClearCurrentScopeCommand());
81+
82+
/*-
83+
"ioc.scope.current" - возвращает текущий scope, который есть в текущем потоке.
84+
currentScopeThreadLocal.get() - возвращает значение из текущего потока.
85+
*/
86+
scopesMap.putIfAbsent(
87+
"ioc.scope.current",
88+
(Object[] args) ->
89+
currentScopeThreadLocal.get() != null ? currentScopeThreadLocal.get() : scopesMap);
90+
/*-
91+
У контекста можно прочитать родительский контекст. Все контексты упорядочены иерархически.
92+
"ioc.scope.parent" - возвращает ссылку на родительский контекст.
93+
*/
94+
scopesMap.putIfAbsent(
95+
"ioc.scope.parent",
96+
(Object[] args) -> {
97+
throw new RuntimeException("The root scope has no a parent scope.");
98+
});
99+
scopesMap.putIfAbsent(
100+
"ioc.scope.create.empty",
101+
(Object[] args) -> new HashMap<String, Function<Object[], Object>>());
102+
scopesMap.putIfAbsent(
103+
"ioc.scope.create",
104+
(Object[] args) -> {
105+
Map<String, Function<Object[], Object>> scopeMap =
106+
IoC.resolve("ioc.scope.create.empty");
107+
if (args.length > 0) {
108+
scopeMap.put("ioc.scope.parent", (Object[] innerArgs) -> args[0]);
109+
} else {
110+
scopeMap.put(
111+
"ioc.scope.parent", (Object[] innerArgs) -> IoC.resolve("ioc.scope.current"));
112+
}
113+
return scopeMap;
114+
});
115+
116+
/*-
117+
При регистрации зависимости мы создаем команду RegisterDependencyCommand и передаем ей
118+
нужные параметры.
119+
Регистрация новой стратегии разрешения(resolve) зависимости в нашем контейнере.
120+
Когда мы регистрируем зависимость, то мы создаем команду RegisterDependencyCommand.
121+
RegisterDependencyCommand - команда, которая будет регистрировать зависимость.
122+
Param#1 - имя зависимости
123+
Param#2 - лямбда функция(Function <Object[], Object>), которая будет вызываться
124+
Usage:
125+
IoC.<Command>resolve("ioc.register", "A", (Object[] args -> new A()}).execute()
126+
, где
127+
- "ioc.register" - имя разрешаемой зависимости
128+
На вход подается два параметра:
129+
- "A" - имя зависимости
130+
- (Object[] args) -> { return new A();} - стратегия(лямбда функция - Function <Object[], Object>)
131+
с помощью которой будет разрешена эта зависимость.
132+
Стратегия принимает на вход параметры args(Object[]) и возвращает что-то(Object).
133+
В примере стратегия возвращает экземпляр класса A.
134+
Т.е. Когда просят вернуть экземпляр класса A, то будет вызвана стратегия(лямбда функция) и
135+
результат работы этой лямбда функции вернется на выходе. В примере это Command, которую мы
136+
выполняем(execute) для регистрации стратегии внутри фабрики.
137+
138+
В случае Spring-а, "A" - это имя bean-а в Spring контейнере.
139+
140+
Т.е. Вместо
141+
SomeInterface user = new User();
142+
Function<Object[], Object> dependencyResolveStrategy = args -> new User();
143+
IoC.register("A", dependencyResolveStrategy)
144+
145+
мы пишем
146+
IoC.<Command>resolve("ioc.register", "A", dependencyResolveStrategy).execute()
147+
148+
Второй подход более универсальный, т.к. При появлении новой команды (например IoC.freeze) мы не будем менять существующий клиентский код.
149+
Usage:
150+
SomeInterface a = IoC.<SomeInterface>resolve("A");
151+
*/
152+
scopesMap.putIfAbsent(
153+
"ioc.register",
154+
(Object[] args) -> { // регистрация стратегии(strategy) внутри фабрики
155+
String dependencyName = (String) args[0];
156+
Function<Object[], Object> strategy = (Function<Object[], Object>) args[1];
157+
/*-
158+
Таким образом, метод IoC.resolve() полностью заменяет создание объекта через "new", решая главную задачу:
159+
клиентский код не меняется при изменении правил создания объектов.
160+
Ключевое слово "new" будет использоваться только при регистрации зависимости, что
161+
позволяет соблюдать принцип открытости/замкнутости (Open-Closed Principle).
162+
*/
163+
return new RegisterDependencyCommand(dependencyName, strategy);
164+
});
165+
166+
/*-
167+
Основная стратегия.
168+
Стратегия, которая разрешает(resolve) зависимость в виде лямбды функции -
169+
BiFunction<String, Object[], Object>.
170+
Когда нас просят разрешить зависимость, то мы в текущем потоке "currentScopeThreadLocal"
171+
получаем установленный scope - currentScopeThreadLocal.get().
172+
Если scope есть, то возвращаем его. Если не установлен, то возвращаем rootScope.
173+
DependencyResolverImpl - специальная конструкция с помощью которой мы находим нужную
174+
зависимость и вызываем ее(strategyResolver).
175+
*/
176+
BiFunction<String, Object[], DependencyResolver> strategyResolver =
177+
new BiFunction<String, Object[], DependencyResolver>() {
178+
@Override
179+
public DependencyResolver apply(String dependency, Object[] args) {
180+
// 1. Находим scope
181+
Object findScope =
182+
currentScopeThreadLocal.get() != null ? currentScopeThreadLocal.get() : scopesMap;
183+
// 2. Находим нужную зависимость и вызываем ее.
184+
DependencyResolver dependencyResolver = new DependencyResolverImpl(findScope);
185+
return (DependencyResolver) dependencyResolver.resolve(dependency, args);
186+
}
187+
};
188+
189+
/*-
190+
Вызываем зависимость "update.ioc.resolve.dependency.strategy" для того, чтобы заменить на стратегию.
191+
param1 = имя зависимости "update.ioc.resolve.dependency.strategy" (same as Spring Bean name)
192+
param2 = стратегия с помощью которой будет разрешена(resolve) эта зависимость.
193+
Вызываем данную зависимость для того, чтобы заменить ее на стратегию.
194+
*/
195+
Command command = IoC.resolve("update.ioc.resolve.dependency.strategy", strategyResolver);
196+
command.execute();
197+
198+
isAlreadyExecutesSuccessfully.set(true);
199+
}
200+
}
201+
}

0 commit comments

Comments
 (0)