Архитектурная легкость
Как достигается легкость (простота) архитектуры
Главным критерием легкости архитектуры является возможность разбить монолитную архитектуру на минимально функциональные компоненты с минимальными зависимостями. И в последующем, как можно проще комбинировать различные компоненты между собой, не внося в свой проект крупную монолитную систему, а только то, что реально используется в проекте разработчика. В подавляющем большинстве случаев это достигается путем оверхейда, примером этому служит использование интерфейсов, ECS, MVC и тому подобные архитектурные надстройки. Все это свидетельство, лишь того, что текущие языки программирования с трудом дают удобные для этого механизмы. Но в C# для этого можно использовать т.н. partial class. Это позволяет включать компоненты исключительно путем физического копирования файлов, в которых определенны классы.
Самый простой и часто используемый класс это класс Item. Item позволяет идентифицировать, группировать и различать сущности в вашем проекте. Но что делать, если вы реализуете компонент, который перемещает сущность по некоторым правилам? А другой компонент его вращает. При этом у вас есть проекты, где нужно только перемещение, где нужно только вращение, и проекты где нужно и то и другое. Вот правильный подход с нулевым оверхедом. Partial классы обеспечивают вам возможность просто копировать в проект нужные компоненты и не создавать оверхед архитектурные надстройки. Это удивительно просто.
//TacItemMove.cs
namespace Tac.ItemMove
{
public class ItemMove
{
public void Move(BuildItem item, ... ) { ... }
}
}
namespace Tac
{
public partial class BuildItem : Item
{
public bool AllowMove = true;
}
}
//TacItemRotate.cs
namespace Tac.ItemRotate
{
public class ItemRotate
{
public void Rotate(BuildItem item, ... ) { ... }
}
}
namespace Tac
{
public partial class BuildItem : Item
{
public bool AllowRotate = true;
}
}
Выше демонстрируется только идея использования partial. Настоящий пример см. состав класса BuildItem, как он наполняется в зависимости от используемых компонентов. Так же учитывайте, что сами компоненты могут дополнять свою функциональность через исполоьзование partial. Например, см. в компоненте базовой камеры TopCamera и её расширение компонентом TacItemMove. Таким образом, partial так же автоматически выполняет задачу встраивания, что называют инъекцией/внедрением зависимостей, но без оверхеда.
Это не только одна ветвь наследования
Из-за упрощенных примеров, может сложится впечатление, что строится одна ветвь наследования Item наследуется от BuildItem, а все остальные наследуются дальше. Но это совсем не обязательно и не исключает агрегации. Рассмотрим более сложный пример.
namespace Tac.Agent
{
public partial class Agent : Item
{
}
public class AgentInPoint
{
public Agent Agent;
public GameTime EnterTime;
}
public partial class AgentPoint : Item
{
public Queue<AgentInPoint> Agents = new Queue<AgentInPoint>();
}
}
namespace Tac.Society
{
public partial class Business : AgentPoint
{
public Agent Owner;
public override void Init()
{
base.Init();
Title = "Частный дом";
}
}
}
Здесь мы видим класс описывающий агента (Agent), который сам по себе самостоятелен, и может создаваться на сцене компонентом ItemCreate, посредством того ,что наследуется от Item. Другой класс точка в которой может быть агент (AgentPoint), тоже самостоятельный объект, который может быть создан в сцене без агента. Но в процессе игры, мы будем отслеживать, какие агенты в него "заходили" и опосредственно через очередь Queue
И теперь самое главное. Префабы в сцене юнити могут одновременно содержать несколько компонентов. В нашем случае, префаб "Маша Иванова", будет содержать компонент Agent. А префаб "Ламбард Васильева" будет содержать два компонента BuildItem, который позволит игроку разместить строение в сцене, и Business, который будет ответственен за специфику самого бизнеса и описывать его поведение. Более высокоуровневые компоненты ItemBuild [не путайте с BuildItem] будут взаимодействовать с геймобъектом в сцене, через компонент BuildItem, а Job будет отправлять "Машу Иванову" в "Ломбард Васильева" используя компонент AgentPoint.
Соответственно, код будет распределен в классах в соответствии с их ролями и ответственностями. И не смотря на то, что различные части компонентов при использовании partial-подхода, будут склеиваться в один класс, нарушения или объединения ответственностей классов присходить не будет.
Нативная поддержка от Microsoft
Наш текущий подход с организацией папок и asmref — это практичное решение, навязанное существующими ограничениями языка C# и экосистемы Unity. (подробнее см. линковка)
Мы надеемся, что в будущих версиях C# появится нативная поддержка распределённой компиляции partial-классов, что позволит реализовать нашу архитектуру чище и элегантнее.
Почему это возможно и нужно?
-
Логическое продолжение: Механизм partial-классов уже существует. Сейчас все части должны компилироваться в одной сборке, но технически IL-код и метаданные уже содержат информацию о том, что класс является частичным. Расширить эту концепцию на несколько сборок — естественный шаг.
-
Требуется не так много: По сути, нужно лишь:
-
Разрешить ключевое слово partial в определениях классов в разных сборках (с указанием на "базовую" сборку).
-
На этапе линковки/загрузки сборок производить "мягкое" слияние метаданных этих частей.
-
Обеспечить безопасность: конфликты имён должны обнаруживаться и выдавать понятные ошибки на этапе загрузки, а не выполнения.
Как мог бы выглядеть идеальный синтаксис будущего:
// В сборке TacLibrary.dll (ядро библиотеки)
public partial class Unit {
public void BaseMethod() { ... }
}
// В сборке MyGame.dll (в игре от разработчика)
// Указание на то, что это расширение к классу из другой сборки
[assembly: Extends(typeof(TacLibrary.Unit))]
public partial class Unit {
public void NewGameSpecificMethod() { ... }
}