Комментарии 22
Очень интересная статья. Думаю, что стоит потратить время на ее усвоение.
В своем последнем пет-проекте ( https://habr.com/ru/articles/848836/ ) я тоже столкнулся с проблемами хорошего структурирования кода. Как в части организации графического интерфейса, так и на уровне алгоритмов работы с данными. Неимоверными усилиями (в результате чего, пришлось даже полностью бросить пить) я вышел на первую рабочую версию, которая, естественно, не может быть идеальной. Поэтому, при разработке следующих версий, надеюсь, воспользоваться вашими советами по эффективной организации кода, но, сначала, ваши идеи еще надо переварить.
Идеально было бы, если бы вы предоставили тестовый проект на Visual Studio C++, с использованием GUI. Для меня лично, лучший способ усвоения новых идей – работа с самодостаточными прототипами кода. Но я не настаиваю, понимаю, что у вас есть свои планы на использование вашего личного времени.
В любом случае, за статью – спасибо!
Статья отличная.
Есть некоторая проблема воспроизводимости во многих материалах по архитектуре ПО.
Она состоит в том, что многие подходы и знания, являющиеся исходными для общепринятых решений, таких как ООП, паттерны, чистые функции и т.д считаются самоочевидными.
Вместе с тем, они, во первых, неочевидны, а во вторых, это создает разрыв между самими правилами и условиями при которых их нужно применять.
Получается культ-карго, когда нужно применять подход просто потому что нужно.
Жалко, что не очень много пояснений про иерархии и декомпозиции более сложных решений, если наш слой приложения становится слишком сложный, или, к примеру, основная сложность состоит в структурировании объектов слоя обработки. Например игры - приложения с полным стейтом, где сохранять нужно сравнительно немного по сравнению с тем, сколько нужно организовывать объектов для игровой симуляции.
Есть некоторая проблема воспроизводимости во многих материалах по архитектуре ПО.
...
Например игры
Я к геймдеву вообще никакого отношения не имею, поэтому возможно фигню сморожу, но у меня такое мнение - львиная доля информации по архитектуре (включая мой доклад/статью), посвящена архитектуре (бакэндов) информационных систем. А "естество" бакенда сильно отличается от "естества" игры и поэтому эта информация плохо ложится на реалии разработки игр.
Жалко, что не очень много пояснений про иерархии и декомпозиции более сложных решений
Опять же, для бэков у меня есть решение. Актуальную версию пока не расписал, но она базируется на алгоритме декомпозиции на базе эффектов (это тоже статья по мотивам доклада и этот доклад уже есть в публичном доступе на Ютубе).
Именно так, именно поэтому зачастую функциональщики не понимают игрожелов и бездумный перенос опыта оканчивается плачевно.
Ну, мы тоже пишем сервера)
Ещё, кстати, мысль дилетанта - а оно (архитектура) вам вообще надо?:)
Бакендеры заморачиваются, потому что бакэнды живут годами и десятилетиями. И покопавшись в говне мамонта невольно начинаешь задумываться о том, чтобы в следующий раз позаботиться о себе и сделать понятную и поддерживаемую кодовую базу.
А игры, кажется - максимально быстро запилил, зарелизал и ещё до релиза забыл про первую игру и начал пилить вторую.
Раз существует https://en.wikipedia.org/wiki/Entity_component_system, значит иногда приходится поддерживать. Плюс одна игра сама может превратиться в такого мамонта, что до релиза не доживёт
Игры тоже поддерживаются десятилетиями, WoW 20 лет вот недавно встретил.
Плюс многие бывают довольно массивными, их пилят десятки людей.
Многие игры довольно сложны, для сети нужны архитектуры для детерминированных симуляций к примеру.
По сути даже если ты не задумываешься об архитектуре - какая то архитектура все равно есть, сами какие то связи появляются.
В целом если ты один пилишь игру можно действительно почти не париться - плюс многие движки предоставляют мощный фреймворк, который навязывает как минимум какие то основы организации.
Интересно, что мы невольно пришли у себя на проекте к такой же архитектуре, только двигаясь из классического DDD. Мы объявляем агрегат (но это агрегат не в терминологии Эванса, а как раз комбинация сущностей, что-то вроде вашего AssignData
), собираем его за один запрос целиком в слое инфраструктуры, а сам он при этом выполняет свою бизнес-логику независимо от IO. Широко используется сокрытие в рамках одной сборки (модификатор internal
в C#, на котором мы пишем), чтобы слою приложения вообще нельзя было сломать логику.
В вашем случае, например, в примере с Barcoder слой приложения может не проверить единственность одной из коробок, и тогда Domain-слой сработает неправильно. Как мы делали мы. Вообще, паковать одну коробку в несколько и несколько в одну выглядит разными бизнес-операциями. Но если нужно в одной, то:
Domain:
public class BoxesAggregate
{
public IReadOnlyList<Box> Boxes { get; private set; }
public IReadOnlyList<OsgBox> OsgBoxes { get; private set; }
public BoxesAggregate(List<Box> boxes, List<OsgBox> osgBoxes) {
if (boxes == null || osgBoxes == null || (boxes.Count > 1 && osgBoxes.Count > 1))
{
throw new Exception("Перепаковать можно только одну во много или наоборот");
}
Boxes = boxes.AsReadOnly();
OsgBoxes = osgBoxes.AsReadOnly();
}
public void Repack()
{
if (boxes.Count == 1)
{
// код перепаковки 1
}
else
{
// код перепаковки 2
}
}
public (List<BoxId>, List<OsgBoxId>) GetIds()
{
// возвращаем идентификаторы коробок для связывания в слое приложения
}
}
Application:
public interface IAggregateRepo
{
public BoxesAggregate BuildFor(List<BoxId> boxIds, List<OsgBoxId> osgBoxIds);
}
public class RepackBoxesAppService(
IAggregateRepo agRepo,
IBoxRepo boxRepo,
IOsgBoxRepo osgBoxRepo,
ILinkRepo linkRepo
)
{
public void Handle(RepackBoxesRequest request)
{
// разумеется всё это одна транзакция, и никакие данные не сохранятся частично
BoxesAggregate aggregate = agRepo.BuildFor(request.BoxIds, request.OsgBoxIds);
aggregate.Repack();
boxRepo.SaveChanges(aggregate.Boxes);
osgBoxRepo.SaveChanges(aggregate.OsgBoxes);
linkRepo.UpdateLinks(aggregate.GetIds());
}
}
Infrastructure:
public class AggregateRepo : IAggregateRepo
{
// реализация сборки агрегата путём запроса сразу всех коробок и коробов,
// и затем вызова конструктора BoxesAggregate
}
Таким образом, почти никакие манипуляции из слоя приложения не сломают данные и не смогут обойти ограничения в бизнес-логике.
Вообще, паковать одну коробку в несколько и несколько в одну выглядит разными бизнес-операциями
Да, согласен, но когда реально делали проект я этот момент упустил (видимо и правда был с похмелья:) ), а сейчас хотелось оставить код максимально приближенным к реальному.
А за BoxesAggregate - спасибо. Вы, кажется, наконец-то мне объяснили идею "по агрегату на запрос", которую я полгода правильно понять не мог и из-за этого отвергал. Сейчас она заиграла для меня новыми красками, возможно утащу к себе.
Очень помогает в этом подходе то, что API отдельных сущностей может быть более свободным, но закрытым для использования вне домена. Грубо говоря, часто в энтерпрайзе кейсы типа: "отправить письмо можно только тогда, когда к нему приложен документ в статусе Согласовано". Тогда собираем агрегат из письма и документа, в самом письме метод отправки делаем internal (в джаве это package private, вроде как), а публичный метод отправки остается только у агрегата.
Поправьте меня, если я неправ, но похоже в этой секции нарушены границы транзакционности:
boxRepo.SaveChanges(aggregate.Boxes);
osgBoxRepo.SaveChanges(aggregate.OsgBoxes);
linkRepo.UpdateLinks(aggregate.GetIds());
Первое. Что произойдет в случае, если
boxRepo.SaveChanges(aggregate.Boxes);
выполнится успешно, а последующие инструкции провалятся (недоступность сети, системные прерывания и тд)?
Второе. Подход с выделением частичного агрегата или композиция сущностей разных агрегатов в один для проведения бизнес операции небезопасна, т.к. этот подход не учитывает конкуретность записи. Если вы беспокоитесь о целостности агрегата, он должен быть полностью гидрирован, версионирован и должна быть применены принципы оптимистичной конкуренции.
В противном случае вы не можете гарантировать целостность агрегата, что в свою очередь ведет к тому, что вы не можете гарантировать корректность и целостность данных в системе.
Что думаете по этому поводу?
Идея структурного дизайна конечно имеет право на жизнь. Но это всё покрывает только примитивные программы, типа CRUD (хоть с тысячей ручек), где самая главная проблема - это отделить io, чтобы проще писать тесты. Что делать с программами, где появляются очень большая вариативность (тот же пример "Охота на хампуса" из "Чистой архитектуры"), что от чего должно зависеть, чтобы программа была расширяемая - на все эти вопросы этот подход ответа на даёт. DDD и Чистая архитектура, были представлены существенно позже Structured Design и они пытались дать ответы для более масштабных программ. Просто ли применять более сложные подходы - нет, и в этом я соглашусь с автором. Но это совсем не значит, что нужно скатываться в такой примитивизм из 60ых годов вне зависимости от задачи.
Но это всё покрывает только примитивные программы, типа CRUD
Я правильно понимаю, что второй кейс с функцией назначения водителя вы тоже занесли в "примитивные программы, типа CRUD"?
В таком случае да, СД покрывает только их.
Но тогда боже упаси меня писать то, что вы называете сложными программами:)
Что делать с "Охота на хампуса" из "Чистой архитектуры")
Не заниматься ерундой делать оверинжиниринг, как пацаны из третьего кейса:
Не выдумывать, что у вас будет ввод и из консоли, и из смс и вы всё это будете разруливать в одной кодовой базе. Сделайте бэк с игрой, смс-шлюз который дёргает бэк и настольное приложение которое дёргает бэк
Переводы команд на разные языки вынесите в ресурсы. Соответственно, когда приходит команда - идёте в файлик с ресурсами и получаете каноничное имя
Возьмите РСУБД (на такую игру - точно хватит) и храните данные только там. Следите за тем, чтобы это решение не текло через АПИ кода, хранящего состояние игры
Безусловно, бывают случаи, когда есть вариативность в технологиях - тогда вводите интерфейс между кодом управления и кодом реализации io и всё.
Но вообще, см. коммент выше - в докладе и статье я держал в уме разработку бэков информационных систем. Разработка игр, как и фронта, как и АСУТП, как и встроенного софта - имеют свою специфику и не факт, что идея сбалансированной формы там применима.
По-моему, те кто так сильно ратуют за функциональное программирование противопоставляя его объектно-ориентированному подходу, лукавят. ООП никуда из их программ не девается - просто внутри ООП кода вставляются куски кода, написанного функциональным способом. Если попытаться абсолютно всё перенести в функциональную парадигму (то есть, отказаться от всех фишек ООП - объектов, полиморфизма, наследования и т.п.), начиная с гуёв и заканчивая бизнес-логикой и сервисным кодом - создать более-менее управляемое и, при этом, достаточно сложное, приложение будет невозможно... ООП - это же не способ программирования или написания кода. Это способ проектирования, метод борьбы со сложностью задачи.
Сам проект, причины и ход его реинжиниринга, а также методика получения цифр для оценки результатов приведены у меня в отдельных постах — если интересно, то можно покопаться. Здесь же приведу только выжимку.
В этом фрагменте ссылки ведут куда-то на localhost
Ну так себе идея. И без этого понятно что энергозатратные операции надо обьединять поэтому сиранно что понядобилось двадцать лет чтобы понять это. Так вы скоро поймете что нехрен пихать в бд бизнес логику. Скажу в стиле гуру: в бд надо держать то что требует агрегации типа count, group и тд , т.е. они для анализа а не оперативной работы. А если вся работа это тысячу заказов обработать то уж можно выделить для этого мегабайт памяти а не дрючить бд. А еще еще эффективнее непрерывно заранее вычислять
Еще мудрая мысль из сис анализа: любой принцип хорош если применяется везде.
То что описано в книге это типичный процесс работы программ в то время в стиле батч заданий: есть данные на бобинах, их надо считать, пред обработать и подать основному обработчику а в конце записать результат.
Имхо, решение всех ваших проблем очень простое - нужно запретить читать и записывать по одной записи. Только пакетом. И сразу всё логика выстроится правильно, и программы будут работать быстро.
Спасибо за статью.
Есть ли у такого подхода (балансировка, 4 категории кода) какие-нибудь преимущества перед более простым "Functional Core, Imperative Shell"?
PS. Не так давно у Кента Бека был доклад с упоминанием книги "Structured Design" https://www.youtube.com/watch?v=yBEcq23OgB4
Структурный дизайн. Древний секрет простого и быстрого кода