diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/ActionRegistry.java b/app/src/main/java/ru/otus/main_patterns/hw13/ActionRegistry.java new file mode 100644 index 0000000..d8b2dc1 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/ActionRegistry.java @@ -0,0 +1,27 @@ +package ru.otus.main_patterns.hw13; + +import java.util.HashMap; +import java.util.Map; +import ru.otus.main_patterns.hw13.handler.ActionHandler; + +public class ActionRegistry { + + private static final Map handlers = new HashMap<>(); + + public static void register(String action, ActionHandler handler) { + handlers.put(action, handler); + } + + public static ActionHandler get(String action) { + ActionHandler handler = handlers.get(action); + + if (handler == null) { + throw new RuntimeException("Unknown action: " + action); + } + return handler; + } + + public static void clear() { + handlers.clear(); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/GameObject.java b/app/src/main/java/ru/otus/main_patterns/hw13/GameObject.java new file mode 100644 index 0000000..11ff0a4 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/GameObject.java @@ -0,0 +1,19 @@ +package ru.otus.main_patterns.hw13; + +import java.util.HashMap; +import java.util.Map; + +public class GameObject implements UObject { + + private final Map properties = new HashMap<>(); + + @Override + public Object getProperty(String key) { + return properties.get(key); + } + + @Override + public void setProperty(String key, Object value) { + properties.put(key, value); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/HW13.java b/app/src/main/java/ru/otus/main_patterns/hw13/HW13.java new file mode 100644 index 0000000..22cb9c8 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/HW13.java @@ -0,0 +1,181 @@ +package ru.otus.main_patterns.hw13; + +import java.util.HashMap; +import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import ru.otus.main_patterns.hw13.command.*; +import ru.otus.main_patterns.hw13.handler.BurnFuelHandler; +import ru.otus.main_patterns.hw13.handler.ShootHandler; +import ru.otus.main_patterns.hw13.handler.StartMoveHandler; +import ru.otus.main_patterns.hw13.handler.StopMoveHandler; + +/* + Домашнее задание: + Разработать систему команд для космических кораблей (pattern Интерпретатор) + + Цель: + Научиться применять интерпретатор. + Необходимо научить игровые объекты реагировать на действия игроков. + + Описание/Пошаговая инструкция выполнения домашнего задания: + Предположим, что приказ имеет следующий вид: + { + "ID": "ид объекта, которому адресован приказ", + "action": "действие, которое необходимо выполнить", + // какие-то специфичные параметры для данного приказа + } + + Например, вот так может выглядеть приказ кораблю с id "548" начать двигаться по прямой со скоростью 2. + { + "id": "548", + "action": "StartMove", + "initialVelocity": 2 + } + + Приказы будем представлять также, как и игровые объекты, с помощью интерфейса UObject + Необходимо реализовать: + 1. Команду, которая получает на вход приказ и с помощью IoC выполняет необходимое действие. + 2. Необходимо запретить одному игроку отдавать приказы другому игроку. + + Критерии оценки: + 1. Задача сдана на проверку. 1 балл + 2. Оформлен MR/PR. 1 балл + 3. CI. 1 балл. + 4. Реализован интерпретатор приказов из п. 1. + 1 балл, если с помощью этого интерпретатора можно обработать приказы на старт/стоп движения, выстрел. + 2 балла, если преподаватель не может привести пример приказа для управления игровыми объектами, + для реализации которого требуется изменить код. + 3 балла, если можно обрабатывать не только приказы игровым объектам, но и любе другие. + 1 балл, если код покрыт тестами. + 5. Реализована защита, которая не позволяет отдавать приказы чужим объектам. + 1 балл, если защита реализована любым способом. + 2 балла, если защита реализована через отдельные скоупы для каждого игрока. + 1 балл, если код покрыт тестами. + + Максимальная оценка в 10 баллов + Задание принято, если оценено не менее, чем в 7 баллов + + Решение: + Паттерн Интерпретатор берёт выражение на некотором языке (DSL) и преобразует его в смысл (поведение) + + Общая идея: + Здесь не просто “команды корабля”, а мини-язык управления космическими объектами (DSL): + order: + { + id = "548" + action = "StartMove" + initialVelocity = 2 + } + + Это по сути скрипт/инструкция, которую система должна: + 1. Прочитать + 2. Понять (интерпретировать) + 3. Превратить в действия + 4. Выполнить + + Схема: + UObject (DSL) + ↓ + InterpretCommand (Interpreter) + ↓ + ActionRegistry (семантика action) + ↓ + ActionHandler (построение команды) + ↓ + IoC (создание Command) + ↓ + Command.execute() + + Итог: + Мини-язык управления кораблями + система "язык -> интерпретация -> исполнение". + Command Pattern: + - StartMoveCommand + - StopMoveCommand + Interpreter Pattern: + - InterpretCommand ← главный интерпретатор + - ActionRegistry ← грамматика + - ActionHandler ← семантика + - UObject order ← AST/DSL + IoC: + - фабрика команд + - dependency injection + + InterpretCommand реализует паттерн Interpreter, так как он принимает входной DSL-объект (UObject), + интерпретирует его поле action как символ языка, находит соответствующее правило в ActionRegistry + и преобразует его в исполняемую Command. Таким образом он отделяет синтаксис (UObject) от семантики (Command execution). +*/ +public class HW13 { + private static final Logger logger = LoggerFactory.getLogger(HW13.class); + + public static void main(String[] vars) { + logger.info( + "Java version: {}, Java vendor: {}\nРазработать систему команд для космических кораблей", + System.getProperty("java.version"), + System.getProperty("java.vendor")); + + initRegistry(); + + String playerId = "player-1"; + + // создаём scope игрока + IoC.createScope(playerId); + + // СОЗДАЁМ И РЕГИСТРИРУЕМ КОРАБЛЬ В SCOPE + UObject ship = new GameObject(); + ship.setProperty("id", "548"); + ship.setProperty("ownerId", playerId); + + // scoped storage внутри IoC + IoC.register( + playerId, + "Ships.Storage", + args -> { + Map ships = new HashMap<>(); + ships.put("548", ship); + return ships; + }); + + // доступ к объектам через scope + IoC.register( + playerId, + "Objects.GetById", + args -> { + String id = (String) args[0]; + Map ships = IoC.resolve(playerId, "Ships.Storage"); + return ships.get(id); + }); + + // DSL приказ + UObject order = new GameObject(); + order.setProperty("id", "548"); + order.setProperty("action", "StartMove"); + order.setProperty("initialVelocity", 2); + + // интерпретация и выполнение + Command command = new InterpretCommand(order, playerId); + command.execute(); + logger.debug("Finish executed command, playerId = {}", playerId); + } + + private static void initRegistry() { + // COMMAND FACTORIES (GLOBAL IoC) + IoC.register( + "Commands.StartMove", args -> new StartMoveCommand((UObject) args[0], (int) args[1])); + + IoC.register( + "Commands.BurnFuel", args -> new BurnFuelCommand((UObject) args[0], (int) args[1])); + + IoC.register( + "Commands.Shoot", + args -> new ShootCommand((UObject) args[0], (String) args[1], (String) args[2])); + + IoC.register("Commands.StopMove", args -> new StopMoveCommand((UObject) args[0])); + + // ACTION REGISTRY (DSL → handler) + ActionRegistry.register("StartMove", new StartMoveHandler()); + ActionRegistry.register("BurnFuel", new BurnFuelHandler()); + ActionRegistry.register("Shoot", new ShootHandler()); + ActionRegistry.register("StopMove", new StopMoveHandler()); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/IoC.java b/app/src/main/java/ru/otus/main_patterns/hw13/IoC.java new file mode 100644 index 0000000..e63d121 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/IoC.java @@ -0,0 +1,121 @@ +package ru.otus.main_patterns.hw13; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * IoC-контейнер (Inversion of Control) — реализация паттернов Dependency Injection / Service + * Locator. + * + *

Предоставляет двухуровневую систему разрешения зависимостей: + * + *

    + *
  • Глобальный уровень — зависимости, доступные всем участникам. + *
  • Уровень scope — изолированные зависимости для конкретного игрока/контекста. + *
+ * + *
+ *   Global IoC (общие зависимости)
+ *      ↑ fallback
+ *   Player Scope (player1)  — приватные зависимости игрока 1
+ *   Player Scope (player2)  — приватные зависимости игрока 2
+ * 
+ * + *

При разрешении зависимости из scope сначала ищется локальная фабрика, а при её отсутствии — + * происходит fallback на глобальный контейнер. + */ +public class IoC { + + /** Глобальный реестр фабрик: ключ зависимости → фабричная функция. */ + private static final Map> container = new HashMap<>(); + + /** Изолированные scope: идентификатор scope → локальный реестр фабрик. */ + private static final Map>> scopes = + new HashMap<>(); + + /** + * Регистрирует фабрику зависимости в глобальном контейнере. + * + * @param key уникальный ключ зависимости + * @param factory фабричная функция, принимающая аргументы и возвращающая экземпляр + */ + public static void register(String key, Function factory) { + container.put(key, factory); + } + + /** + * Регистрирует фабрику зависимости в указанном scope. Если scope ещё не существует — он будет + * создан автоматически. + * + * @param scopeId идентификатор scope (например, ID игрока) + * @param key уникальный ключ зависимости внутри scope + * @param factory фабричная функция + */ + public static void register(String scopeId, String key, Function factory) { + createScope(scopeId); + scopes.get(scopeId).put(key, factory); + } + + /** + * Создаёт новый scope, если он ещё не существует. + * + * @param scopeId идентификатор scope + */ + public static void createScope(String scopeId) { + scopes.putIfAbsent(scopeId, new HashMap<>()); + } + + /** + * Разрешает зависимость из глобального контейнера. + * + * @param key ключ зависимости + * @param args аргументы, передаваемые в фабричную функцию + * @param ожидаемый тип возвращаемого объекта + * @return экземпляр, созданный фабрикой + * @throws RuntimeException если зависимость с указанным ключом не найдена + */ + @SuppressWarnings("unchecked") + public static T resolve(String key, Object... args) { + Function factory = container.get(key); + if (factory == null) { + throw new RuntimeException("Dependency not found: " + key); + } + return (T) factory.apply(args); + } + + /** + * Разрешает зависимость из указанного scope с fallback на глобальный контейнер. + * + *

Порядок поиска: + * + *

    + *
  1. Локальный scope (по {@code scopeId}) + *
  2. Глобальный контейнер (если в scope не найдено) + *
+ * + * @param scopeId идентификатор scope + * @param key ключ зависимости + * @param args аргументы для фабричной функции + * @param ожидаемый тип возвращаемого объекта + * @return экземпляр, созданный фабрикой + * @throws RuntimeException если зависимость не найдена ни в scope, ни глобально + */ + @SuppressWarnings("unchecked") + public static T resolve(String scopeId, String key, Object... args) { + Map> scope = scopes.get(scopeId); + if (scope != null && scope.containsKey(key)) { + return (T) scope.get(key).apply(args); + } + return resolve(key, args); + } + + /** + * Полностью очищает глобальный контейнер и все scope. Используется для сброса состояния + * (например, в тестах). + */ + public static void clear() { + container.clear(); + scopes.clear(); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/UObject.java b/app/src/main/java/ru/otus/main_patterns/hw13/UObject.java new file mode 100644 index 0000000..8d3e6f9 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/UObject.java @@ -0,0 +1,15 @@ +package ru.otus.main_patterns.hw13; + +/* + Универсальный контейнер данных, который: + - хранит приказ + - хранит игровые объекты + - хранит параметры команд + + Делает систему динамической. +*/ +public interface UObject { + Object getProperty(String key); + + void setProperty(String key, Object value); +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/command/BurnFuelCommand.java b/app/src/main/java/ru/otus/main_patterns/hw13/command/BurnFuelCommand.java new file mode 100644 index 0000000..b1f1662 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/command/BurnFuelCommand.java @@ -0,0 +1,23 @@ +package ru.otus.main_patterns.hw13.command; + +import ru.otus.main_patterns.hw13.UObject; + +public class BurnFuelCommand implements Command { + + private final UObject ship; + private final int fuelAmount; + + public BurnFuelCommand(UObject ship, int fuelAmount) { + this.ship = ship; + this.fuelAmount = fuelAmount; + } + + @Override + public void execute() { + int fuel = (int) ship.getProperty("fuel"); + if (fuel < fuelAmount) { + throw new RuntimeException("Not enough fuel"); + } + ship.setProperty("fuel", fuel - fuelAmount); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/command/Command.java b/app/src/main/java/ru/otus/main_patterns/hw13/command/Command.java new file mode 100644 index 0000000..0bb628a --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/command/Command.java @@ -0,0 +1,5 @@ +package ru.otus.main_patterns.hw13.command; + +public interface Command { + void execute(); +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/command/InterpretCommand.java b/app/src/main/java/ru/otus/main_patterns/hw13/command/InterpretCommand.java new file mode 100644 index 0000000..9f65562 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/command/InterpretCommand.java @@ -0,0 +1,67 @@ +package ru.otus.main_patterns.hw13.command; + +import ru.otus.main_patterns.hw13.ActionRegistry; +import ru.otus.main_patterns.hw13.IoC; +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.exception.AccessDeniedException; +import ru.otus.main_patterns.hw13.handler.ActionHandler; + +public class InterpretCommand implements Command { + + private final UObject order; + private final String currentPlayerId; + + /* + InterpretCommand интерпретирует DSL (UObject) в Command. + InterpretCommand является интерпретатором, потому что он преобразует входной DSL (UObject приказ) в исполняемые команды, + интерпретируя значение поля action как семантическое действие системы. + Шаги: + 1. читает входной DSL (order) + 2. выделяет символы языка (action, id, параметры) + 3. находит смысл через registry + 4. строит execution tree (Command) + 5. запускает выполнение + + Важно: + InterpretCommand НЕ выполняет действие. + Он делает только “перевод текста команды в исполняемую структуру”. + Это и есть Interpreter: + - понять что значит "StartMove" + - собрать объект команды + - запустить её + + В данной реализации паттерн Интерпретатор тесно связан с Service Locator (IoC). + Это делает систему гибкой: + - Scopes (Области видимости): В main создается playerId. Это позволяет интерпретатору проверять права доступа. + Если один игрок попытается "интерпретировать" приказ для корабля другого игрока, checkAccess выдаст ошибку. + - Decoupling (Разделение): InterpretCommand не знает о существовании StartMoveCommand. Он общается только с интерфейсами. + Это значит, что можно добавить новую команду (например, "HyperJump"), просто зарегистрировав новый хендлер, + не меняя код самого интерпретатора. + */ + public InterpretCommand(UObject order, String currentPlayerId) { + this.order = order; + this.currentPlayerId = currentPlayerId; + } + + @Override + public void execute() { + String objectId = (String) order.getProperty("id"); + String action = (String) order.getProperty("action"); + UObject gameObject = IoC.resolve(currentPlayerId, "Objects.GetById", objectId); + checkAccess(gameObject); + Command command = buildCommand(action, gameObject); + command.execute(); + } + + private void checkAccess(UObject gameObject) { + String ownerId = (String) gameObject.getProperty("ownerId"); + if (!currentPlayerId.equals(ownerId)) { + throw new AccessDeniedException("Игрок не может управлять чужим объектом"); + } + } + + private Command buildCommand(String action, UObject gameObject) { + ActionHandler handler = ActionRegistry.get(action); + return handler.handle(order, gameObject); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/command/ShootCommand.java b/app/src/main/java/ru/otus/main_patterns/hw13/command/ShootCommand.java new file mode 100644 index 0000000..fc435ae --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/command/ShootCommand.java @@ -0,0 +1,21 @@ +package ru.otus.main_patterns.hw13.command; + +import ru.otus.main_patterns.hw13.UObject; + +public class ShootCommand implements Command { + private final UObject ship; + private final String weaponId; + private final String direction; + + public ShootCommand(UObject ship, String weaponId, String direction) { + this.ship = ship; + this.weaponId = weaponId; + this.direction = direction; + } + + @Override + public void execute() { + ship.setProperty("weaponId", weaponId); + ship.setProperty("direction", direction); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/command/StartMoveCommand.java b/app/src/main/java/ru/otus/main_patterns/hw13/command/StartMoveCommand.java new file mode 100644 index 0000000..82805c2 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/command/StartMoveCommand.java @@ -0,0 +1,19 @@ +package ru.otus.main_patterns.hw13.command; + +import ru.otus.main_patterns.hw13.UObject; + +public class StartMoveCommand implements Command { + private final UObject ship; + private final int velocity; + + public StartMoveCommand(UObject ship, int velocity) { + this.ship = ship; + this.velocity = velocity; + } + + @Override + public void execute() { + ship.setProperty("velocity", velocity); + ship.setProperty("moving", true); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/command/StopMoveCommand.java b/app/src/main/java/ru/otus/main_patterns/hw13/command/StopMoveCommand.java new file mode 100644 index 0000000..1cff9e1 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/command/StopMoveCommand.java @@ -0,0 +1,17 @@ +package ru.otus.main_patterns.hw13.command; + +import ru.otus.main_patterns.hw13.UObject; + +public class StopMoveCommand implements Command { + private final UObject ship; + + public StopMoveCommand(UObject ship) { + this.ship = ship; + } + + @Override + public void execute() { + ship.setProperty("velocity", 0); + ship.setProperty("moving", false); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/exception/AccessDeniedException.java b/app/src/main/java/ru/otus/main_patterns/hw13/exception/AccessDeniedException.java new file mode 100644 index 0000000..4da500b --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/exception/AccessDeniedException.java @@ -0,0 +1,8 @@ +package ru.otus.main_patterns.hw13.exception; + +public class AccessDeniedException extends RuntimeException { + + public AccessDeniedException(String message) { + super(message); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/handler/ActionHandler.java b/app/src/main/java/ru/otus/main_patterns/hw13/handler/ActionHandler.java new file mode 100644 index 0000000..baf5b3a --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/handler/ActionHandler.java @@ -0,0 +1,29 @@ +package ru.otus.main_patterns.hw13.handler; + +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.command.Command; + +/** + * Стратегия преобразования DSL-приказа (order) в исполняемую {@link Command}. + * + *

Каждая реализация отвечает за конкретный тип действия (например, {@code StartMove}, {@code + * Shoot}, {@code BurnFuel}) и регистрируется в {@link ru.otus.main_patterns.hw13.ActionRegistry} + * под соответствующим строковым ключом. + * + *

Вызывается из {@link ru.otus.main_patterns.hw13.command.InterpretCommand} при интерпретации + * игрового приказа. + * + * @see ru.otus.main_patterns.hw13.ActionRegistry + * @see ru.otus.main_patterns.hw13.command.InterpretCommand + */ +public interface ActionHandler { + + /** + * Преобразует приказ в команду для указанного игрового объекта. + * + * @param order DSL-приказ, содержащий параметры действия + * @param gameObject игровой объект, к которому применяется действие + * @return готовая к выполнению команда + */ + Command handle(UObject order, UObject gameObject); +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/handler/BurnFuelHandler.java b/app/src/main/java/ru/otus/main_patterns/hw13/handler/BurnFuelHandler.java new file mode 100644 index 0000000..d961920 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/handler/BurnFuelHandler.java @@ -0,0 +1,14 @@ +package ru.otus.main_patterns.hw13.handler; + +import ru.otus.main_patterns.hw13.IoC; +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.command.Command; + +public class BurnFuelHandler implements ActionHandler { + + @Override + public Command handle(UObject order, UObject gameObject) { + int fuelAmount = (int) order.getProperty("fuelAmount"); + return IoC.resolve("Commands.BurnFuel", gameObject, fuelAmount); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/handler/ShootHandler.java b/app/src/main/java/ru/otus/main_patterns/hw13/handler/ShootHandler.java new file mode 100644 index 0000000..923f3b8 --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/handler/ShootHandler.java @@ -0,0 +1,15 @@ +package ru.otus.main_patterns.hw13.handler; + +import ru.otus.main_patterns.hw13.IoC; +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.command.Command; + +public class ShootHandler implements ActionHandler { + + @Override + public Command handle(UObject order, UObject gameObject) { + String weaponId = (String) order.getProperty("weaponId"); + String direction = (String) order.getProperty("direction"); + return IoC.resolve("Commands.Shoot", gameObject, weaponId, direction); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/handler/StartMoveHandler.java b/app/src/main/java/ru/otus/main_patterns/hw13/handler/StartMoveHandler.java new file mode 100644 index 0000000..c330e4f --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/handler/StartMoveHandler.java @@ -0,0 +1,14 @@ +package ru.otus.main_patterns.hw13.handler; + +import ru.otus.main_patterns.hw13.IoC; +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.command.Command; + +public class StartMoveHandler implements ActionHandler { + + @Override + public Command handle(UObject order, UObject gameObject) { + int velocity = (int) order.getProperty("initialVelocity"); + return IoC.resolve("Commands.StartMove", gameObject, velocity); + } +} diff --git a/app/src/main/java/ru/otus/main_patterns/hw13/handler/StopMoveHandler.java b/app/src/main/java/ru/otus/main_patterns/hw13/handler/StopMoveHandler.java new file mode 100644 index 0000000..117869a --- /dev/null +++ b/app/src/main/java/ru/otus/main_patterns/hw13/handler/StopMoveHandler.java @@ -0,0 +1,13 @@ +package ru.otus.main_patterns.hw13.handler; + +import ru.otus.main_patterns.hw13.IoC; +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.command.Command; + +public class StopMoveHandler implements ActionHandler { + + @Override + public Command handle(UObject order, UObject gameObject) { + return IoC.resolve("Commands.StopMove", gameObject); + } +} diff --git a/app/src/test/java/ru/otus/main_patterns/hw13/IoCTest.java b/app/src/test/java/ru/otus/main_patterns/hw13/IoCTest.java new file mode 100644 index 0000000..db9c167 --- /dev/null +++ b/app/src/test/java/ru/otus/main_patterns/hw13/IoCTest.java @@ -0,0 +1,126 @@ +package ru.otus.main_patterns.hw13; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class IoCTest { + + @BeforeEach + void setUp() { + IoC.clear(); + } + + @Test + void shouldResolveFromGlobalContainer() { + IoC.register("test.key", args -> "global-value"); + + String result = IoC.resolve("test.key"); + + assertThat(result).isEqualTo("global-value"); + } + + @Test + void shouldPassArgumentsToFactory() { + IoC.register("sum", args -> (int) args[0] + (int) args[1]); + + int result = IoC.resolve("sum", 2, 3); + + assertThat(result).isEqualTo(5); + } + + @Test + void shouldThrowWhenGlobalDependencyNotFound() { + assertThatThrownBy(() -> IoC.resolve("missing")) + .isInstanceOf(RuntimeException.class) + .hasMessageContaining("Dependency not found"); + } + + @Test + void shouldResolveFromScopeWhenPresent() { + IoC.register("player1", "service", args -> "scoped-value"); + + String result = IoC.resolve("player1", "service"); + + assertThat(result).isEqualTo("scoped-value"); + } + + @Test + void shouldFallbackToGlobalWhenScopeMissingKey() { + IoC.register("service", args -> "global-value"); + + IoC.createScope("player1"); // empty scope + + String result = IoC.resolve("player1", "service"); + + assertThat(result).isEqualTo("global-value"); + } + + @Test + void shouldPreferScopeOverGlobal() { + IoC.register("service", args -> "global"); + IoC.register("player1", "service", args -> "scoped"); + + String result = IoC.resolve("player1", "service"); + + assertThat(result).isEqualTo("scoped"); + } + + @Test + void scopesShouldBeIsolatedBetweenPlayers() { + // act + IoC.register("player1", "service", args -> "p1"); + IoC.register("player2", "service", args -> "p2"); + + // assert + assertThat((String) IoC.resolve("player1", "service")).isEqualTo("p1"); + assertThat((String) IoC.resolve("player2", "service")).isEqualTo("p2"); + } + + @Test + void clearShouldRemoveAllRegistrations() { + // arrange + IoC.register("test", args -> "value"); + IoC.createScope("player1"); + IoC.register("player1", "test", args -> "value2"); + + // act + IoC.clear(); + + // assert + assertThatThrownBy(() -> IoC.resolve("test")).isInstanceOf(RuntimeException.class); + } + + @Test + void factoryShouldBeExecutedOnResolve() { + // act + AtomicBoolean executed = new AtomicBoolean(false); + IoC.register( + "lazy", + args -> { + executed.set(true); + return "ok"; + }); + + // act + IoC.resolve("lazy"); + + // assert + assertThat(executed.get()).isTrue(); + } + + @Test + void shouldFallbackWhenScopeDoesNotExist() { + // arrange + IoC.register("global", args -> "global-value"); + + // act + String result = IoC.resolve("unknown-player", "global"); + + // assert + assertThat(result).isEqualTo("global-value"); + } +} diff --git a/app/src/test/java/ru/otus/main_patterns/hw13/command/InterpretCommandTest.java b/app/src/test/java/ru/otus/main_patterns/hw13/command/InterpretCommandTest.java new file mode 100644 index 0000000..4b9574f --- /dev/null +++ b/app/src/test/java/ru/otus/main_patterns/hw13/command/InterpretCommandTest.java @@ -0,0 +1,157 @@ +package ru.otus.main_patterns.hw13.command; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import ru.otus.main_patterns.hw13.ActionRegistry; +import ru.otus.main_patterns.hw13.GameObject; +import ru.otus.main_patterns.hw13.IoC; +import ru.otus.main_patterns.hw13.UObject; +import ru.otus.main_patterns.hw13.exception.AccessDeniedException; +import ru.otus.main_patterns.hw13.handler.*; + +class InterpretCommandTest { + + @BeforeEach + void setUp() { + IoC.clear(); + ActionRegistry.clear(); + } + + // Happy path — команда выполняется + @Test + void shouldExecuteCommandSuccessfully() { + + // arrange + UObject ship = new GameObject(); + ship.setProperty("id", "1"); + ship.setProperty("ownerId", "player1"); + + UObject order = new GameObject(); + order.setProperty("id", "1"); + order.setProperty("action", "StartMove"); + + Command command = mock(Command.class); + ActionHandler handler = mock(ActionHandler.class); + + when(handler.handle(order, ship)).thenReturn(command); + + ActionRegistry.register("StartMove", handler); + + IoC.register("Objects.GetById", args -> ship); + + InterpretCommand interpret = new InterpretCommand(order, "player1"); + + // act + interpret.execute(); + + // assert + verify(handler, times(1)).handle(order, ship); + verify(command, times(1)).execute(); + } + + // Проверка доступа (owner check) + @Test + void shouldThrowWhenPlayerTriesToControlForeignObject() { + + // arrange + UObject ship = new GameObject(); + ship.setProperty("id", "1"); + ship.setProperty("ownerId", "player1"); + + UObject order = new GameObject(); + order.setProperty("id", "1"); + order.setProperty("action", "StartMove"); + + IoC.register("Objects.GetById", args -> ship); + + InterpretCommand cmd = new InterpretCommand(order, "enemyPlayer"); + + // act / assert + assertThatThrownBy(cmd::execute) + .isInstanceOf(AccessDeniedException.class) + .hasMessageContaining("чужим объектом"); + } + + // Проверка отсутствующего action в registry + @Test + void shouldFailWhenActionNotRegistered() { + // arrange + UObject ship = new GameObject(); + ship.setProperty("id", "1"); + ship.setProperty("ownerId", "player1"); + + UObject order = new GameObject(); + order.setProperty("id", "1"); + order.setProperty("action", "UnknownAction"); + + IoC.register("Objects.GetById", args -> ship); + + ActionRegistry.register("StartMove", mock(ActionHandler.class)); // другое действие + + InterpretCommand cmd = new InterpretCommand(order, "player1"); + + // act / assert + assertThatThrownBy(cmd::execute).isInstanceOf(RuntimeException.class); + } + + // Проверка IoC вызова объекта + @Test + void shouldUseIoCToResolveGameObject() { + // arrange + UObject ship = new GameObject(); + ship.setProperty("id", "1"); + ship.setProperty("ownerId", "player1"); + + UObject order = new GameObject(); + order.setProperty("id", "1"); + order.setProperty("action", "StartMove"); + + IoC.register("Objects.GetById", args -> ship); + + ActionHandler handler = mock(ActionHandler.class); + Command command = mock(Command.class); + + when(handler.handle(order, ship)).thenReturn(command); + + ActionRegistry.register("StartMove", handler); + + InterpretCommand cmd = new InterpretCommand(order, "player1"); + // act + cmd.execute(); + + // assert + verify(handler).handle(order, ship); + verify(command).execute(); + } + + // Проверка полного pipeline + @Test + void shouldPassThroughFullPipeline() { + // arrange + UObject ship = new GameObject(); + ship.setProperty("id", "1"); + ship.setProperty("ownerId", "player1"); + + UObject order = new GameObject(); + order.setProperty("id", "1"); + order.setProperty("action", "StartMove"); + + Command command = mock(Command.class); + ActionHandler handler = mock(ActionHandler.class); + + when(handler.handle(order, ship)).thenReturn(command); + + IoC.register("Objects.GetById", args -> ship); + ActionRegistry.register("StartMove", handler); + + // act + new InterpretCommand(order, "player1").execute(); + + // assert + verify(handler).handle(order, ship); + verify(command).execute(); + } +}