Около десяти лет назад, в 2017 году, некий Роберт Мартин выпустил книгу "Clean Architecture", в которой он рассказал о своем видение лучшей архитектуры для программных проектов. На самом деле он писал об этом еще в 2012 году, но тогда его идеи, к сожалению, не получили большой популярности. И даже сейчас, далеко не все понимают и используют эти идеи.

Если коротко, то суть его идей заключалась в том, что проекты должены состоять из нескольких слоев: пользовательский интерфейс (не обязательно графический), приложение, предметная область бизнеса и инфраструктура. Разумеется, что первые слои могут иметь зависимости только на следующие. Так же важным было то, что слой бизнеса не должен иметь зависимости на инфраструктуру, а должен использовать инверсию зависимостей. Хотя последнее, во многих случаях, - излишний оверкодинг. И вроде бы в этом нет ничего особо хитрого, но, к сожалению, потребовались десятилетия, чтобы хоть кто-то пришел к этим идеям и написал книгу.

А в игровой индустрии, складывается впечатление, что до сих пор о подобном никто всерьез не думает. К сожалению, существующие модные движки (особенно editor-first движки) имеют свои проблемы и ограничения, которые мешают реализовать эти идеи. Например, Unreal сильно зависит от своего фреймворка, который задаёт некую другую архитектуру. А Unity вынуждает использовать проклятый MonoBehaviour, ибо только MonoBehaviour может слушать события других компонентов. Возможно в Godot с этим лучше? Поэтому, ради интереса, я реализовал эти идеи в обычном .NET проекте, который назвал GameFramework.Pro. И теперь я хочу рассказать вам об этом фреймворке.

Обзор

Данный фреймворк определяет основные слои проекта, основные сущности и сервисы, а так же содержит минимальный набор утилит. Давайте рассмотрим это подробнее...

System

  • Check - набор проверок для аргументов и операций.

  • DisposableBase - освобождаемый объект. Содержит некоторый дополнительный функционал.

  • IDependencyProvider - локатор служб. Предоставляет запрашиваемые объекты и значения.

GameFramework.Pro

  • Main - корень проекта.

    • ProgramBase - главная сущность проекта. Создает другие сущности и предоставляет запрашиваемые зависимости.

  • UI - аудио-графический пользовательский интерфейс.

    • ThemeBase - сущность аудио темы. Проигрывает музыкальные плейлисты.

    • PlayListBase

    • ScreenBase - сущность графического экрана. Показывает дерево виджетов.

    • WidgetBase

    • ViewableWidgetBase

    • RouterBase - сервис менеджера состояния. Предоставляет методы для загрузки главного меню, загрузки/перезагрузки/выгрузки игры, а так же выхода из приложения.

  • App - модель приложения.

    • ApplicationBase - сущность приложения. Выполняет инициализацию, запуск главного цикла, запуск игры, а так же предоставляет хранилище и подобное.

  • Game - модель домена / бизнеса (в нашем случае самой игры).

    • GameBase - сущность игры. Содержит информацию об игре, игровые правила, состояние и сущности игроков.

    • PlayerBase - сущность игрока. Содержит информацию об игроке и состояние. Так же может обрабатывать ввод.

    • WorldBase - сущность мира. Содержит все остальные сущности.

    • EntityBase - любой значимый для игры объект.

Детальный обзор

Теперь давайте рассмотрим некоторые компоненты этого фреймворка более детально.

Program

// Программа содержит другие сущности и сервисы,
// а так же реализует паттерн локатор служб.
// Заметьте, что локатор служб считается анти-паттерном,
// но другие решения приводят к оверкодингу.
public abstract class ProgramBase : DisposableBase {
  
