Парадигма

Парадигма пространственно-потоковой логики (ППЛ, Spatial-Flow-Logic (SFL))

Есть одна очень устойчивая идея в архитектуре программного обеспечения: чем сильнее мы отделим предметную логику от среды исполнения, тем лучше будет архитектура. Логику стараются освободить от UI, таймеров, ввода, графики, базы данных, Unity, WinForms, консоли и вообще от всего, что напоминает реальный исполняющий мир. В пределе мечта выглядит почти платоновски: где-то существует чистая логика, а среда лишь аккуратно вызывает её методы.

Проблема в том, что в реальных интерактивных системах эта картина плохо описывает сам предмет. Игра не состоит из вычислений, к которым потом случайно приставили Unity. В игре существуют персонажи, здания, компании, время, пространство, события, ресурсы и отношения между ними. Логика не просто вычисляет что-то в вакууме. Она является частью мира и постоянно взаимодействует с его носителями.

ППЛ начинается не с попытки устранить эту зависимость, а с признания простого факта: зависимость от среды неизбежна. Вопрос не в том, как сделать так, чтобы её не существовало, а в том, как сделать её понятной, классифицируемой и контролируемой.

Для этого вводятся три категории. Logic — логика, которую можно выразить без непосредственного обращения к среде исполнения. Flow — логика, связанная с потоком существования и исполнения: временем, событиями, вводом, UI, настройками среды и тому подобным. Spatial — частный случай Flow, где принципиальным становится пространство: положение объекта, физические взаимодействия, Transform, Collider и всё, что связано с существованием объекта в пространственной сцене.

В этом смысле Spatial и Flow можно рассматривать почти философски. Человек не мыслит вне времени и пространства. Пространство отвечает на вопрос «где существует объект», а поток — «когда и в каком процессе он существует». ППЛ не пытается выбросить эти категории из логики программы. Она делает их явными.

Возьмём агента. У агента есть здоровье, цель, навыки, занятость, состояние, способность получать урон и так далее. Значительная часть этих правил может быть выражена в отдельной AgentLogic. Она не обязана знать о Transform или Collider. В этом смысле это действительно логика в вакууме.

Но возникает простой вопрос: что толку от такой логики, если она никогда не будет применена к реальному агенту? Само существование AgentLogic ещё не означает существование персонажа в игровом мире. Поэтому появляется Agent как её носитель. Agent находится в пространстве, участвует в потоке игры, получает события среды и предоставляет своей логике возможность действовать.

И здесь начинается важное отличие ППЛ от идеи стерильного разделения. Agent не является «грязной оболочкой», которую приходится терпеть ради Unity. Он является объектом мира, носителем AgentLogic. Логика отделена от среды настолько, насколько это действительно необходимо, но не отрезана от объекта, через который она существует в мире.

Из этого возникает следующий вопрос: если Agent содержит AgentLogic, должен ли внешний код знать о существовании AgentLogic? Ответ ППЛ — нет. Внешний код должен работать с Agent как с агентом. Если логика имеет публичное состояние Health, IsDead, TargetId или методы ApplyDamage и SetTarget, TacCompile автоматически проецирует их на Agent. Снаружи разработчик видит свойства и методы Agent, а не внутреннюю реализацию AgentLogic.

Это принципиально отличается от ручного написания бесконечного количества прокси-методов. Здесь проекция является не повторением кода, а правилом архитектуры. Logic объявляет своё публичное содержание, а препроцессор строит внешний контракт её носителя.

Причём эта проекция не означает, что внешний мир получает полный доступ к Logic. У носителя и у внешнего мира существуют разные двери. Сам Agent имеет полный доступ к собственной AgentLogic. Это его внутренняя часть. Но другой Flow не должен автоматически получать возможность произвольно менять внутреннее состояние AgentLogic.

По умолчанию состояние логики проецируется наружу для чтения. Если же некоторое состояние действительно должно изменяться внешней средой, разработчик явно помечает его как разрешённое для записи. Например, возраст человека, усталость или зарплата могут изменяться не потому, что сам персонаж решил изменить их внутренним правилом, а потому, что соответствующее изменение пришло извне. Тогда TacCompile создаёт отдельный синтаксический метод изменения.

Это важный момент для понимания инкапсуляции. Инкапсуляция — не в том, чтобы превратить каждое публичное поле в приватное поле с тривиальным getter и setter. Само по себе это часто меняет только синтаксис, но не права доступа. Если наружу всё равно разрешены и чтение, и запись, то добавление свойства вместо поля ещё не создало архитектурного ограничения.

