Как организуешь архитектуру модуля без View?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Архитектура модуля без View в Unity: подходы и примеры реализации
Организация архитектуры модуля без View подразумевает создание чисто логического ядра, которое полностью отделено от Unity-специфичных компонентов (MonoBehaviour, трансформов, GameObject). Это ключевой принцип для достижения тестируемости, переиспользуемости и независимости бизнес-логики от движка.
Ключевые принципы и паттерны
-
Чистый C# Domain Layer (Слой предметной области): Ядро модуля состоит из обычных классов C#, не наследуемых от
MonoBehaviour. Эти классы содержат всю бизнес-логику, состояние (данные) и правила. -
Разделение ответственности: Четкое разделение на:
* **Модель (Model):** Данные и состояние системы.
* **Сервисы (Services):** Классы, реализующие сложную логику, часто с зависимостями.
* **Команды/Случаи использования (Commands/Use Cases):** Классы, инкапсулирующие конкретные пользовательские сценарии.
-
Dependency Injection (Внедрение зависимостей): Для связывания независимых слоев и обеспечения тестируемости. Все зависимости (например, доступ к данным, другим системам) передаются в класс через конструктор или свойства, а не создаются внутри.
-
Принцип инверсии зависимостей (DIP): Высокоуровневые бизнес-модули не должны зависеть от низкоуровневых деталей (вроде Unity API). И те, и другие должны зависеть от абстракций (интерфейсов).
Пример реализации: модуль инвентаря
Допустим, мы создаем систему инвентаря.
1. Слой Domain (Ядро, без Unity)
Этот слой можно собрать в отдельную Assembly Definition (.asmdef) для физического разделения.
Модель данных (Item, InventorySlot):
// Чистый C# класс. Не MonoBehaviour!
[System.Serializable] // Для возможной сериализации данных
public class InventoryItem
{
public string Id { get; private set; }
public string Name { get; private set; }
public int MaxStack { get; private set; }
public int CurrentAmount { get; private set; }
public InventoryItem(string id, string name, int maxStack)
{
Id = id;
Name = name;
MaxStack = maxStack;
CurrentAmount = 1;
}
public bool TryStackWith(InventoryItem otherItem)
{
if (Id != otherItem.Id) return false;
if (CurrentAmount >= MaxStack) return false;
int spaceLeft = MaxStack - CurrentAmount;
int amountToAdd = Mathf.Min(spaceLeft, otherItem.CurrentAmount);
CurrentAmount += amountToAdd;
otherItem.CurrentAmount -= amountToAdd;
return true;
}
}
public class InventorySlot
{
public InventoryItem Item { get; private set; }
public bool IsEmpty => Item == null;
public bool TryPlaceItem(InventoryItem item)
{
if (!IsEmpty) return false;
Item = item;
return true;
}
public void Clear() => Item = null;
}
Интерфейсы для сервисов (абстракции):
public interface IItemRepository
{
InventoryItem GetItemById(string id);
bool ItemExists(string id);
}
public interface IInventoryService
{
bool AddItemToFirstEmptySlot(InventoryItem item);
void SwapSlots(int indexA, int indexB);
}
Реализация Inventory (ядро логики):
public class Inventory : IInventoryService
{
private readonly InventorySlot[] _slots;
private readonly IItemRepository _itemRepository; // Зависимость через интерфейс
// Зависимости инжектируются через конструктор
public Inventory(int capacity, IItemRepository itemRepository)
{
_slots = new InventorySlot[capacity];
for (int i = 0; i < capacity; i++)
{
_slots[i] = new InventorySlot();
}
_itemRepository = itemRepository;
}
public bool AddItemToFirstEmptySlot(InventoryItem item)
{
foreach (var slot in _slots)
{
if (slot.IsEmpty)
{
return slot.TryPlaceItem(item);
}
else if (slot.Item.TryStackWith(item))
{
return true;
}
}
return false;
}
// ... остальные методы
}
2. Слой Infrastructure / Adapters (Адаптеры для Unity)
Эти классы уже могут быть MonoBehaviour и живут в сцене. Их задача — связать ядро с движком.
Реализация репозитория для Unity (например, с ScriptableObject):
// Этот класс уже может быть MonoBehaviour или ScriptableObject
public class ScriptableObjectItemRepository : MonoBehaviour, IItemRepository
{
[SerializeField] private ItemDatabase _itemDatabase; // ScriptableObject с данными
public InventoryItem GetItemById(string id)
{
var soData = _itemDatabase.GetItemData(id);
if (soData == null) return null;
// Адаптируем данные ScriptableObject в нашу доменную модель
return new InventoryItem(soData.Id, soData.DisplayName, soData.MaxStack);
}
}
Фасад/Композиционный корень для инъекции зависимостей:
public class InventoryCompositionRoot : MonoBehaviour
{
private Inventory _inventoryCore;
void Awake()
{
// 1. Создаем "низкоуровневые" адаптеры
var itemRepo = GetComponent<IItemRepository>();
// 2. Собираем ядро, инжектируя зависимости
_inventoryCore = new Inventory(20, itemRepo);
// 3. Можем передать ядро другим MonoBehaviour-системам
var saveSystem = GetComponent<InventorySaveSystem>();
saveSystem.Initialize(_inventoryCore);
var inputHandler = GetComponent<InventoryInputHandler>();
inputHandler.Initialize(_inventoryCore);
}
}
Преимущества такого подхода
- Тестируемость: Ядро
Inventoryможно unit-тестировать без Unity Test Runner, с использованием моков дляIItemRepository. - Переиспользуемость: Логику инвентаря можно перенести в другой проект или даже на сервер (Backend) без изменений.
- Чистота архитектуры: Бизнес-правила (максимальный размер стака, логика объединения) изолированы и легко изменяемы.
- Независимость от движка: Если потребуется сменить способ представления (например, на другой UI-фреймворк), ядро останется нетронутым. Изменятся только адаптеры и слой представления.
Таким образом, организация модуля без View сводится к выделению независимого доменного слоя на чистых классах C# и последующему подключению его к Unity-экосистеме через слой адаптеров, реализующих определенные интерфейсы. Это требует большего объема начального кода, но многократно окупается при поддержке и расширении средних и крупных проектов.