    public ProgramBase();
    private protected override void OnDisposeInternal();
  
}
// Каждая сущность и сервис разделены на две класса: базовый и расширенный.
// Все расширенные классы добавляют поддержку IDependencyProvider.
public abstract class ProgramBase2<TTheme, TScreen, TRouter, TApplication> : ProgramBase, IDependencyProvider
    where TTheme : ThemeBase
    where TScreen : ScreenBase
    where TRouter : RouterBase
    where TApplication : ApplicationBase {

    protected TTheme Theme { get; init; }
    protected TScreen Screen { get; init; }
    protected TRouter Router { get; init; }
    protected TApplication Application { get; init; }

    public ProgramBase2();
    private protected override void OnDisposeInternal();

    object? IDependencyProvider.GetValue(Type type, object? argument);

}

Theme

// Тема содержит плейлист,
// который проигрывает текущий список аудио треков.
// Заметьте, что я использовал машину состояний,
// которая позволляет вам легко управлять текущим плейлистом.
public abstract class ThemeBase : DisposableBase {

    protected StateMachine Machine { get; }

    public ThemeBase();
    private protected override void OnDisposeInternal();

}
// Заметьте, что вместо наследования,
// я использовал двухсторонюю связь между плейлистом и состоянием.
public abstract class PlayListBase {
    public sealed class State2 : State {

        public PlayListBase PlayList { get; }

        public State2(PlayListBase playList);
        protected override void OnDispose();

        protected override void OnActivate(object? argument);
        protected override void OnDeactivate(object? argument);

    }

    public State2 State { get; }

    public PlayListBase();
    protected internal abstract void OnDispose();
    private protected virtual void OnDisposeInternal();

    protected internal abstract void OnActivate(object? argument);
    protected internal abstract void OnDeactivate(object? argument);

}
public abstract class ThemeBase2<TRouter, TApplication> : ThemeBase
    where TRouter : RouterBase
    where TApplication : ApplicationBase {

    protected IDependencyProvider Provider { get; }
    protected TRouter Router { get; }
    protected TApplication Application { get; }

    public ThemeBase2();
    private protected override void OnDisposeInternal();

}
public abstract class PlayListBase2 : PlayListBase {

    protected IDependencyProvider Provider { get; }

    public PlayListBase2();
    private protected override void OnDisposeInternal();

}

Screen

// Экран содержит дерево виджетов,
// которые рисуют текущий пользовательский интерфейс.
// Заметьте, что я использовал машину дерева,
// которая позволляет вам легко управлять иерархией виджетов.
public abstract class ScreenBase : DisposableBase {

    protected TreeMachine Machine { get; }

    public ScreenBase();
    private protected override void OnDisposeInternal();

}
// Заметьте, что вместо наследования,
// я использовал двухсторонюю связь между виджетом и нодой.
public abstract class WidgetBase {
    public sealed class Node2 : Node {

        public WidgetBase Widget { get; }

        public Node2(WidgetBase widget);
        protected override void OnDispose();

        protected override void OnActivate(object? argument);
        protected override void OnDeactivate(object? argument);

        protected override void Sort(List<INode> children);

    }

    public Node2 Node { get; }

    public WidgetBase();
    protected internal abstract void OnDispose();
    private protected virtual void OnDisposeInternal();

    protected internal abstract void OnActivate(object? argument);
    protected internal abstract void OnDeactivate(object? argument);

    protected internal virtual void OnBeforeDescendantActivate(INode descendant, object? argument);
    protected internal virtual void OnAfterDescendantActivate(INode descendant, object? argument);
    protected internal virtual void OnBeforeDescendantDeactivate(INode descendant, object? argument);
    protected internal virtual void OnAfterDescendantDeactivate(INode descendant, object? argument);

    protected internal virtual void Sort(List<INode> children);

}
public abstract class ViewableWidgetBase : WidgetBase {

    public object View { get; protected init; }

