Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions app/src/main/java/ru/otus/main_patterns/hw13/ActionRegistry.java
Original file line number Diff line number Diff line change
@@ -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<String, ActionHandler> 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();
}
}
19 changes: 19 additions & 0 deletions app/src/main/java/ru/otus/main_patterns/hw13/GameObject.java
Original file line number Diff line number Diff line change
@@ -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<String, Object> 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);
}
}
181 changes: 181 additions & 0 deletions app/src/main/java/ru/otus/main_patterns/hw13/HW13.java
Original file line number Diff line number Diff line change
@@ -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<String, UObject> ships = new HashMap<>();
ships.put("548", ship);
return ships;
});

// доступ к объектам через scope
IoC.register(
playerId,
"Objects.GetById",
args -> {
String id = (String) args[0];
Map<String, UObject> 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());
}
}
121 changes: 121 additions & 0 deletions app/src/main/java/ru/otus/main_patterns/hw13/IoC.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Предоставляет двухуровневую систему разрешения зависимостей:
*
* <ul>
* <li><b>Глобальный уровень</b> — зависимости, доступные всем участникам.
* <li><b>Уровень scope</b> — изолированные зависимости для конкретного игрока/контекста.
* </ul>
*
* <pre>
* Global IoC (общие зависимости)
* ↑ fallback
* Player Scope (player1) — приватные зависимости игрока 1
* Player Scope (player2) — приватные зависимости игрока 2
* </pre>
*
* <p>При разрешении зависимости из scope сначала ищется локальная фабрика, а при её отсутствии —
* происходит fallback на глобальный контейнер.
*/
public class IoC {

/** Глобальный реестр фабрик: ключ зависимости → фабричная функция. */
private static final Map<String, Function<Object[], Object>> container = new HashMap<>();

/** Изолированные scope: идентификатор scope → локальный реестр фабрик. */
private static final Map<String, Map<String, Function<Object[], Object>>> scopes =
new HashMap<>();

/**
* Регистрирует фабрику зависимости в глобальном контейнере.
*
* @param key уникальный ключ зависимости
* @param factory фабричная функция, принимающая аргументы и возвращающая экземпляр
*/
public static void register(String key, Function<Object[], Object> 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<Object[], Object> 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 <T> ожидаемый тип возвращаемого объекта
* @return экземпляр, созданный фабрикой
* @throws RuntimeException если зависимость с указанным ключом не найдена
*/
@SuppressWarnings("unchecked")
public static <T> T resolve(String key, Object... args) {
Function<Object[], Object> factory = container.get(key);
if (factory == null) {
throw new RuntimeException("Dependency not found: " + key);
}
return (T) factory.apply(args);
}

/**
* Разрешает зависимость из указанного scope с fallback на глобальный контейнер.
*
* <p>Порядок поиска:
*
* <ol>
* <li>Локальный scope (по {@code scopeId})
* <li>Глобальный контейнер (если в scope не найдено)
* </ol>
*
* @param scopeId идентификатор scope
* @param key ключ зависимости
* @param args аргументы для фабричной функции
* @param <T> ожидаемый тип возвращаемого объекта
* @return экземпляр, созданный фабрикой
* @throws RuntimeException если зависимость не найдена ни в scope, ни глобально
*/
@SuppressWarnings("unchecked")
public static <T> T resolve(String scopeId, String key, Object... args) {
Map<String, Function<Object[], Object>> 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();
}
}
15 changes: 15 additions & 0 deletions app/src/main/java/ru/otus/main_patterns/hw13/UObject.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package ru.otus.main_patterns.hw13;

/*
Универсальный контейнер данных, который:
- хранит приказ
- хранит игровые объекты
- хранит параметры команд

Делает систему динамической.
*/
public interface UObject {
Object getProperty(String key);

void setProperty(String key, Object value);
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package ru.otus.main_patterns.hw13.command;

public interface Command {
void execute();
}
Loading
Loading