Основная проблема неиспользования фич конкретной БД в том, что ради потенциального перехода на другую БД вы отказываете себе в использовании остро заточенных ножей конкретной БД. Есть, конечно, продукты, где совместимость с различными БД - это необходимая, а потому изначально заложенная возможность. Но и эта ситуация не обязывает отказываться от остро заточенных ножей и ограничиваться ванильным SQL, если правильно реализовать паттерн репозиторий.
У вас, всё-таки, ошибка в методе лечения. >Лечение: generic-ограничение where T : struct, IShape вместо переменной интерфейса — JIT специализирует код по value-типу и вызывает метод без упаковки.
struct в ограничении параметра интерфейса совсем необязательно указывать. Важно сделать аргументом метода не интерфейс, а generic-тип, на который наложено ограничение имплементировать нужный интерфейс. Тогда, как писал ещё Джеффри Рихтера, C# для reference-типов создаст одну общую имплементацию этого метода, а для каждого структурного типа сделает отдельную имплементацию. Эта отдельная имплементация будет знать конкретный тип параметра и не будет боксинга.
using System;
public interface IShape { double Area(); }
public struct Circle : IShape
{
public double R;
public double Area() => Math.PI * R * R;
}
public class C {
public double GetDoubleAreaWithBoxing() {
IShape s = new Circle { R = 2 };
return s.Area();
}
public double GetDoubleAreaWithoutBoxing() {
Circle circle = new Circle { R = 2 };
return GetShapeDoubleArea(circle);
}
public double GetShapeDoubleArea<T>(T shape) where T: IShape {
var area = shape.Area();
return area * 2;
}
}
Блог с комментом, конечно, учебный пример, но если отвечать кратко, то частая проблема с агрегатами именно в том, что для разных задач необходим различный набор данных.
Там, где данных немного, размером можно пренебречь. Когда полный агрегат становится достаточно большим, эта проблема появляется. И главная сложность в том, что границы таких агрегатов подвижны.
Если нет правил на добавление коммента, тогда коммент - агрегат. Если появляются правила, заданные постом, тогда агрегат уже пост. И эти правила могут меняться. Сначала вводится ограничение для ролевой модели (френд-не френд), потом добавляется ограничение на максимальное количество комментов к посту, потом для серебряных пользователей вводится расширенный лимит на добавление комментариев, а для золотых пользователей лимит и вовсе снимается. Грузить текст с картинками для определения правил всё равно не нужно.
На практике это часто приводит к протекающим моделям. В EF можно заинклюдить какие-то связи, а можно грузить сущность без них. В репу можно передать это флагами. На выходе будет "единая" модель, которая будет вести себя по разному в зависимости от того, каким образом был выгружен агрегат.
Таким образом, задача добавления комментария уже свою собственную модель доменной области потянет, под капотом которой, возможно, будет прятаться сущность EF. В общем, "агрегат" на практике получается более подвижная штука, чем это принято считать, и определяется во многом сценариями использования. Не сценарии от агрегатов пляшут, а агрегаты от сценариев.
Если у вас доменнная модель отделена от моделей EF, то изменения в доменной модели не трекаются EF. А ведь на этом "эффекте" (изменения "в памяти" плавно перетекут в БД) базируется уже логика доменного слоя.
Если вы попробуете добавить прослойку, которая будет делать маппинг из доменной модели в модель EF, то окажется, что это дело не такое уж и простое. Прямолинейный маппинг из одной сущности в другую будет работать, только если у вас вариант key-value хранилища. Маппинг, который "апдайтит" сущность написать не намного проще, чем собственный change-tracker.
В общем, от того, что под капотом репозитория на самом деле очень многое зависит на уровне бизнес-слоя. И от этой зависимости невозможно полностью абстрагироваться.
Про IQueryable, как протекающую абстракцию и про тестирование запросов in-memory +много. Сам на собесах такие вещи проверяю.
А вот про загрузуку всего поста для того, чтобы добавить один коммент (работа через агрегаты) - стоило бы отдельно поговорить.
Ну и в целом, для того чтобы паттерн "загрузли агрегат из репы -> внесли изменения в агрегат -> сохранили агрегат целиком" нужна либо ORM, либо key-value хранилище. И отсутствие вопросов с конкурентностью. А если на той стороне базёнка с процедурами, то уложиться в такой паттерн становится намного труднее. Так что инфраструктурные ограничения далеко не всегда DDD-паттернами устраняются.
>Это и есть legacy-форма, протекающая через границу. На уровне БД она оправдана
На уровне БД стоит check-constraint'ы навешать для контроля состояния. Но 1) о них почему-то реально мало знают 2) DDD-исты часто выступают против _любых_ проверок на уровне БД.
Я же считаю такие структурные проверки в БД оправданными. Стоят они дёшево, а от разных неожиданностей хорошо спасают.
Спасибо, хорошая статья. Но можно ли подробней осветить этот момент?
>Если провайдер не поддерживает ничего из этого – оборачиваем его в свой адаптер с собственным reference-store и реализуем двухфазную модель на свой страх и риск. Это работает, но добавляет новые точки отказа.
Если провайдер не поддерживает "ничего", то какими приседаниями вы от него добиваетесь идемпотентности?
Забавно то, что скорее всего - разные. Есть известный тест на объединение двух таблиц 4 способами.
select a.* from a, b where a.x = b.y
select a.* from a inner join b on a.x = b.y
select a.* from a where a.x in (select y from b)
select a.* from a inner exists (select * from b where b.y = a.x)
Запросы логически эквивалентны (в случае уникальности b.y). Но на Oracle я получал для них 3 плана выполнения запроса, а на Postgres - 4.
С ростом версий, возможно что-то и поменялось, но в целом этот пример показывает, что rule-based в построении запросов существенную роль до сих пор играет. И это, кстати, даёт возможность оптимизировать запросы путём переписывания их в другие логически-эквивалентные формы, без прибегания к хинтам.
Вообще, лучшем видом документирования программного кода являются юнит-тесты. И если система действительно "сложная", то они всё равно необходимы. Если "рок-звезда" сам не пишет юнит-тесты, то его можно было бы об этом попросить, дать ему на это время. Программист понимает, что это не бессмысленное занятие, если делается это не ради метрик покрытия кода тестами. Но если "рок-звезда" настоящая рок-звезда, то он сам пишет юнит-тесты. И тогда тем более понятно, почему он шлёт куда подальше всех этих "нипанятна, напиши документацию".
Хватает ситуаций, когда комментарии нужны, но не вида userId // идентификатор пользователя. Но уметь писать полезные комментарии и не писать бесполезных - это отдельный сложный навык. А если в компании отсутствие комментариев приравнивается к "красному флагу", то могу представить, какой бардак там творится.
С критикой примеров согласен. А фичу считаю полезной. У меня сейчас как раз такой проект, где вот этих вложенных циклов много. И элегантная замена goto или флагам была бы весьма кстати.
Не знаю, может вам и получится найти источник, в котором говорится, что предназначение DTO - передача данных между слоями приложения, но в том источнике, на который вы сослались, говорится следующее: "The application service is then responsible for moving data back and forth between the DTOs and the domain objects". Т.е., ровно о том, про что я писал выше.
>Выражение “data transfer” означает передача данных, это и есть определение. Передавать можно и между разными слоями приложения.
В программировании всё везде передаётся. Параметры передаются в методы, результаты передаются обратно, между "слоями" приложения тоже можно параметры передавать, хотя физически в приложении никаких слоёв нет, это условность и абстракция. Всё можно назвать "data transfer", поэтому выделять некие объекты в DTO на основании того, что они куда-то "передаются" и это их "определение" просто бессмысленно. Любой параметр на этом основании можно было бы назвать DTO.
А отличие объектов DTO от любых других объектов, классов, параметров состоит как раз в их особом предназначении - передача данных между разными процессами и разными системами. Это накладывает на них специфические условия, которые в первую очередь касаются вопросов сериализации.
Основная проблема неиспользования фич конкретной БД в том, что ради потенциального перехода на другую БД вы отказываете себе в использовании остро заточенных ножей конкретной БД. Есть, конечно, продукты, где совместимость с различными БД - это необходимая, а потому изначально заложенная возможность. Но и эта ситуация не обязывает отказываться от остро заточенных ножей и ограничиваться ванильным SQL, если правильно реализовать паттерн репозиторий.
У вас, всё-таки, ошибка в методе лечения.
>Лечение: generic-ограничение
where T : struct, IShapeвместо переменной интерфейса — JIT специализирует код по value-типу и вызывает метод без упаковки.struct в ограничении параметра интерфейса совсем необязательно указывать. Важно сделать аргументом метода не интерфейс, а generic-тип, на который наложено ограничение имплементировать нужный интерфейс. Тогда, как писал ещё Джеффри Рихтера, C# для reference-типов создаст одну общую имплементацию этого метода, а для каждого структурного типа сделает отдельную имплементацию. Эта отдельная имплементация будет знать конкретный тип параметра и не будет боксинга.
Вот ваш немного переделанный пример на sharplab. Ограничения struct нет, а боксинг лечится.
https://sharplab.io/#gist:a9fa36483d3aa22fb79b12b3704efac6
Блог с комментом, конечно, учебный пример, но если отвечать кратко, то частая проблема с агрегатами именно в том, что для разных задач необходим различный набор данных.
Там, где данных немного, размером можно пренебречь. Когда полный агрегат становится достаточно большим, эта проблема появляется. И главная сложность в том, что границы таких агрегатов подвижны.
Если нет правил на добавление коммента, тогда коммент - агрегат. Если появляются правила, заданные постом, тогда агрегат уже пост. И эти правила могут меняться. Сначала вводится ограничение для ролевой модели (френд-не френд), потом добавляется ограничение на максимальное количество комментов к посту, потом для серебряных пользователей вводится расширенный лимит на добавление комментариев, а для золотых пользователей лимит и вовсе снимается. Грузить текст с картинками для определения правил всё равно не нужно.
На практике это часто приводит к протекающим моделям. В EF можно заинклюдить какие-то связи, а можно грузить сущность без них. В репу можно передать это флагами. На выходе будет "единая" модель, которая будет вести себя по разному в зависимости от того, каким образом был выгружен агрегат.
Таким образом, задача добавления комментария уже свою собственную модель доменной области потянет, под капотом которой, возможно, будет прятаться сущность EF. В общем, "агрегат" на практике получается более подвижная штука, чем это принято считать, и определяется во многом сценариями использования. Не сценарии от агрегатов пляшут, а агрегаты от сценариев.
Если у вас доменнная модель отделена от моделей EF, то изменения в доменной модели не трекаются EF. А ведь на этом "эффекте" (изменения "в памяти" плавно перетекут в БД) базируется уже логика доменного слоя.
Если вы попробуете добавить прослойку, которая будет делать маппинг из доменной модели в модель EF, то окажется, что это дело не такое уж и простое. Прямолинейный маппинг из одной сущности в другую будет работать, только если у вас вариант key-value хранилища. Маппинг, который "апдайтит" сущность написать не намного проще, чем собственный change-tracker.
В общем, от того, что под капотом репозитория на самом деле очень многое зависит на уровне бизнес-слоя. И от этой зависимости невозможно полностью абстрагироваться.
>в книгах куча примеров
"- Так моему соседу 85, а он говорит, что с женщинами ещё ого-го.
- Вот и вы _говорите_" (c)
>Хотя возможно тут проблема что ОРМ через которую пишутся запросы не предоставляет возможности на такой запрос)
Т.е. у вас "архитектура" приложения возможностями ORM диктуется?
Про IQueryable, как протекающую абстракцию и про тестирование запросов in-memory +много. Сам на собесах такие вещи проверяю.
А вот про загрузуку всего поста для того, чтобы добавить один коммент (работа через агрегаты) - стоило бы отдельно поговорить.
Ну и в целом, для того чтобы паттерн "загрузли агрегат из репы -> внесли изменения в агрегат -> сохранили агрегат целиком" нужна либо ORM, либо key-value хранилище. И отсутствие вопросов с конкурентностью. А если на той стороне базёнка с процедурами, то уложиться в такой паттерн становится намного труднее. Так что инфраструктурные ограничения далеко не всегда DDD-паттернами устраняются.
>Это и есть legacy-форма, протекающая через границу. На уровне БД она оправдана
На уровне БД стоит check-constraint'ы навешать для контроля состояния. Но
1) о них почему-то реально мало знают
2) DDD-исты часто выступают против _любых_ проверок на уровне БД.
Я же считаю такие структурные проверки в БД оправданными. Стоят они дёшево, а от разных неожиданностей хорошо спасают.
Спасибо за развёрнутый ответ.
Ну, это вариант компенсации саги, для этого провайдер должен поддерживать методы отмены. В статье этот момент явно не оговаривается.
Спасибо, хорошая статья.
Но можно ли подробней осветить этот момент?
>Если провайдер не поддерживает ничего из этого – оборачиваем его в свой адаптер с собственным reference-store и реализуем двухфазную модель на свой страх и риск. Это работает, но добавляет новые точки отказа.
Если провайдер не поддерживает "ничего", то какими приседаниями вы от него добиваетесь идемпотентности?
Забавно то, что скорее всего - разные.
Есть известный тест на объединение двух таблиц 4 способами.
Запросы логически эквивалентны (в случае уникальности
b.y). Но на Oracle я получал для них 3 плана выполнения запроса, а на Postgres - 4.С ростом версий, возможно что-то и поменялось, но в целом этот пример показывает, что rule-based в построении запросов существенную роль до сих пор играет. И это, кстати, даёт возможность оптимизировать запросы путём переписывания их в другие логически-эквивалентные формы, без прибегания к хинтам.
А почему "никто вам об этом не скажет"? Книжки с мануалами совсем перестали читать?
В общем, без просмотра плана выполнения запроса - это гадание. Тем более без указания конкретной базы данных.
Вообще, лучшем видом документирования программного кода являются юнит-тесты. И если система действительно "сложная", то они всё равно необходимы.
Если "рок-звезда" сам не пишет юнит-тесты, то его можно было бы об этом попросить, дать ему на это время. Программист понимает, что это не бессмысленное занятие, если делается это не ради метрик покрытия кода тестами.
Но если "рок-звезда" настоящая рок-звезда, то он сам пишет юнит-тесты. И тогда тем более понятно, почему он шлёт куда подальше всех этих "нипанятна, напиши документацию".
Хватает ситуаций, когда комментарии нужны, но не вида userId // идентификатор пользователя.
Но уметь писать полезные комментарии и не писать бесполезных - это отдельный сложный навык. А если в компании отсутствие комментариев приравнивается к "красному флагу", то могу представить, какой бардак там творится.
Интересный вариант.
С критикой примеров согласен. А фичу считаю полезной. У меня сейчас как раз такой проект, где вот этих вложенных циклов много. И элегантная замена goto или флагам была бы весьма кстати.
Что ж, называйте DTO всё, что вашей душе угодно, не буду спорить.
Не знаю, может вам и получится найти источник, в котором говорится, что предназначение DTO - передача данных между слоями приложения, но в том источнике, на который вы сослались, говорится следующее: "The application service is then responsible for moving data back and forth between the DTOs and the domain objects". Т.е., ровно о том, про что я писал выше.
>Выражение “data transfer” означает передача данных, это и есть определение. Передавать можно и между разными слоями приложения.
В программировании всё везде передаётся. Параметры передаются в методы, результаты передаются обратно, между "слоями" приложения тоже можно параметры передавать, хотя физически в приложении никаких слоёв нет, это условность и абстракция. Всё можно назвать "data transfer", поэтому выделять некие объекты в DTO на основании того, что они куда-то "передаются" и это их "определение" просто бессмысленно. Любой параметр на этом основании можно было бы назвать DTO.
А отличие объектов DTO от любых других объектов, классов, параметров состоит как раз в их особом предназначении - передача данных между разными процессами и разными системами. Это накладывает на них специфические условия, которые в первую очередь касаются вопросов сериализации.