← Назад к вопросам

Как организуешь архитектуру модуля без View?

1.0 Junior🔥 241 комментариев
#Паттерны проектирования

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура модуля без View в Unity: подходы и примеры реализации

Организация архитектуры модуля без View подразумевает создание чисто логического ядра, которое полностью отделено от Unity-специфичных компонентов (MonoBehaviour, трансформов, GameObject). Это ключевой принцип для достижения тестируемости, переиспользуемости и независимости бизнес-логики от движка.

Ключевые принципы и паттерны

  1. Чистый C# Domain Layer (Слой предметной области): Ядро модуля состоит из обычных классов C#, не наследуемых от MonoBehaviour. Эти классы содержат всю бизнес-логику, состояние (данные) и правила.

  2. Разделение ответственности: Четкое разделение на:

    *   **Модель (Model):** Данные и состояние системы.
    *   **Сервисы (Services):** Классы, реализующие сложную логику, часто с зависимостями.
    *   **Команды/Случаи использования (Commands/Use Cases):** Классы, инкапсулирующие конкретные пользовательские сценарии.

  1. Dependency Injection (Внедрение зависимостей): Для связывания независимых слоев и обеспечения тестируемости. Все зависимости (например, доступ к данным, другим системам) передаются в класс через конструктор или свойства, а не создаются внутри.

  2. Принцип инверсии зависимостей (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-экосистеме через слой адаптеров, реализующих определенные интерфейсы. Это требует большего объема начального кода, но многократно окупается при поддержке и расширении средних и крупных проектов.