Skip to content

Commit a5cdae8

Browse files
committed
feat(HW05): Расширяемая фабрика и IoC
1 parent e5a5b98 commit a5cdae8

21 files changed

Lines changed: 848 additions & 3 deletions

.idea/misc.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.sdkmanrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
# Java SE 8 = 52 (0x34 hex)
99
#
1010

11-
java=jdk1.8.0_381
12-
gradle=8.10
11+
java=8.0.472.fx-zulu
12+
gradle=8.0

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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
// new InitCommand().execute();
65+
}
66+
}
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: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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>При переопределении стратегии (через "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+
// SetCurrentScopeCommand - команда установит, в этом потоке, текущий контекст(смена
72+
// контекста), который будет передан в качестве аргумента(args[0]).
73+
// E.g. change scope:
74+
// IoC.resolve<Command>("ioc.scope.current.set", otherScope).execute()
75+
scopesMap.putIfAbsent(
76+
"ioc.scope.current.set", (Object[] args) -> new SetCurrentScopeCommand(args[0]));
77+
scopesMap.putIfAbsent(
78+
"ioc.scope.current.clear", (Object[] args) -> new ClearCurrentScopeCommand());
79+
80+
// ioc.scope.current - возвращает текущий scope, который есть в текущем потоке.
81+
// currentScopeThreadLocal.get() - возвращает значение из текущего потока.
82+
scopesMap.putIfAbsent(
83+
"ioc.scope.current",
84+
(Object[] args) ->
85+
currentScopeThreadLocal.get() != null ? currentScopeThreadLocal.get() : scopesMap);
86+
// У контекста можно прочитать родительский контекст. Все контексты упорядочены иерархически.
87+
// "ioc.scope.parent" - возвращает ссылку на родительский контекст.
88+
scopesMap.putIfAbsent(
89+
"ioc.scope.parent",
90+
(Object[] args) -> {
91+
throw new RuntimeException("The root scope has no a parent scope.");
92+
});
93+
scopesMap.putIfAbsent(
94+
"ioc.scope.create.empty",
95+
(Object[] args) -> new HashMap<String, Function<Object[], Object>>());
96+
scopesMap.putIfAbsent(
97+
"ioc.scope.create",
98+
(Object[] args) -> {
99+
Map<String, Function<Object[], Object>> scopeMap =
100+
IoC.resolve("ioc.scope.create.empty");
101+
if (args.length > 0) {
102+
scopeMap.put("ioc.scope.parent", (Object[] innerArgs) -> args[0]);
103+
} else {
104+
scopeMap.put(
105+
"ioc.scope.parent", (Object[] innerArgs) -> IoC.resolve("ioc.scope.current"));
106+
}
107+
return scopeMap;
108+
});
109+
110+
// При регистрации зависимости мы создаем команду RegisterDependencyCommand и передаем ей
111+
// нужные параметры.
112+
// Регистрация новой стратегии разрешения(resolve) зависимости в нашем контейнере.
113+
// Когда мы регистрируем зависимость, то мы создаем команду RegisterDependencyCommand.
114+
// RegisterDependencyCommand - команда, которая будет регистрировать зависимость.
115+
// Param#1 - имя зависимости
116+
// Param#2 - лямбда функция(Function <Object[], Object>), которая будет вызываться
117+
// Usage:
118+
// IoC.<Command>resolve("ioc.register", "A", (Object[] args -> new A()}).execute()
119+
// , где
120+
// - "ioc.register" - имя разрешаемой зависимости
121+
// На вход подается два параметра:
122+
// - "A" - имя зависимости
123+
// - (Object[] args) -> { return new A();} - стратегия(лямбда функция - Function <Object[],
124+
// Object>) с помощью которой будет разрешена эта зависимость.
125+
// Стратегия принимает на вход параметры args(Object[]) и возвращает что-то(Object).
126+
// В примере стратегия возвращает экземпляр класса A.
127+
// Т.е. когда просят вернуть экземпляр класса A, то будет вызвана стратегия(лямбда функция) и
128+
// результат работы этой лямбда функции вернется на выходе. В примере это Command, которую мы
129+
// выполняем(execute) для регистрации стратегии внутри фабрики.
130+
// В случае Spring-а, "A" - это имя bean-а в Spring контейнере.
131+
scopesMap.putIfAbsent(
132+
"ioc.register",
133+
(Object[] args) -> { // регистрация стратегии(strategy) внутри фабрики
134+
String dependencyName = (String) args[0];
135+
Function<Object[], Object> strategy = (Function<Object[], Object>) args[1];
136+
return new RegisterDependencyCommand(dependencyName, strategy);
137+
});
138+
139+
// Основная стратегия.
140+
// Стратегия, которая разрешает(resolve) зависимость в виде лямбды функции -
141+
// BiFunction<String, Object[], Object>.
142+
// Когда нас просят разрешить зависимость, то мы в текущем потоке "currentScopeThreadLocal"
143+
// получаем установленный scope - currentScopeThreadLocal.get().
144+
// Если scope есть, то возвращаем его. Если не установлен, то возвращаем rootScope.
145+
// DependencyResolverImpl - специальная конструкция с помощью которой мы находим нужную
146+
// зависимость и вызываем ее(strategyResolver).
147+
BiFunction<String, Object[], DependencyResolver> strategyResolver =
148+
(dependency, args) -> {
149+
// 1. Находим scope
150+
Object findScope =
151+
currentScopeThreadLocal.get() != null ? currentScopeThreadLocal.get() : scopesMap;
152+
// 2. Находим нужную зависимость и вызываем ее.
153+
DependencyResolver dependencyResolver = new DependencyResolverImpl(findScope);
154+
return (DependencyResolver) dependencyResolver.resolve(dependency, args);
155+
};
156+
// Вызываем зависимость "update.ioc.resolve.dependency.strategy" для того, чтобы заменить на
157+
// стратегию.
158+
// param1 = имя зависимости "update.ioc.resolve.dependency.strategy" (same as Spring Bean
159+
// name)
160+
// param2 = стратегия с помощью которой будет разрешена(resolve) эта зависимость
161+
// Вызываем данную зависимость для того, чтобы заменить ее на стратегию.
162+
Command command = IoC.resolve("update.ioc.resolve.dependency.strategy", strategyResolver);
163+
command.execute();
164+
165+
isAlreadyExecutesSuccessfully.set(true);
166+
}
167+
}
168+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package ru.otus.main_patterns.hw05.commands;
2+
3+
import java.util.Map;
4+
import java.util.function.Function;
5+
import ru.otus.main_patterns.hw05.core.IoC;
6+
import ru.otus.main_patterns.hw05.interfaces.Command;
7+
8+
/** Команда для регистрации зависимости. Обычно команда регистрируется сразу после ее создания. */
9+
public class RegisterDependencyCommand implements Command {
10+
private final String dependency;
11+
private final Function<Object[], Object> dependencyResolverStrategy;
12+
13+
/**
14+
* @param dependency имя зависимости
15+
* @param dependencyResolverStrategy лямбда-выражение, которое будет вызываться
16+
*/
17+
public RegisterDependencyCommand(
18+
String dependency, Function<Object[], Object> dependencyResolverStrategy) {
19+
this.dependency = dependency;
20+
this.dependencyResolverStrategy = dependencyResolverStrategy;
21+
}
22+
23+
@Override
24+
public void execute() {
25+
// Из текущего ThreadLocal получаем ссылку на текущий контекст(scope).
26+
Map<String, Function<Object[], Object>> currentScope = IoC.resolve("ioc.scope.current");
27+
// В текущем контексте добавляем(регистрируем) зависимость.
28+
currentScope.put(dependency, dependencyResolverStrategy);
29+
}
30+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ru.otus.main_patterns.hw05.commands;
2+
3+
import ru.otus.main_patterns.hw05.interfaces.Command;
4+
5+
public class SetCurrentScopeCommand implements Command {
6+
private Object scope;
7+
8+
public SetCurrentScopeCommand(Object scope) {
9+
this.scope = scope;
10+
}
11+
12+
@Override
13+
public void execute() {
14+
InitCommand.currentScopeThreadLocal.set(scope);
15+
}
16+
}

0 commit comments

Comments
 (0)