Стас Выщепан @gandjustas
Оптимизирую программы
Информация
- В рейтинге
- 224-й
- Откуда
- Москва, Москва и Московская обл., Россия
- Дата рождения
- Зарегистрирован
- Активность
Специализация
Архитектор программного обеспечения, Деливери-менеджер
Ведущий
C#
.NET Core
Entity framework
ASP.NET
Базы данных
Высоконагруженные системы
Проектирование архитектуры приложений
Git
PostgreSQL
Docker
Снижается по сравнению с чем?
Вернемся к простому случаю. Бизнес-правило - в одном заказе не больше трех позиций.
Так как на уровне БД мы не контролируем количество позиций, то поднять из базы можем любое количество. Поэтому проверять это правило при загрузке мы не можем.
Кроме того: мы используем ORM, аля EF, который использует свойства объекта или конструктор для создания. Это значит что мы не можем проверять правила в конструкторе.
У нас остается один вариант: проверять правила при выполнении действия.
Итак у нас получается цепочка вызовов:
Контроллер -> Объект.Действие -> Проверка. Естественно каждая стрелка может заключать еще 100500 вызовов, это не имеет значенияЯ предлагаю заменять это на:
Контроллер -> Правила.Проверка(объект).Что это дает:
Меньше косвенность вызовов и меньше приседаний с инъекцией зависимостей в Объект.
Проверка может сколь угодно сложной - принимать любое количество параметров, загружать данные из базы, вызывать веб-сервисы.
Проверка может оперировать на состоянием объекта в памяти, а сделать запрос в базу.
Первый вариант может только выбросить исключение при непрохождении проверки, а второй вариант может возвращать true или false и использоваться для деактивации элементов интерфейса ДО выполнения действия пользователем.
Тестируемость обоих вариантов одинаковая. У второго даже выше, так как для создания Объекта ему надо передать кучу зависимостей, которые нужны не только для проверки. Формальные метрики при преобразовании первого варианта во второй не меняются (проверял). В целом если отказаться от инъекции сервисов в Domain Object можно значительно сократить объемы кода.
Недостатки у второго варианта только надуманные. Можно сказать что легко проверку не вызвать в контроллере. Но её также легко не вызвать и в объекте, особенно если действий много. Можно сказать что действие будет вызываться в нескольких контроллерах, тогда будет дублирование кода. Но это тоже неправда, так как никто не мешает проверку и действие вынести в одни метод Domain Service или как он там называется.
Эванс книгу выпустил в 2003, а фаулер упоминал Domain Model в POEAA в 2001. Но история началась гораздо раньше, с Гради Буча.
Он выпустил свои книги в по ООП в 1990-1995 году, где центральной идеей было выразить связи объектов реального мира в виде классов в ОО-языке. По его мнению это могло бы упаковать сложность внутрь объектов, представив программиста простой интерфейс для создания программ.
Вполне возможно для некоторых классов программ это так и есть. Например для симуляций или АСУТП - там у каждого компонента будет свое поведение.
Фаулер в POEAA описал паттрен domain model, где подход Буча применялся для корпоративных приложений. Описал он его поверхностно, не особо углубляясь в недостатки. Далее они с эвансом написали книгу про DDD, где первая глава про анализ, а остальные про DDD pattern language.
В POEAA кстати было и про стейтлесс, и про веб, и query builder, и современные БД. Так что вряд ли получится сказать, что вознесение DDD было обусловлено внешними факторами.
Буч, кстати, переобулся. В последней редакции OOAD не пишет что
Чайник.Закипеть()- отличное решение. Он рассматривает реалистичные примеры приложений и иерархии классов их реализующие. И гораздо чаще на страницах книги можно встретитьЭкскаватор.Копать(земля), чемЗемля.Копайся().А чего особенного в связных списках? Почему для того же питона или java они проблем не представляют, а в rust это обязательно unsafe?
Имхо это как раз говорит о недостатках языка.
Вот мы и добрались до ключевого аспекта. Далеко не все "инварианты" можно выразить в системе типов. Особенно если они меняются с изменением требований.
Учитывая ваш коммент выше оказывается что даже простые "инварианты" фактически являются не инвариантами, а пред- и пост-условиями операций.
Если мы эти пред и пост условия отделим от объектов данных, то окажется, что мы сможем легко создавать условия, которые оперируют множеством сущностей. Например "заказ может содержать не более трёх позиций, если клиент не ВИП, и если позиции не содержат товары по акции".
Более того, эти пред и пост условия можно выразить в виде запросов к бд (с помощью ef) и даже не поднимать объекты в память
Это не ответ на вопрос. В базе может произойти что угодно.
Опустим вопрос о том, как это реализовать технически, чтобы я не мог в программе руками создать невалидный заказ.
Интереснее другой вопрос: как написать код, чтобы не забыть вызвать логику валидации? Кто помешает добавить метод бизнес-логики, где нарушается инвариант и не вызывает проверку?
Если "антипаттерны" показывают характеристики лучше, чем идиоматичный код, то это не антипаттерны. В этом случае ваши идиомы являются антипаттернами.
А является ли инвариантом какого-то класса правило "не более трёх позиций в заказе, если клиент не ВИП"?
То есть если вы подняли из базы заказ с 4 позициями должна вылететь ошибка?
Почему должно смутить? У меня disk mark показывает разницу в 4 раза. Плюс чтение лучше оптимизируется кэшем, чем запись.
Это результаты замера на моем компьютере при работе кода из статьи.
Остаётся только вопрос что есть инвариант и является ли правило "заказ должен содержать не более трёх позиций" инвариантом класса заказа для приложения интернет-магазина.
Никто не говорил что нельзя. Просто недостатков от DDD больше, чем преимуществ.
Небольшая метафора:
Почесать ухо можно и пяткой ноги. Можно годиться этим, организовать конференцию на тему того как чесать ухо пяткой, рассмотреть все особенности чесания ушей пяткой, потом написать книгу с советами дяди боба о том как чесать ухо пяткой.
Но рукой все равно удобнее.
А вся остальная книга про то как надо делать в java.
На HDD скорость чтения почти в 10 раз превышает скорость записи.
Нет. Именно менее 2% уникальных строк, то есть текстовых элементов строк исходного файла.
В каком месте борьба, тем более с языком?
FileStream вызывает непосредственно api операционной системы, над ним только один слой - для преобразования в строки. В итоге от него пришлось отказаться, так как сортируются в итоге не строки, а байтовые массивы.
Все остальное - высокоуровневые абстракции. Например итераторы, динамические коллекции, каналы и таски, стрим для сжатия, да и сама сортировка выполняется в итоге штатной функцией. Так что язык и библиотека очень даже помогает решить задачу.
Именно призывает и настаивает. Как и Буч в своей книге ООАД (на самом деле с него все началось). Это их парадигма и подход к проектированию. Перечитайте.
Отделение данных от алгоритмов обработки этих данных конечно решает проблемы DDD. Потому что это и есть отказ от DDD. Мы просто начинаем применять ООП в другом месте - в моделировании алгоритмов, а не в моделировании данных.
Я предлагаю даже не начинать DDD если это нетривиальный пример.
Решение проблемы - отказаться от DDD, строить системы "сверху вниз", сжечь книги буча и эванса, бить себя по лбу при желании написать
Земля.Копайся()Давайте поподробнее про "одно место, которое будет следить за инвариантами".
Сначала простой вариант. нам нужны инварианты в рамках одного класса.
Известные мне ORM поддерживают возможность перехватить событие записи объектов в базу и туда можно навесить свою валидацию. В C# уже готовый интерфейс есть для этого https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject который еще и в UI поддерживается.
Более того, можно эту валидацию в виде check constraints в базе. Тогда инварианты нарушить будет невозможно.
Сложный случай: нам нужны инварианты в рамках нескольких классов. Например "не более трех позиций в одном заказе". В рамках какого класса разместить эту логику? Будет ли это "одно место" ? Надо ли поднимать позиции заказа из базы, чтобы проверить инвариант, если вы только статус заказа меняете? Надо ли выкидывать ошибку если вы из базы подняли заказ с четырьмя позициями?
Как сделать это "одно место", если "не более трех позиций в заказе" это не "инвариант", а правило, которое меняется. Например разрешить больше трех позиций ВИП-клиентам или во время распродаж?
В итоге у вас получится движок правил (стратегий), которые вы сможете применять (или не применять) к классам Order во время работы с ними. Очевидно нет смысла применять набор правил когда вы меняете статус заказа или получаете данные для отображения.
Для начала отделим мух от котлет.
ООП - это средства языка по созданию объектов, обладающих свойствами инкапсуляции и полиморфизма, а также средства наследования интерфейса и реализации для уменьшения дублирования кода.
Моделировать предметную область, то есть объекты физического мира с которыми оперирует программа, с помощью системы типов ОО-языка называется Domain Driven Design (ака DDD). Вернее DDD описывает в целом подход к анализу и проектированию, а моделирование предметной области с помощью типов называется DDD pattern language.
Хотя и до появления термина DDD многократно в книгах встречались попытки моделировать объекты реального мира с помощью ООП (у Буча например), но именно Эванс с Фаулером ввели в обращение понятие DDD.
Так вот:
Можете ссылаться.
Причины банальные.
Во-первых, при более двух классов доменной модели неизбежно возникает вопроса где должны располагаться методы, использующие три и более разнотипных объекты. Например у нас есть Заказ, Покупатель и Магазин, где должен располагаться метод, реализующий логику получения Покупателем Заказа в Магазине?
Во-вторых, DDD-классы очень быстро превращаются в полу-god-object, содержащие в себе все методы-бизнес логики. Например Заказ может быть Сформирован, Проверен, Отгружен, Доставлен, Получен покупателем. В итоге в DDD-класс стекается куча вызовов из разных мест, это описание очень походит на антипаттерн god-object.
Справедливости ради в DDD pattern language есть средства борьбы со сложностью создаваемой DDD. Domain Services, Application Services и прочие сервисы. Суть их в том, что они выносят логику работы из DDD-классов. Все эти сервисы очень даже объектно-ориентированы, используют инкапсуляцию и полиморфизм, иногда даже наследование. Их строят с применением ОО-паттренов: стратегии, фабрики, фасады, chain-of-responsibility иногда даже рекурсивная композиция и интерпретаторы.
До нормального дизайна остается всего один шаг - убрать вызовы к DDD-классам и сделать так, чтобы контроллеры\команды вызывали Application Services, те вызывали Domain Services, они в свою очередь Infrastructure Services и оперировали с данными во внешнем хранилище. Если оперирование с данными проще сделать с помощью ORM, который отражает данные на классы, то так и надо делать.