Настоящая инкапсуляция начинается там, где различаются операции и права. Читать можно, изменять нельзя. Изменять можно только определённым способом. Внутренний носитель может делать больше, чем внешний потребитель. ППЛ формализует именно эти различия.

Но одной проекции Logic наружу недостаточно. Logic должна иногда обращаться к среде. И здесь появляется другая сторона системы — внутренние делегаты.

Представим здание. У него есть точка входа, и оно должно определить, стоит ли у двери конкретный агент. Это нельзя решить чистой логикой. Нужно обратиться к физической сцене, найти Collider, проверить положение, получить Agent из GameObject и сравнить его идентификатор. Это Spatial-операция.

Однако сама по себе проверка «стоит ли агент у входа» ещё не является правилом поведения здания. Правило находится уровнем выше: нужно определить, какие агенты вообще направляются в это здание, проверить рабочее время, проверить дополнительные требования, а затем решить, принимать ли агента.

Получается интересная последовательность. Logic задаёт вопрос пространству. Spatial выполняет запрос и возвращает результат. Logic использует результат для принятия решения. При этом пространственная операция не становится Logic только потому, что её вызвала Logic.

Для этого Logic может объявить внутреннюю capability в форме делегата. TacCompile ищет соответствующий метод у Flow-хоста и автоматически связывает их. Если соответствующего метода нет, вместо молчаливой ошибки можно создать диагностический stub. Если же делегат объявлен публичным, это уже означает, что capability разрешено выставить наружу.

Так возникает очень важная идея: Logic не получает абстрактный «доступ к Unity». Она получает конкретную возможность, предоставленную её Flow. Logic может спросить среду: «находится ли агент у входа», но не обязана знать, как именно среда отвечает на этот вопрос. Сегодня ответ получен через Unity Physics, завтра через другой пространственный движок, тестовый стенд или другую реализацию.

Именно здесь становится понятна настоящая роль TacCompile. Это не просто генератор скучного boilerplate. Он компилирует архитектурные отношения. Разработчик декларативно указывает, какая Logic является внутренней частью Flow, какие её члены должны быть представлены наружу, какие свойства можно изменять извне и какие возможности Logic требует от собственного носителя. Препроцессор превращает эти декларации в конкретные связи и одновременно может обнаруживать нарушения контракта.

До этого момента можно было бы предположить, что всё это лишь ещё один способ изолировать чистую логику. Но самое интересное начинается, когда мы поднимаемся на уровень выше.

Представим Society и SocietyLogic. В обществе существуют люди и компании. Кажется заманчивым сказать, что SocietyLogic должна работать только с PersonLogic и CompanyLogic. Ведь так логика останется «чистой». Но это будет искусственная чистота.

Общество имеет дело не с абстрактными логиками компаний. Оно имеет дело с реальными компаниями. Компания является участником общества, хранит сырьё, имеет капитал, производит товары, делает заказы, получает деньги, взаимодействует с рынком. Поэтому SocietyLogic вполне естественно содержит коллекцию компаний как объектов Flow.

На первый взгляд это выглядит смешением: Logic содержит ссылки на Flow. Но это не смешение ответственности. SocietyLogic не начинает заниматься Transform компании, её Collider или Unity UI. Она просто работает с компанией как с сущностью предметного мира.

Если SocietyLogic должна закупать сырьё для всех предприятий, она должна перебрать реальные компании, посмотреть их производство, проверить складские запасы, вызвать заказ и учесть финансовое состояние. Было бы странно заставлять её сначала получать внутреннюю CompanyLogic, затем вручную обходить отдельный слой посредников, только чтобы убедиться, что она не нарушила догму о чистоте.

ППЛ говорит иначе: SocietyLogic работает с Company как с носителем сущности мира. А Company уже сама построена по ППЛ и автоматически предоставляет наружу необходимую часть своей CompanyLogic.

Получается рекурсивная структура. SocietyLogic работает с Company. Company содержит CompanyLogic. CompanyLogic в свою очередь может пользоваться своим Flow и предоставлять его возможности наружу. На следующем уровне Person, Building, Market и другие сущности работают точно по тому же принципу.

Это позволяет строить не дерево абстрактных логик, а связный граф реальных сущностей мира, внутри которых находятся отделённые логики.

Именно поэтому ППЛ не пытается заставить SocietyLogic работать с CompanyLogic. Она позволяет SocietyLogic работать с Company, но при этом не раскрывает ей внутреннее устройство Company. Это гораздо ближе к предметной модели.

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