    internal ViewableWidgetBase();
    private protected override void OnDisposeInternal();

}
public abstract class ViewableWidgetBase<TView> : ViewableWidgetBase
    where TView : notnull {

    protected new TView View { get; init; }

    public ViewableWidgetBase();
    private protected override void OnDisposeInternal();

}
public abstract class ScreenBase2<TRouter, TApplication> : ScreenBase
    where TRouter : RouterBase
    where TApplication : ApplicationBase {

    protected IDependencyProvider Provider { get; }
    protected TRouter Router { get; }
    protected TApplication Application { get; }

    public ScreenBase2();
    private protected override void OnDisposeInternal();

}
public abstract class WidgetBase2 : WidgetBase {

    protected IDependencyProvider Provider { get;}

    public WidgetBase2();
    private protected override void OnDisposeInternal();

}
public abstract class ViewableWidgetBase2<TView> : ViewableWidgetBase<TView>
    where TView : notnull {

    protected IDependencyProvider Provider { get;}

    public ViewableWidgetBase2();
    private protected override void OnDisposeInternal();

}

Router

// Роутер предоставляет ссылки на другие сущности.
public abstract class RouterBase : DisposableBase {
  
    public RouterBase();
    private protected override void OnDisposeInternal();
  
}
public abstract class RouterBase2<TTheme, TScreen, TApplication> : RouterBase
    where TTheme : ThemeBase
    where TScreen : ScreenBase
    where TApplication : ApplicationBase {

    protected IDependencyProvider Provider { get; }
    protected TTheme Theme { get; }
    protected TScreen Screen { get; }
    protected TApplication Application { get; }

    public RouterBase2();
    private protected override void OnDisposeInternal();

}

Application

// Приложение предоставляет лишь провайдер зависимостей.
// Остальные сущности являются такими же простыми,
// поэтому я не буду продолжать описывать их.
public abstract class ApplicationBase : DisposableBase {
  
    public ApplicationBase();
    private protected override void OnDisposeInternal();
  
}
public abstract class ApplicationBase2 : ApplicationBase {
  
    protected IDependencyProvider Provider { get; }
  
    public ApplicationBase2();
    private protected override void OnDisposeInternal();
  
}

Дополнение

Чтобы создать качественный проект, вам нужен фреймворк определяющий основные слои и компоненты вашего проекта. А чтобы качественно проектировать отдельные компоненты проекта, вам нужно понимать какие бывают компоненты и из чего они состоят. К сожалению, я не смог найти хоть какую-то информацию на эту тему. Но я смог прийти к некоторым идеям, о которых хочу далее рассказать.

Я могу предложить следующую классификацию объектов:

  • Entity - первичный объект (названный существительным), который, в идеале, только обрабатывает события и не предоставляет никакого API.

  • Service - первичный объект (названный отглагольным существительным), который, в идеале, только предоставляет свой API другим сущностям.

  • Data - вторичный объект, который, в идеале, только содержит какие-то данные. Иногда такие объекты имеют в названии суффиксы Info или Desc.

  • Utility - технический объект содержащий какие-то полезные методы.

Так же я могу предложить следующие классификации:

  • Для свойств объектов:

    • Property/Data - полезная нагрузка.

    • Property/Info - информация об объекте.

    • Property/Info/Question - вопрос об объекте.

    • Property/Info/Assertion - утверждение об объекте.

    • Property/Info/Directive - инструкция объекту.

    • Property/Reference/Object - ссылка на объект.

    • Property/Reference/Delegate - ссылка на метод.

    • Property/Reference/Event - ссылка на методы (обработчики событий).

  • Для методов объектов:

    • Method/Constructor

    • Method/Destructor

    • Method/Command/Query - команда объекту на получение данных.

    • Method/Command/Request - команда объекту на выполнение работы.

    • Method/Handler - обработчик события.

  • Для атрибутов объектов:

    • Attribute/Info

    • Attribute/Info/Directive

Заключение

В заключении, я хочу отметить, что большую сложность фреймворка, я выделил в две отдельные библиотеки StateMachine.Pro и TreeMachine.Pro. Эти библиотеки могли бы быть достойны отдельных статей, но думаю, их суть ясна из их названий.

Ссылки