Здравствуйте, #John, Вы писали:
J>Здравствуйте,
J>как вы боритесь с тем, что в методы entity, через параметры, в Domain слой протягиваютя классы из других слоев?
Здравствуйте, #John, Вы писали:
J>Здравствуйте,
J>как вы боритесь с тем, что в методы entity, через параметры, в Domain слой протягиваютя классы из других слоев?
ORM и Dependency Injection?
Re: DDD протаскивание других слоев через параметры методов Domain
Здравствуйте, Буравчик, Вы писали:
Б>Выделяю поведение в отдельные классы — бизнес-сервисы. Зависимости прокидываю через конструктор в composition root.
Спасибо за ответ, поискал информацию еще в интернете, получается, бизнес-сервисы это один из оптимальных вариантов.
Но и у него есть недостатки: бизнес логика дробится.
Підтримати Україну у боротьбі з країною-терористом.
Здравствуйте, Буравчик, Вы писали:
J>>Но и у него есть недостатки: бизнес логика дробится.
Б>И какая проблема из этого возникает (пример)?
class Root
{
// ..void Smth()
{
bar.DoBar();
// other logic ..
}
}
class Bar
{
// ..void DoBar()
{
foo.UpdateStatus();
// other logic ..
}
}
class Foo
{
// ..private StatusEnum status;
void UpdateStatus()
{
switch(..)
{
case"yyy":
status = StatusEnum.Old;
break;
case"xxx":
// Если не создавать бизнес сервис,
// бизнес-логика выглядит последовательной. var x = new HttpClient().GetString("http://...");
if(x == "str")
{
status = StatusEnum.New;
// other logic ..
}
//..
}
// other logic ..
}
}
Если тут использовать бизнес сервис, то придется часть метода(или весь метод) `UpdateStatus()` вынести в бизнес сервис,
там сделать проверку результата, а потом продолжить выполнение логики из метода "UpdateStatus".
Чем выше будет цикломатическая сложность/вложеных_методов тем сложнее/больше_кода придется вынести в бизнес сервис.
Б>Вообще, она всегда дробится. Вопрос лишь как — по сущностям или по сервисам.
Если выносить код в бизнес сервисы, то данные и способы их обработки оказываются разделены, что нарушает один из принципов ООП,
т.к. не позволяет модели обеспечивать собственные инварианты.
Підтримати Україну у боротьбі з країною-терористом.
Здравствуйте, #John, Вы писали:
J>Если тут использовать бизнес сервис, то придется часть метода(или весь метод) `UpdateStatus()` вынести в бизнес сервис, J>там сделать проверку результата, а потом продолжить выполнение логики из метода "UpdateStatus".
А Foo и Bar у вас что — типы предметной области?
В нормальной архитектуре метод UpdateStatus не является членом класса "данных", а принадлежит сервису.
Поэтому никакого дробления не будет.
J>Чем выше будет цикломатическая сложность/вложеных_методов тем сложнее/больше_кода придется вынести в бизнес сервис.
Б>>Вообще, она всегда дробится. Вопрос лишь как — по сущностям или по сервисам. J>Если выносить код в бизнес сервисы, то данные и способы их обработки оказываются разделены, что нарушает один из принципов ООП, J>т.к. не позволяет модели обеспечивать собственные инварианты.
Нет такого принципа.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[6]: DDD протаскивание других слоев через параметры методов Domain
Здравствуйте, Sinclair, Вы писали:
S>А Foo и Bar у вас что — типы предметной области?
Да, Foo, Bar, Root — это модели из предметной области; Root — это агрегат рут.
S>В нормальной архитектуре метод UpdateStatus не является членом класса "данных", а принадлежит сервису. S>Поэтому никакого дробления не будет.
В примере не дописал код.
UpdateStatus меняет значение переменной `status` у класса Foo:
(`status` — потом буде сохранено в бд)
class Foo
{
// ..private StatusEnum status;
void UpdateStatus()
{
switch(..)
{
case"yyy":
status = StatusEnum.Old;
break;
case"xxx":
var x = new HttpClient().GetString("http://...");
if(x == "str")
{
status = StatusEnum.New;
// other logic ..
}
//..
}
// other logic ..
}
}
Підтримати Україну у боротьбі з країною-терористом.
Здравствуйте, Sinclair, Вы писали:
J>>Если выносить код в бизнес сервисы, то данные и способы их обработки оказываются разделены, что нарушает один из принципов ООП, J>>т.к. не позволяет модели обеспечивать собственные инварианты. S>Нет такого принципа.
Здравствуйте, #John, Вы писали:
S>>В нормальной архитектуре метод UpdateStatus не является членом класса "данных", а принадлежит сервису. S>>Поэтому никакого дробления не будет. J>В примере не дописал код. J>UpdateStatus меняет значение переменной `status` у класса Foo: J>(`status` — потом буде сохранено в бд) J>
J>class Foo
J>{
J> // ..
J> private StatusEnum status;
J> void UpdateStatus()
J> {
J> switch(..)
J> {
J> case"yyy":
J> status = StatusEnum.Old;
J> break;
J> case"xxx":
J> var x = new HttpClient().GetString("http://...");
J> if(x == "str")
J> {
J> status = StatusEnum.New;
J> // other logic ..
J> }
J> //..
J> }
J> // other logic ..
J> }
J>}
J>
Ну всё верно — поэтому-то рич модель плохо работает. Вы втаскиваете внутрь объекта несвойственную ему логику. Он уже вон и в интернет побежал, и всякое прочее делает.
В нормальном дизайне у вас будет что-то вроде FooService.UpdateStatus(foo). Или FooService.UpdateStatus(IEnumerable<Foo> foos).
А уже вот этот FooService будет оборудован каким-нибудь XProvider, который в него инжектируется DI фреймворком или вообще вручную, если нет нужды поддерживать разнообразие конфигураций. Так что вместо new HttpClient().GetString там будет XProvider.GetX()
Это позволит, в частности, тестировать поведение FooService без подключения к интернету и вызова настоящих внешних сервисов.
А также повторно использовать его код в тех случаях, когда у нас есть классы, которые по данным неотличимы от Foo, но с другим поведением. Или вообще — может оказаться, что это тот же самый Foo, только на другом этапе жизненного цикла.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re[7]: DDD протаскивание других слоев через параметры методов Domain
Здравствуйте, #John, Вы писали:
J>Здравствуйте, Sinclair, Вы писали:
J>>>Если выносить код в бизнес сервисы, то данные и способы их обработки оказываются разделены, что нарушает один из принципов ООП, J>>>т.к. не позволяет модели обеспечивать собственные инварианты. S>>Нет такого принципа.
J>В посте про амемичную модель Фовлер пишет: J>
J>... the basic idea of object-oriented design; which is to combine data and process together ...
In this blog post I will consider the arguments against the ADM, and contend that in some scenarios, the ADM appears be an reasonable choice of design, in terms of adherence to the SOLID principles of Object-Oriented design, established by Robert Martin [3], [4].
Re[8]: DDD протаскивание других слоев через параметры методов Domain
Читал перевод этой статьи на швабре. Там много не правды. Напр.
Анемичная модель предметной области лучше поддерживает автоматизированное тестирование.
...
В юнит тестах бизнес логику из БМПО можно легко тестировать и поддерживать если мокать БМПО которые используются в коде.
Потому тесты будут одинаково поддерживаемыми. (Думаю об этом автор даже не подумал, т.к. думает если класс хранит данные, значит его мокать нельзя?).
Если меняются требования к хранилищу данных — в АМПО задача решается путем передачи в PurchaseService из вышестоящего класса служб приложения новой реализации существующего интерфейса репозитория [17], [19], не требуя модификации существующего кода; в БМПО так легко не отделаться, модификация базового класса затронет все классы бизнес-сущностей
Автор статьи перепутал Domain Layer и Application Layer. Когда пишут проект по ДДД, БМПО с бизнес логикой находятся в доменном слое(а не в Application Layer), а репозиторий c доступом к данным — в инфраструктурном слое, Domain Layer не зависит от Infrastructure Layer, зависит только от фреймверка на котором пишется код и вспомогательных компонентов/хелперов. В ДДД мы легко можем заменить напр. `ms sql на cosmodb`, при этом не поменяв ни строчки кода в доменном слое.
Підтримати Україну у боротьбі з країною-терористом.
J>Анемичная модель предметной области лучше поддерживает автоматизированное тестирование.
J>...
J>В юнит тестах бизнес логику из БМПО можно легко тестировать и поддерживать если мокать БМПО которые используются в коде. J>Потому тесты будут одинаково поддерживаемыми. (Думаю об этом автор даже не подумал, т.к. думает если класс хранит данные, значит его мокать нельзя?).
То, что не требует мокать — лучше тестируется. Полагаю, что это правда, согласен с автором. Он ведь не утверждает о невозможности.
J>
J>Если меняются требования к хранилищу данных — в АМПО задача решается путем передачи в PurchaseService из вышестоящего класса служб приложения новой реализации существующего интерфейса репозитория [17], [19], не требуя модификации существующего кода; в БМПО так легко не отделаться, модификация базового класса затронет все классы бизнес-сущностей
J>Автор статьи перепутал Domain Layer и Application Layer. Когда пишут проект по ДДД, БМПО с бизнес логикой находятся в доменном слое(а не в Application Layer), а репозиторий c доступом к данным — в инфраструктурном слое, Domain Layer не зависит от Infrastructure Layer, зависит только от фреймверка на котором пишется код и вспомогательных компонентов/хелперов. В ДДД мы легко можем заменить напр. `ms sql на cosmodb`, при этом не поменяв ни строчки кода в доменном слое.
Тут в самом деле лукавство, т.к. изолировать часть инфраструктуры за интерфейсом репозитория позволяет и рич модель тоже. Но анемик к этому тяготеет по своей природе, а рич требует специальных усилий, что приводит к образованию тем вроде "DDD протаскивание других слоев через параметры методов Domain".
Re[10]: DDD протаскивание других слоев через параметры методов Domain
Здравствуйте, samius, Вы писали: S>То, что не требует мокать — лучше тестируется. Полагаю, что это правда, согласен с автором. Он ведь не утверждает о невозможности.
Допустим у нас есть пользователь у него есть контактная информация и нам нужно добавить
бизнес логику, когда пользователь нажимает кнопку "Save", нам приходят данные: `Info` и `email` и мы их обрабатываем.
В DDD это выглядело бы вот так:
Domain Layer
public class User
{
public Guid Id { get; private set; }
public string UserName { get; private set; }
public string Info { get; private set; }
public ContactInformation ContactInformation { get; private set; }
protected internal User() { }
/// <summary>
/// Used for loading for ex. from the DB
/// </summary>public static User Create(Guid id, string userName, string info, ContactInformation contactInformation)
{
return new User
{
Id = id,
Info = info,
UserName = userName,
ContactInformation = contactInformation,
};
}
/// <summary>
/// Used for creation
/// </summary>public static User Create(string userName, string info, ContactInformation contactInformation)
{
if (string.IsNullOrWhiteSpace(userName))
{
throw new ArgumentException();
}
// other validation ...var id = Guid.NewGuid();
return Create(id, info, userName, contactInformation);
}
public virtual ContactInformation GetContactInformation() => ContactInformation;
public virtual void UpdateInformation(string info, string email)
{
// validation ..this.Info = info;
GetContactInformation().UpdateEmail(email);
}
}
public class ContactInformation
{
public Guid Id { get; private set; }
public string Phone { get; private set; }
public string Email { get; private set; }
protected internal ContactInformation() { }
public static ContactInformation Create(Guid id, string phone, string email) => new ContactInformation
{
Id = id,
Phone = phone,
Email = email
};
public static ContactInformation Create(string phone, string email)
{
var id = Guid.NewGuid();
return Create(phone, email);
}
public static ContactInformation Create(string email) => Create(null, email);
internal virtual void UpdateEmail(string newEmail)
{
if (newEmail.Contains("admin"))
{
throw new ArgumentException();
}
this.Email = newEmail;
}
}
Infrastructure Layer
public class UserRepository
{
public User Load(Guid id)
{
// Loading from the DB ...var contactInformation = ContactInformation.Create(Guid.NewGuid(), "bob@example.com", "+123456");
var user = User.Create(id, "Bob", "Smth", contactInformation);
return user;
}
public void Save(User user)
{
}
}
Application Layer
public class UserInformationDto
{
public string Email { get; set; }
public string Info { get; set; }
}
public class UserService
{
// IUserRepositoryprivate UserRepository userRepository;
public UserService(UserRepository userRepository)
{
this.userRepository = userRepository;
}
public void UpdateUserInformation(Guid userId, UserInformationDto userInformation)
{
// validation ..var user = userRepository.Load(userId);
user.UpdateInformation(userInformation.Info, userInformation.Email);
userRepository.Save(user);
}
}
В трехслойной/четырехслойной архитектуре с анемичными моделями, нам точно так уже пришлось бы мокать `IChangeContactInformationService`
и метод "GetContactInformation". Потому разницы в сложности поддержания/написания тестов — нет.
Unit tests
public class UserTests
{
private User user;
private ContactInformation contactInformation;
public UserTests()
{
contactInformation = Mock.Of<ContactInformation>();
user = Mock.Of<User>();
}
[Fact]
public void UpdateInformationTest()
{
// Arrangevar email = "bob2@example.com";
Mock.Get(user).Setup(x => x.GetContactInformation()).Returns(contactInformation);
// Actvar ex = Record.Exception(() => user.UpdateInformation("xxx", email));
// Assert
Assert.Null(ex);
}
}
Бизнес логика в одном месте, работа с данными и все остальное отдельно, все легко тестируется и поддерживается.
Если придется работать с внешним сервисом, придется создавать DomainService и дробить бизнес логику в rich-моделях.
Из дополнительных плюсов с DDD мы легко можем применить CQRS подход.
Підтримати Україну у боротьбі з країною-терористом.
Здравствуйте, #John, Вы писали:
J>Здравствуйте, samius, Вы писали:
S>>То, что не требует мокать — лучше тестируется. Полагаю, что это правда, согласен с автором. Он ведь не утверждает о невозможности.
J>Допустим у нас есть пользователь у него есть контактная информация и нам нужно добавить J>бизнес логику, когда пользователь нажимает кнопку "Save", нам приходят данные: `Info` и `email` и мы их обрабатываем.
J>В DDD это выглядело бы вот так:
Удивительно рафинированно-анемичный получился пример DDD. Я не вижу в нем ничего, что нельзя было бы выполнить на голом DTO, за исключением, разве что, комментариев
// validation ..
PurchaseItem из статьи был куда более типичен для DDD.
А так же исходный
или Foo.UpdateStatus с заходом в HttpClient.
J>В трехслойной/четырехслойной архитектуре с анемичными моделями, нам точно так уже пришлось бы мокать `IChangeContactInformationService` J>и метод "GetContactInformation". Потому разницы в сложности поддержания/написания тестов — нет.
Разница есть и для меня она очевидна. Она в том числе в количестве сущностей, которые придется мокать при взаимодействии множества объектов предметной области между собой.
И в анемике нет нужды мокать вообще все.
J>Бизнес логика в одном месте, работа с данными и все остальное отдельно, все легко тестируется и поддерживается.
Бизнес логики я в этом примере не увидел. Хотелось посмотреть на чуть более сложном примере, где взаимодействуют покупатель, корзина товаров, набор акций на отдельные группы товаров, накопительные бонусные программы или что-то в этом роде.
Я не прошу приводить код, я его довольно ясно представляю, в том числе с тестами и обилием моков.
J>Если придется работать с внешним сервисом, придется создавать DomainService и дробить бизнес логику в rich-моделях.
Проблема рич в том, что чем больше сценариев, тем больше придется дробить бизнес логику в рич моделях.
Та же валидация внутри рич моделей говорит "ой", когда внезапно выясняется, что для одних процессов должна быть одна валидация, а для других — другая.
J>Из дополнительных плюсов с DDD мы легко можем применить CQRS подход.
Сорян, я не вижу причин, почему мы не можем легко применить CQRS подход с анемиком.
Re[12]: DDD протаскивание других слоев через параметры методов Domain
Здравствуйте, samius, Вы писали: S>Бизнес логики я в этом примере не увидел. Хотелось посмотреть на чуть более сложном примере, где взаимодействуют покупатель, корзина товаров, набор акций на отдельные группы товаров, накопительные бонусные программы или что-то в этом роде. S>Я не прошу приводить код, я его довольно ясно представляю, в том числе с тестами и обилием моков. J>>Если придется работать с внешним сервисом, придется создавать DomainService и дробить бизнес логику в rich-моделях. S>Проблема рич в том, что чем больше сценариев, тем больше придется дробить бизнес логику в рич моделях. S>Та же валидация внутри рич моделей говорит "ой", когда внезапно выясняется, что для одних процессов должна быть одна валидация, а для других — другая.
В DDD есть такие понятия как Entity, Value Object, Aggregate, Aggregate Root.
Entity/Value Object — это объекты. Концепция эквивалентности ссылок относится к Entity (User), в то время как структурая эквивалентность — к Value Object(Contact Information). швабра.
Aggregate: набор Entities или Value Objects, связанных друг с другом через объект Aggregate Root.
Aggregate Root: каждый агрегат имеет корень (в примере выше: User) и границу, агрегатный корень владеет агрегатом и служит шлюзом для всех модификаций внутри агрегата,
т.е. из Application Layer нельзя написать код:
var contactInformation = new ContactInformation(...);
repo.Save(contactInformaion);
Все изменения нужно делать только через агрегат рут(User):
var user = userRepository.Load(userId);
user.UpdateInformation(userInformation.Info, userInformation.Email);
userRepository.Save(user);
Бизнес логику желательно писать в самых конечных объектах (Value Objects).
В моделях валидацию можно рассматривать как инварианты. Проверка того что объекты всегда в правильном состоянии,
весь остальной код в моделях относится только к бизнес логике.
Валидация типа
if(repo.GetUserById(id) != null)
{
throw new NotFoundException();
}
будет в Application Layer-e.
Бизнес логика — это все что говорит заказчик. Сначала определяется уникальный язык(термины) которые заказчик/менеджеры/бизнес аналитик/девы будут использовать чтобы понимать друг друга.
Потом договаривают что напр. "Хотим изменить `Info` и `email` у пользователя".
Т.к. это относится к сущности User, в моделе User создается метод `UpdateInformation(string info, string email)`, который является прокси методом к бизнес логике,
которая будет писаться во вложеных Entitites и Value Objects.
--
Покупатель, корзина товаров, набор акций — это все будут разные агрегаты/агрегат руты.
Для взаимодействия агрегатов, что бы проще было расширять бизнес логику и меньше было локов при сохранении данных в бд, в DDD есть понятие как DomainEvents. msdn
Напр. у нас есть агрегат руты: User, Order, Product. Пользователь делает заказ.
В модель `Order` при создании будет передается 'userId' (Id агрегат рута пользователя) и кидается event создания заказа.
После мы можем добавить товар или изменить статус, при изменении статуса будет кидаться другой event.
и книга по ddd без воды: "Alexey Zimarev, Hands-On Domain-Driven Design with .NET Core — Tackling complexity in the heart of software by putting DDD principles into practice (2019)"
Підтримати Україну у боротьбі з країною-терористом.
Здравствуйте, #John, Вы писали:
J>и
[]
Все споры anemic vs rich можно закончить примером наблюдений за новыми языками и обновлениями старых. Почти везде основной упор идет на фишки ФП. Так что anemic, а именно ее можно сделать через ФП, побеждает сейчас.
Re[14]: DDD протаскивание других слоев через параметры методов Domain
Здравствуйте, alexsoff, Вы писали:
A>Все споры anemic vs rich можно закончить примером наблюдений за новыми языками и обновлениями старых. Почти везде основной упор идет на фишки ФП. Так что anemic, а именно ее можно сделать через ФП, побеждает сейчас.
Оставлю для дополнительного размышления ссылку на статью "Про Anemic Domain Model" . В принципе там все сказано.
Підтримати Україну у боротьбі з країною-терористом.