Это также хорошо объясняет, почему отложенное создание объектов оказывается не просто техническим приёмом. Если коллекции людей и компаний создаются тогда, когда создаётся соответствующая среда, предметная модель может быть материализована позже. Здесь естественно появляется возможность связать тот же граф объектов с механизмом сохранения данных и ORM. Если объекты являются реальными сущностями предметного мира, а их создание и связывание контролируется средой, появляется возможность подключить Entity Framework или другой ORM к уже сформированной модели. Это отдельная тема, но архитектурно она возникает совершенно естественно.

Есть, однако, редкие случаи, когда Logic действительно должна работать непосредственно с другой Logic. И пример школы здесь особенно показателен.

Школа обучает человека. Но обучение в данном случае не имеет отношения к пространственному существованию человека. Школе не важно, где находится его Transform, есть ли у него Collider и вообще существует ли он уже в пространственной сцене. Предметом обучения являются знания: математика, языки, физика, программирование, история, философия и другие навыки.

Поэтому School может принять реального Person как внешний объект, но внутри передать его PersonLogic. Это не автоматическая проекция Flow на другой Flow, а сознательный переход на уровень чистого содержания.

Причём такой персонаж может существовать в логическом смысле раньше, чем появится в пространстве. Его можно создать как мыслимого человека, обучить, сформировать его навыки, дать ему образование, определить профессию и только после этого материализовать как пространственного персонажа.

Это показывает, что Logic и Spatial — не две обязательные половины каждой сущности. Пространственное существование может быть лишь одной формой существования объекта. Некоторые процессы имеют смысл до появления объекта в пространстве.

Поэтому прямое взаимодействие Logic с другой Logic в ППЛ не запрещено. Оно просто не является правилом по умолчанию. Если операция относится непосредственно к чистому содержанию сущности и не требует её пространственного воплощения, такое взаимодействие оправдано.

На этом примере особенно хорошо видно, почему ППЛ не пытается выразить всё одним универсальным правилом. По умолчанию Logic работает с Flow, потому что предметная логика должна работать с реальными сущностями мира. Но иногда предметом операции является не сущность в пространстве, а её чистое логическое содержание. Тогда связь Logic с Logic становится естественной.

Именно поэтому ППЛ не является очередной попыткой построить стерильную архитектуру, где каждый слой общается только с соседним слоем через набор интерфейсов. Её цель другая.

Логика не должна быть изолирована от мира настолько, чтобы перестать описывать сам мир. Но она также не должна быть намертво связана с конкретной реализацией среды. Между этими крайностями существует третий путь: разделять природу зависимости.

Если логика зависит от вычислений и правил — это Logic. Если она зависит от процесса исполнения — это Flow. Если она зависит от пространственного существования — это Spatial. Если одна Logic должна воспользоваться возможностью среды, она делает это через внутреннюю capability. Если Flow должен представить часть своей Logic наружу, TacCompile строит этот контракт автоматически. Если внешняя среда должна изменить внутреннее состояние, это должно быть явно разрешено. Если две чистые логики действительно должны взаимодействовать напрямую, это допускается как осознанное исключение.

В таком подходе зависимость не считается архитектурным поражением.

Она становится классифицируемой.

И это, пожалуй, главное отличие ППЛ от традиционного стремления к «чистой логике». Невозможно убрать из реальной программы время, пространство, людей, компании, события, рынок, UI и другие элементы мира. Можно только сделать вид, что их нет, спрятав их за достаточным количеством интерфейсов.

ППЛ предлагает не делать вид.

Она говорит: среда существует. Логика существует. Они связаны. Но связь должна иметь определённую природу, направление и правила.

Поэтому Flow не является «грязным слоем», который необходимо держать как можно дальше от Logic. Flow — это носитель логики в мире.

Spatial не является противоположностью Logic. Spatial — это логика существования и взаимодействия в пространстве.

Logic не обязана быть логикой «в вакууме». Она может быть логикой реальных людей, компаний и общества, оставаясь при этом отделённой от непосредственной реализации среды.

А TacCompile нужен именно потому, что большая часть этих отношений достаточно формальна, чтобы не заставлять разработчика писать их вручную.

В конечном счёте ППЛ предлагает отказаться от вопроса «насколько чиста эта логика?» и заменить его более практичным вопросом: «какова природа её зависимости от мира?»

И если ответ на этот вопрос можно выразить правилами, значит архитектуру можно не только обсуждать, но и компилировать.