Недавно я проводил консультацию, в ходе которой заметил, что можно эффективно использовать несколько простых приемов для улучшения дизайна ПО. Несмотря на то, что приемы и правда простые, эффект получает очень сильным. Эти приемы давно известны и применяются в повседневной работе, особенно теми, кто уже использует Domain-Driven Design.
Проблема
Как оказалось, обычными являются доменные объекты вида (код из реального проекта):
public class ScanerData : IEntity { public virtual int Id { get; protected set; } public virtual DateTime Time { get; set; } public virtual string PipeId { get; set; } public virtual byte[] Bitmap { get; set; } public virtual ComputerData Mashine {get; set;} }
Если этот доменный объект содержит в себе только поля для данных, то где находится логика работы с этим объектом и связанными с ним объектами? Я обычно нахожу эту логику в классах типа *Manager, *Helper, *Wrapper, *Service и т.п, которые в 99% случаев нарушают принцип единственности ответственности. Бизнес-логику пишут где угодно, только не в самих доменных объектах. В этом случае объекты являются контейнерами для данных, чем-то вроде DTO между БД и слоем "Business Logic". Такая доменная модель называется анемичной со всеми вытекающими отсюда последствиями.
Ниже описаны 3 простых приема, которые позволят улучшить дизайн вашей системы. Детальные описания вы найдете в ссылках в конце статьи.
№1 Убираем публичные сеттеры
User Story: Пользователь меняет цену продукта. Если цена ниже пороговой, то перевести продукт в категорию обычных. Если категория становится Обычный продукт, то убрать на продукт скидку.
Ошибочное решение
Будем делать это в классе ProductService:
public class Product { public int Price { get; set; } public CategoryType Category { get; set; } public int Discount { get; set; } } public enum CategoryType { Regular, Super } public class ProductService { public void ChangePrice(Product product, int newPrice) { product.Price = newPrice; if (product.Price < 100) product.Category = CategoryType.Regular; if (product.Category == CategoryType.Regular) product.Discount = 0; } }
Где гарантия, что когда мы захотим поменять цену у продукта через setter, мы вспомним про метод ChangePrice у ProductService? А если захотим изменить цену продукта в другом месте, то надо не забыть проверить границу цены и изменить категорию. Еще держим в голове, что при изменении категории, здесь или в любом другом классе, надо бы не забыть обнулить скидку, если категория стала Regular.
Правильное решение
public class AnyClass { public void AnyMethod(Product product, int newPrice) { product.ChangePriceTo(newPrice); } } public class Product { private const int SomeMinPriceFromDomainLogic = 100; public int Price { get; private set; } public CategoryType Category { get; private set; } public int Discount { get; private set; } public void ChangePriceTo(int newPrice) { Price = newPrice; if (Price < SomeMinPriceFromDomainLogic) SetCategory(CategoryType.Regular); } private void SetCategory(CategoryType category) { Category = category; if (Category == CategoryType.Regular) Discount = 0; } }
Мы сделали сеттеры приватными, а сам доменный объект теперь предоставляет открытый метод для изменения своего состояния - ChangePriceTo. Доменный объект сам контролирует внесение изменения в цену.
Здесь речь идет про обычную инкапсуляцию. Почему о ней забывают, когда используют Auto-Property?
№2 Добавляем параметризованный конструктор
User Story: У пользователя обязательно должно быть не пустое имя.
Ошибочное решение
public class AccountHelper { public void CreateAccount(string name) { Account account = new Account(); account.Name = name; } } public class Account { private string name; public string Name { get { return name; } set { if (string.IsNullOrEmpty(name)) throw new ArgumentException("name is empty", "name"); name = value; } } }
Account можно создать без имени? Да, можно, если не выставить ему имя после создания. При этом Account будет в неверном состоянии. Значит User Story не работает. Мы выполнили только ту часть, которая связана с не пустым именем. При создании Account'a API домена никак нас не ограничило в создании класса с неверным состоянием.
Правильное решение
public class Account { public Account([NotNull] string name) { if (string.IsNullOrEmpty(name)) throw new ArgumentNullException("name"); Name = name; } public string Name { get; private set; } } public class AnyClass { public void AnyMethod(string name) { var account = new Account(name); } }
Теперь Account можно создать только в валидном состоянии, без вариантов.
Я использовал атрибут NotNull, очень полезная примочка для ReSharper - External Annotations.
№3 Заменяем List, IList, ICollection и т.п. на IEnumerable
User Story: Добавляем пользователю роль. Если такая роль есть, то роль не добавляем, эта ситуация считается ошибочной. Если роль добавили, то в историю работы с пользователем нужно сохранить новую роль и дату, когда она была добавлена.
Ошибочное решение
public class Account { public Account() { Roles = new List<Role>(); } public List<Role> Roles { get; set; } public List<HistoryEntry> HistoryEntries { get; set; } } public class HistoryEntry { public HistoryEntry(Role role) { Role = role; CreatedAt = DateTime.Now; } public DateTime CreatedAt { get; private set; } public Role Role { get; private set; } } public class AccountHelper { public void AddRole(Account account, Role role) { if (account.Roles.Contains(role)) throw new AccountStateException(); account.Roles.Add(role); account.HistoryEntries.Add(new HistoryEntry(role)); } }
Чем плох интерфейс IList? Тем, что у него есть методы Add, Remove и т.п. Работу с этими методами вне объекта мы контролировать не можем. Кто знает, когда и где будет вызван account.Roles.Add(role)? Будут ли сделаны все необходимые проверки перед добавлением роли?
Правильное решение
public class Account { private readonly List<Role> roles = new List<Role>(); private readonly List<HistoryEntry> historyEntries = new List<HistoryEntry>(); public IEnumerable<Role> Roles { get { return roles; } } public IEnumerable<HistoryEntry> HistoryEntries { get { return historyEntries; } } public void AddRole(Role role) { if (roles.Any(x => x == role)) throw new AccountStateException(); roles.Add(role); historyEntries.Add(new HistoryEntry(role)); } } public class HistoryEntry { public HistoryEntry(Role role) { Role = role; CreatedAt = DateTime.Now; } public DateTime CreatedAt { get; private set; } public Role Role { get; private set; } } public class AnyClass { public void AnyMethod(Account account, Role role) { account.AddRole(role); } }
Интерфейс IEnumerable позволяет делать только чтение коллекции, но не изменение. Никто извне, без контроля со стороны класса Account, не сможет добавить или удалить роль.
Обратите внимание, что Accout является корнем агрегата и вся работа по добавлению роли, проверки и добавлению истории проходит через метод AddRole. Класс Account скрывает работу с объектом HistoryEntry.
Заключение
Какими бы простыми и очевидными ни были эти приемы, их нельзя игнорировать. Со временем я прихожу к понимаю, что простые подходы дают гораздо больший профит, чем сложные концепции или применение метрик для кода (KISS?). Какие еще приемы вы посоветуете?
Ссылки
Серия статей: Strengthening your domain, Jimmy Bogard
Anemic Domain Model, Martin Fowler
Doing it wrong: getters and setters
public void ChangePriceTo(int newPrice) { Price = newPrice; if (Price < SomeMinPriceFromDomainLogic) SetCategory(CategoryType.Regular); }
ОтветитьУдалитьЭто вполне переносится в setter без лишних потерь.
private readonly List roles = new List(); private readonly List historyEntries = new List(); public IEnumerable Roles { get { return roles; } } public IEnumerable HistoryEntries { get { return historyEntries; } }
Это тоже ошибочное решение. Эти IEnumerable снаружи легко приводятся к List и вся инкапсуляция оказывается мнимой.
И кстати, в чем реальная потребность наследовать все доменные объекты от IEntity?
ОтветитьУдалитьSergey Zwezdin
ОтветитьУдалитьПо поводу IEnumerable - это уже проблема потребителя, если он хочет проблем - пусть делает что хочет. Хоть рефлексией приватные поля выставляет.
По поводу IEntity. Во-первых, в DDD есть 2 типа объектов "entity" и "value object". Первые идентифицируются по идентификатору (ID): если Id равны, то это должен быть тот же бизнес объект. Вторые идентифицируются по состоянию: если полное состояния объектов одинаковые, то это объекты аналогичны.
IEntity это простой интерфейс IEntity { int Id { get; } }. Вместо int может быть любой другой тип.
Интерфейс нужен для того, чтобы можно было отличить entity от value object, ну и всякие плюшки виде ограничений на обработку объектов. например, мы можем сделать такой интерфейс:
IRepository where TEntity: IEntity {
void Add(TEntity);
void Remove(TEntity);
TEntity Get(int id);
}
Т.е. репозиторий может работать только с entity и ни с чем больше.
Парсер съел угловые скобочки у IRepository
ОтветитьУдалитьдолжно быть так: IRepository{TEntity} where TEntity: IEntity
Sergey Zwezdin - по поводу перенесения логики установки в Setter.
ОтветитьУдалитьЕсть такое соглашение в среде .net разработчиков: свойства не должны вызывать побочных эффектов и должны быть независимыми от порядка вызовов.
@Sergey Zwezdin
ОтветитьУдалитьНапишу дополнение к тому, что уже написал @hazzik
> Это вполне переносится в setter без лишних потерь
Как на счет DSL?
Прочитайте, как это хорошо звучит: product.ChangePriceTo(newPrice);
> Эти IEnumerable снаружи легко приводятся к List и вся инкапсуляция оказывается мнимой
Сергей, что скажет тест по этому поводу:
[Test]
public void IsSergeyWrong()
{
var account = new Account();
account.AddRole(Role.Admin);
List roles = account.Roles.ToList();
roles.Remove(Role.Admin);
Assert.Equals(0, account.Roles.Count());
}
Александр Бындю,
ОтветитьУдалитьКроме параметризованных конструкторов мне еще нравиться решение с фабричным методом Create, который инкапулирует всю логику создания объекта. Конструктор по умолчанию, при этом будет protected (для обеспечения правильной работы NHibernate, если это entity) или private.
Мне это решение кажется более удачным, в виду того простого факта, что рефакторить методы в ReSharper намного удобнее, чем конструкторы.
Также данный подход убирает возможность пользоваться инициализаторами, которые зачастую только мешают, а не помогают.
Инициализаторы объектов + конструктор по-умолчанию я предпочитаю использовать для DTO и прочих ViewModel - объектов без логики, т.к. в этом случае использование параметризированного конструктора или фабричного метода только добавляет лишнего кода и не приносит никакой пользы.
Александр Бындю,
ОтветитьУдалитьСергей, скорее всего, имел ввиду варварское приведение типов:
var roles = (ICollection{Role})account.Roles;
@hazzik
ОтветитьУдалить> Кроме параметризованных конструкторов мне еще нравиться решение с фабричным методом Create...
+1
Отличная идея!
@hazzik
ОтветитьУдалить> Сергей, скорее всего, имел ввиду варварское приведение типов
Ого! От программистов, которые готовы так делать и рефлексии можно ожидать)))
Как я понимаю, ты консультировал каких-то разработчиков и внезапно обнаружил, что у них даже инкапсуляция не выполняется? ))) Как знакомо!
ОтветитьУдалитьЯ вот тоже недавно видел код бизнес-логики, который скорее всего был написан теми людьми, что пять минут назад программировали микроконтроллеры.
Пора бы и мне уж написать в блог: "Проводя консультацию по одному из проектов, я заметил, что неплохо было бы использовать классы..."
@Мурадов Мурад
ОтветитьУдалить> Как я понимаю, ты консультировал каких-то разработчиков и внезапно обнаружил, что у них даже инкапсуляция не выполняется?
Дело не в инкапсуляции, а движению от анемичной доменной модели к более насыщенной, к формированию DSL.
> Проводя консультацию по одному из проектов, я заметил, что неплохо было бы использовать классы...
:)
@Александр Бындю
ОтветитьУдалить> Дело не в инкапсуляции, а движению от анемичной доменной модели к более насыщенной, к формированию DSL.
Мне почему-то кажется, что люди, которые оставили публичные сеттеры в *** и написали ***Service/Helper, про анемичную доменную модель и DSL впервые услышали от тебя )))
Кстати, тебе удается узнать, что происходит с проектом после твоей консультации? Было бы интересно узнать.
По поводу IEnumerable - это уже проблема потребителя, если он хочет проблем - пусть делает что хочет. Хоть рефлексией приватные поля выставляет.
ОтветитьУдалитьГоспода, вы уж определяйтесь у вас инкапсуляция или только ее видимость.
В случае, когда вы отдаете наружу настоящий List, пусть завуалированный в IEnumerable - проблема того, кто отдает. Про рефлексию это совсем другая история.
Если я непонятно выразился, то вот вам код::
var account = new Account();
List history = (List)account.HistoryEntries;
history.Add( ..... etc
И проблему, кстати, решает тривиальный вызов AsReadOnly()
Во-первых, в DDD есть 2 типа объектов "entity" и "value object". Первые идентифицируются по идентификатору (ID): если Id равны
Это не так. Равны могут быть не только Id, идентичность объекта могут представлять несколько полей и ни разу не обязательно, что это должен быть int. Отсюда и был мой вопрос. Принуждать все сущности системы использовать поле Id еще и с фиксированным типом не очень удобное решение, не?
Есть такое соглашение в среде .net разработчиков: свойства не должны вызывать побочных эффектов и должны быть независимыми от порядка вызовов.
С этим согласен, но операции не всегда могут изменять состояние, но иметь логику (например, какие-то сложные предикаты) - и в этом случае они вполне переносятся в свойства.
Как на счет DSL?
C DSL все ок. Сейчас мы обсуждаем не более чем техническое свойство реализации.
Прочитайте, как это хорошо звучит: product.ChangePriceTo(newPrice);
Звучит хорошо. Но и это звучит вполне себе ничего:
product.Price = newPrice;
(product price is newPrice).
Хотя, это уже больше фетиш такой.
Вообще, все эти комментарии с претензией на качественный код. Понятно, что можно и List-ы наружу отдавать, но тогда давайте называть вещи своими именами. :)
по поводу выбора в пользу IEnumerable - это просто обычное использование полезного в работе принципа You ain't gonna need it. Для вашей логики достаточно возможностей IEnumerable? так зачем использовать IList
ОтветитьУдалитьSergey Zwezdin,
ОтветитьУдалить> List history = (List)account.HistoryEntries;
Только не факт, что после восстановления из базы там будет вообще List, скорее всего там будет какой-нибудь нхибернейтовский PersistentBag и всё у вас сломается.
> Это не так. Равны могут быть не только Id, идентичность объекта могут представлять несколько полей
Прочитайте про entity в какой-нибудь умной книжке по DDD, или хотя бы тут у Фаулера: http://martinfowler.com/bliki/EvansClassification.html там ясно сказано - entity - это то, что идентифицируется идентификатором.
>и ни разу не обязательно, что это должен быть int.
Про это я сразу сказал, что тип может быть любым, обычно это int/long (в зависимости от планируемого объема базы и в системах, не использующих шардинг) или Guid (в системах, использующих шардинг). Тип идентификатора в каждом случае выбирается командой для конкретного проекта.
>Принуждать все сущности системы использовать поле Id еще и с фиксированным типом не очень удобное решение, не?
Очень удобное - ни разу не встречал проблем. Если есть какие-то проблемы, которыми можете поделиться - welcome.
>операции не всегда могут изменять состояние, но иметь логику ... в этом случае они вполне переносятся в свойства.
В данном конкретном примере, метод ChangePriceTo имеет сайд эффект и изменяет состояние, поэтому в свойство его никак нельзя.
Далее про списки. У Саши в примере написано вот так:
> private List{Role} roles = new List{Role}();
Но, на самом деле у нас в коде всегда поле объявляется как ICollection`1
> private ICollection{Role} roles = new List{Role}();
И никакого AsReadOnly() у ICollection нет.
ЗЫ: как-то я вдоволь натрахался с кодом, который делал вот такую вот "честную" инкапсуляцию коллекций.
Здесь hazzik упомянул про отличие value object от entity. Вот, недавно была такая у меня проблема с объектом
ОтветитьУдалитьMoney:
public class Money : AbstractEntity
{
protected Money() {}
public Money(decimal amount, Currency currency)
: this()
{
Amount = amount;
Currency = currency;
}
public virtual decimal Amount { get; private set; }
public virtual Currency Currency { get; private set; }
....
Вроде должен бы был быть ValueObject, но на самом деле стал AbstractEntity из-за типов валюты, хранящиеся в БД.
Соответственно при мат. операциях:
public static Money operator +(Money m1, Money m2)
{
if (m1.Currency.Equals(m2.Currency))
return Create(m1.Amount + m2.Amount, m1.Currency);
throw new InvalidOperationException("Can't exec math operation under different type class");
}
интересует equals валют, где id типа валюты не имеет никакого значения, сравнивается только тип екземпляра класса валюты.
то же самое в money:
public override bool Equals(object obj)
{
var compare = (Money) obj;
if (ReferenceEquals(null, compare)) return false;
return Amount == compare.Amount && compare.Currency.Equals(Currency);
}
По итогу записей в бд для money может быть две, но их идентичность определяется размером и типов валюты, а не по id
Что это? :) Value object или IEntity? :)
menozgrande,
ОтветитьУдалитьЗдесь у вас по-факту нарушение принипов из-за неправильного отображения реляционных данных в объекты, по-простому можно сказать, что это сделано "влоб".
И Money и Currency должны быть ValueObject, и замаплены (если вы используете NHibernate) как component, при этом, тот факт, что они лежат в отдельной таблице никакой роли не играет.
Попробую нарисовать правильный мапинг и выложу гист.
hazzik, использую EF code-first. Если можно, то прошу учесть. :)
ОтветитьУдалитьВ nhibernate у меня не было с этим проблем в другом проекте, а здесь... беда :)
был бы благодарен за поправку на моем примере :)
так выглядит mapping currency:
ОтветитьУдалитьhttp://sapafinance.codeplex.com/SourceControl/changeset/view/88fd0f581e94#SapaFinance.Domain.EntityFramework%2fMappings%2fCurrencyConfiguration.cs
Допустим, у нас есть entity: Wallet, Currency и value-object Money:
ОтветитьУдалитьpublic struct Money : IEquatable{Money}
{
public decimal Value { get; set; }
public Currency Currency { get; set; }
public bool Equals(Money other)
{
return other.Value == Value &&
other.Currency.Id == Currency.Id;
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
return obj is Money && Equals((Money) obj);
}
public override int GetHashCode()
{
unchecked
{
return (Value.GetHashCode()*397) ^ (Currency != null ? Currency.Id.GetHashCode() : 0);
}
}
}
public class Currency:IEntity
{
public virtual string Name { get; set; }
public virtual int Id { get; set; }
}
public class Wallet : IEntity
{
public virtual int Id { get; set; }
//public virtual Money Money { get; set; }
//public virtual IEnumerable{Money} Money { get; set; }
}
Для случая, если у нас в кошельке может быть только одна деньга, то мы пишем просто:
Component(x => x.Money);
Money храниться в таблице Wallet, в виде (money_value, money_currency_id).
Если в кошельке может быть много разных денег, то мы пишем:
HasMany(x => x.Money)
.Component(c =>
{
c.Map(x => x.Value);
c.References(x => x.Currency);
});
Money хранится в отдельной таблице (wallet_id, value, currency_id). money_id придется добавить ручками, если у вас база требует обязательного наличия PK.
---
При этом Wallet и Currency идентифицируются по ID,
а Money, по своему состоянию:
Value == other.Value && Currency.Id == other.Currency.Id
profit.
@Oleg Karpov, к сожалению ни с EF ни EF CodeFirst не работал так плотно, по-этому не помогу.
ОтветитьУдалить@Oleg Karpov, могу предложить небольшой workaround для EF, если вы хотите соблюсти чистоту кода для внешнего наблюдателя: Money не наследовать от AbstractEntity, добавить ему приватное(или публичное - не важно) свойство id и замапить его в базу (ну если EF позволяет делать это без поля, то еще круче). Тогда для EF оно будет как-бы entity, а для стороннего наблюдателя как value-object.
ОтветитьУдалить@Sergey Zwezdin
ОтветитьУдалить> Господа, вы уж определяйтесь у вас инкапсуляция или только ее видимость.
Вы в своих проектах отдаете List наружу?
if (roles.Any(x => x == role))
ОтветитьУдалитьthrow new AccountStateException()
Зачем здесь лямбда когда можно .Contains(role)?
@ASK
ОтветитьУдалитьМожно и так, не принципиально. Пример исправлял ошибку в дизайне ПО.
Ржунимагу. Значит отделение данных от методов работы с ними нарушает SRP, а помещение всего в один класс нет?
ОтветитьУдалить@gandjustas
ОтветитьУдалить> Значит отделение данных от методов работы с ними нарушает SRP
Отделение данных от методов работы с ними? Что это значит? При чем здесь SRP? Что вы имеет ввиду под "помещение всего"? Чего "всего"?
Решение для "№1 Убираем публичные сеттеры" просто неверно. Так как сервис выполняет более одного действия: устанавливает price и category. А в "правильном решении" два метода, которые по сути сеттеры свойств и работают несогласовано.
ОтветитьУдалитьКстати класс сервиса как раз инкапсулирует логику, это гораздо важнее чем инкапсуляция данных.
@gandjustas
ОтветитьУдалить> Значит отделение данных от методов работы с ними нарушает SRP...
Вопросы те же. Отделение данных от методов работы с ними? Что это значит? При чем здесь SRP? Что вы имеет ввиду под "помещение всего"? Чего "всего"?
> А в "правильном решении" два метода...
Это называет корень агрегации, который отвечает за правильное состояние домена приложения. Слышали про такое понятие?
> Кстати класс сервиса как раз инкапсулирует логику, это гораздо важнее чем инкапсуляция данных.
Что такое инкапсуляция логику? Что значит инкапсуляция данные? Почему первое важнее второго?
> Отделение данных от методов работы с ними?
ОтветитьУдалитьЭто значит что данные отдельно - методы отдельно (в другом классе)
> При чем здесь SRP?
SRP говорит что у класса есть ровно одна неотделимая обязанность, а остальные отделимые и ДОЛЖНЫ быть отделены.
Если рассмотреть первый блок код, то видно что ScanerData занимается переносом данных их хранилища. Это и есть неотделимая обязанность, без нее scannerdata не существовал бы. Остальные обязанности - отделимые и находятся в отдельных классах.
@gandjustas
ОтветитьУдалитьЯ не устану переспрашивать.
Вопросы те же. Отделение данных от методов работы с ними? Что это значит?
При чем здесь SRP? Не что такое SRP, а как это связано "Значит отделение данных от методов работы с ними нарушает SRP"
Что вы имеет ввиду под "помещение всего"? Чего "всего"?
Это называет корень агрегации, который отвечает за правильное состояние домена приложения. Слышали про такое понятие?
Что такое инкапсуляция логики? Что значит инкапсуляция данных? Почему первое важнее второго?
> Это значит что данные отдельно - методы отдельно (в другом классе)
Я правильно понимаю, что вы хотите объект разделить на объект-состояние (данные) и объект поведение (методы)? Таким образом, не нарушив SRP?
Корень аггрегации тут не при чем. В "паравильном решении" предложенный интерфейс класса никак не позволяет поддерживать бизнес-логику.
ОтветитьУдалитьИз этого должно быть понятно что инкапсуляция логики - сокрытие деталей за интерфейсом - важнее чем инкапсуляция данных. Так как только инкапсуляция логики позволяет поддерживать корректность реализации БЛ
> Вопросы те же. Отделение данных от методов работы с ними? Что это значит?
ОтветитьУдалитьЯ уже ответил на этот вопрос выше. Читайте внимательно и вдумчиво.
> Я правильно понимаю, что вы хотите объект разделить на объект-состояние (данные) и объект поведение (методы)? Таким образом, не нарушив SRP?
Нет, ScannerData не является состоянием чего-либо. Он существует потому что существует запись в БД о сканере. Этот класс представляет данные о сканере в других частях программы. Это уже обязанность класса, остальные должны быть отделены.
Данные != состояние объекта.
@gandjustas
ОтветитьУдалитьВ предыдущем комментарии гораздо больше вопросов, не игнорируйте их.
А вот это я считаю надо в рамку: "Из этого должно быть понятно что инкапсуляция логики - сокрытие деталей за интерфейсом - важнее чем инкапсуляция данных. Так как только инкапсуляция логики позволяет поддерживать корректность реализации БЛ"
ООП стремилось дать объекту состояние и поведение, а вы в очередной раз (как это было с открытием слоистой архитектуры) изобрели велосипед с квадратными колесами. Читайте мат. часть OOP.
"№2 Добавляем параметризованный конструктор" - решение правильное в теории, но не на практике.
ОтветитьУдалитьНа практике обязательность поля может меняться со временем и внезапно в базе оказались пустые Name, а конструктор требует непустого. Программа начинает падать.
По сути вместо кода надо как раз написать anemic класс и использовать внешнюю валидацию.
> В предыдущем комментарии гораздо больше вопросов, не игнорируйте их.
ОтветитьУдалитьЯ на них уже ответил.
> ООП стремилось дать объекту состояние и поведение
Только сначала поведение, а потом уже состояние, его реализующее. А тут попытка представить что данные это состояние и натянуть на него сверху поведение.
@gandjustas
ОтветитьУдалитьЕще так много ваших гипотиз требуют ответов.
> и внезапно в базе оказались пустые Name
Возможно в Share Point такие "внезапности" норма, но для остального мира так не бывает)))
@gandjustas
ОтветитьУдалитьК предыдущим вопросам, прибавляется:
> Только сначала поведение, а потом уже состояние, его реализующее.
Что это значит? Сначала поведение? Т.е. всегда сначала поведение? Нельзя "натягивать" сверху поведение?))
Я сохранил эту страницу на всякий случай, чтобы вы не удалили свои комментарии))
"№3 Заменяем List, IList, ICollection и т.п. на IEnumerable" кроме технической ошибки с выставлением List, как IEnumerable, которая банально решается с помощью .AsReadOnly есть более тяжелая ошибка.
ОтветитьУдалитьhistoryEntries обязаны вытягиваться из хранилища вместе с account, причем historyEntries будет расти непрерывно, и баналльные операции вроде проверки что account принадлежит к какой-то роли будут тормозить. Причем нету этому никаких оправданий.
>поведение ... состояние, его реализующее.
ОтветитьУдалитьПрости, ржал 5 минут.
> Что это значит? Сначала поведение? Т.е. всегда сначала поведение? Нельзя "натягивать" сверху поведение?))
ОтветитьУдалитьДа, именно так. Сначала надо придумать что делает программа, а потом уже формировать структуры данных с которыми она работает. Тут парадигма не имеет значения, это общее правило.
@gandjustas
ОтветитьУдалить> historyEntries обязаны вытягиваться из хранилища вместе с account, причем historyEntries будет расти непрерывно
Что это за процесс непрерывно роста? Как это происходит?
> баналльные операции вроде проверки что account принадлежит к какой-то роли будут тормозить
Скорее примеры! Обозначьте причины тормозов.
> Причем нету этому никаких оправданий
Есть
> Возможно в Share Point такие "внезапности" норма, но для остального мира так не бывает)))
ОтветитьУдалитьТо есть ты можешь гарантировать что обязательность поля сразу же задается при проектировании и никогда не меняется? Я тебе просто не верю.
> Что это за процесс непрерывно роста? Как это происходит?
ОтветитьУдалитьВызывается addrole
>Скорее примеры! Обозначьте причины тормозов.
Вытягивание historyEntries из хранилища.
@gandjustas
ОтветитьУдалить> То есть ты можешь гарантировать что обязательность поля сразу же задается при проектировании и никогда не меняется? Я тебе просто не верю.
Давай в верю-неверю поиграем)) Так, ладно, отключаю стеб.
> обязательность поля
Что такое обязанность поля?
> задается при проектировании и никогда не меняется?
Меняется, это называется рефакторинг. Конструктор при этом тоже меняется. Есть довольно хорошее предложение - создать фабричный метод, чтобы было проще рефакторить.
@gandjustas, список вопросов еще жив, добейте их.
>Что такое обязанность поля?
ОтветитьУдалитьКак минимум то что оно должно быть не null, логично?
>Меняется, это называется рефакторинг. Конструктор при этом тоже меняется. Есть довольно хорошее предложение - создать фабричный метод, чтобы было проще рефакторить.
Да пожалуйста. У тебя было поле, которое сохраняет null, ты добавил проверку на не-null.
Ты достаешь данные из базы и пытаешься сформировать класс и внезапно в конструктор передается null.
На все вопросы я уже ответил, достаточно вдумчиво прочитать комменты. К сожалению нету времени писать более подробно.
gandjustas,
ОтветитьУдалить>historyEntries обязаны
Кому обязаны?
Если в этом вашем ШирнутомПоинте и нельзя добавлять элементы в коллецкцию, не подгрузив ее, то в NHibernate можно (почитайте тут: http://nhforge.org/doc/nh/en/index.html)
@gandjustas
ОтветитьУдалитьЛадно, пора прервать этот бред)))
> Вытягивание historyEntries из хранилища
По вашим словам я могу сразу понять, что вы не понимаете DDD (потяно, что и ООП), не работали с NHibernate и уж точно не применяли CQRS.
@gandjustas, лучше сидите на SharePoint, где можно писать как получится))) Ваши best practicies и наши best practicies это совсем разные вещи.
Все кто прочитали комментарии от @gandjustas, будьте осторожны - это самые явные ошибки в понимании ООП и проектирования в целом.
Я, кажется, понимаю, что @gandjustas хочет сказать. Пример №1 про класс Product. Получается, что в этом доменном типе хранится информация как о продукте, так и о ценовой политике. 2 обязанности.
ОтветитьУдалитьХотел еще вчера про это сказать. Но оформить мысль помогли только сейчас :-)
gandjustas,
ОтветитьУдалить>Ты достаешь данные из базы и пытаешься сформировать класс и внезапно в конструктор передается null.
Дегидрация использует конструктор по умолчанию - это раз. Два: откуда в базе возьмется null, если туда гарантированно писали NOT NULL, да еще и constraint стоит на колонке?
Или вы имеете привычку в базу руками лазить?
@gandjustas
ОтветитьУдалить> Ты достаешь данные из базы и пытаешься сформировать класс и внезапно в конструктор передается null.
Да-да, вы не работали с нормальной ORM, иначе бы знали, КАК объекты достаются из БД.
Для Сергея пример из референса по NH
ОтветитьУдалитьCat cat = new DomesticCat();
Cat kitten = new DomesticCat();
....
ISet kittens = new HashedSet();
kittens.Add(kitten);
cat.Kittens = kittens;
session.Save(cat);
kittens = cat.Kittens; //Okay, kittens collection is an ISet
HashedSet hs = (HashedSet) cat.Kittens; //Error!
@Мурадов Мурад
ОтветитьУдалить> Получается, что в этом доменном типе хранится информация как о продукте, так и о ценовой политике.
Спасибо, что обратили внимание. Я подумаю про изменение кода, вы тоже предложите свой вариант.
@Мурадов Мурад
ОтветитьУдалитьПри решении надо исходить из предложенной User Story.
@Maksim Galkin
ОтветитьУдалить>Нетрудно заметить, что если программировать, как положено, от интерфейса
Зачем нужны интерфейсы в доменной модели?
>Стоит добавить, что если уж идти на все эти ухищрения, то и тип T нужно делать полностью immutable. Иначе пользователю интерфейса
Вы понимаете различия между проектированием фреймворка и проектированием доменной модели?
@gandjustas
ОтветитьУдалитьНайдите время прочитать эту книгу - откроете для себя много нового
http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215
Maksim Galkin,
ОтветитьУдалитьВы неправы. Прочитайте в чем отличие фреймворка от библиотеки, хотя бы тут: http://stackoverflow.com/questions/148747/what-is-the-difference-between-a-framework-and-a-library
Что такое доменная модель - в книге, приведенной выше.
Не надо кричать о том, в чем вы, очевидно, не понимаете.
Александр Бындю комментирует...
ОтветитьУдалить@Мурадов Мурад
> Получается, что в этом доменном типе хранится информация как о продукте, так и о ценовой политике.
Спасибо, что обратили внимание. Я подумаю про изменение кода, вы тоже предложите свой вариант.
В нашем проекте есть разные типы резервов товара, наследуемые от IStorable. Соответственно техника резервирования товара для таких IEntity будет разная. Такую логику помещать в домен нельзя. То же касается расчета цен. Чаще всего, при простом варианте расчета можно нагрузить доменный объект, но в реальности, позже, этот код мы вынуждены выносить, т.к. расчет цен на разные типы товара может быть разный за счет других объектов типа "общая акция", которая не привязана к товару. В данном конкретном примере нельзя сказать точно кто прав, т.к. с одной стороны мы не видим всех частей проекта, с другой, если верить исключительно коду, то да, можно вынести расчет внутрь доменного объекта во избежание предварительного усложения. Ну и что, что нарушаем немного принципы SRP. Они - не самоцель, но о них надо помнить постоянно.
hazzik, спасибо за комментарий, я его изучу. И, вообще, надо спать ночью, а не код писать в 2:47 :) Спасибо тебе )
Хочу вставить свои 5 копеек.
ОтветитьУдалитьprivate readonly List historyEntries = new List();
public IEnumerable HistoryEntries
{
get { return historyEntries; }
}
Было бы вернее
public IEnumerable HistoryEntries
{
get { return historyEntries.AsEnumerable(); }
}
Тогда можно снаружи кастовать, приводить к листу, вызывать ToList() - это не поможет в манипуляции коллекцией.
@Maksim Galkin
"Нетрудно заметить, что если программировать, как положено, от интерфейса, а не от реализации, то интерфейс IEnumerable нельзя трактовать однозначно как неизменяемую коллекцию. "
IEnumerable служит для того, чтобы перечислять объекты коллекции, не зависимо от её реализации. Если вы предоставляете клиенту возможность перечислять вашу коллекцию, то клиент не должен думать о том, сколько раз ему положено её перечислить. Если, например, коллекцию можно перечислить только один раз, используйте декоратор, реализующий IEnumerable.
Попробую донести мысль gandjustas с другого ракурса.
ОтветитьУдалитьВозьмем ваше правильное решение класса Product.
Вы ввели два метода - ChangePriceTo и SetCategory, чтобы добиться инкапсуляции.
Теперь вопрос - что общего у этих методов, кроме того, чтобы они как-то работают с полями заказа?
Вопрос дальше - если изменяется логика установки цены, какой класс вы будете менять?
Еще опрос - если изменяется логика установки категории, какой класс вы будете изменять?
И напоследок - если изменится набор информации, которую хранит Продукт, какой класс вы будете изменять?
Итого - 3 причины для того, чтобы изменить класс, в то время как SRP требует одну, и как это было в ProductService, например.
Исключить последнюю причину - невозможно, ибо это все приложение построено на том, что непосредственно ваша доменная модель сохраняется в базу.
А дальше будет хуже - ваши объекты будут обрастать методами, между собой не связанными - то есть очень слабое сцепление( http://en.wikipedia.org/wiki/Cohesion_(computer_science) ).
Отсюда вариантов исключить нарушение принципа SRP не так много - делать модель анемичной или между сущностями, что хранятся в базе и доменной моделью делать прослойку.
@Oleg Karpov
ОтветитьУдалить>Соответственно техника резервирования товара для таких IEntity будет разная. Такую логику помещать в домен нельзя. То же касается расчета цен.
Как раз наоборот - это можно и нужно делать. Если вы этого не сделаете - получите классическую anemic domain model. Только естественно не нужно пихать эту логику в ваш IStorable - стоит выделить для этого отдельный агрегат, дочерними классами которого и будут ваши стратегии. Вообще весь DDD построен как раз на том, чтобы мыслить не в терминах таблиц и прочих хранилищ, а в терминах бизнеса. Think different
А с каких пор логика расчета цена продукта, зависящая от состояния этого самого продукта, находящаяся соответственно в этом продукте, является нарушением SRP?
ОтветитьУдалитьВсе перечисленные вами причины для изменения касаются изменения цены и только её, а значит и единственной ответственности, а значит SRP не нарушено.
Я полностью поддерживаю идеи @Dmitry Kryuchkov и @hazzik. От себя тоже добавлю.
ОтветитьУдалить@Vasiliy Shiryaev
Дело в том, что User Story была обозначена. Предложенная реализация полностью ее реализует, открывая наружу только метод ChangePriceTo. Если бы была другая User Story как-то задевающая эту логику, то безусловно надо было бы выносить работу с скидками в другие объекты и т.д. и т.д.
Я предлагаю не усложнять раньше времени, а приведенный пример лишь демонстрирует, что доменные объекты могут и должны иметь поведение.
Если вам интересно развитие этого кода в более сложный проект, то предлагаю вам написать ряд User Story, которые мы все вместе реализуем и посмотрим на результат.
@Александр Бындю
ОтветитьУдалитьКогда собеседник не согласен то надо непременно искать проблемы в собеседнике. Я предпочитаю так не делать.
Я прекрасно понимаю что такое ООП, но самое главное что понимаю границы его применимости.
Что касается SharePoint, то там как раз нельзя писать "как получится", на разных задачах один и тот же подход может дать противоположные результаты. Именно SharePoint учит продумывать заранее последствия тех или иных принятых решений. В отличие от рекомендаций вносить проверку на null внутрь класса с данными.
@Vasiliy Shiryaev
ОтветитьУдалитьЯ согласен, что в данном конкретном случае класс Product перегружен обязанностями. В реальном мире стратегий выбора категорий и определения цены может быть не 2 и не 5, а гораздо больше. Вариантов того, как такую ситуация успешно обработать 2 - либо новые агрегаты, либо новые value object. Что следует использовать в данном конкретном случае - неясно, ибо бизнес логики у тестового примера не много, а доменная модель строится исключительно по правилам бизнеса. В целом описанный в посте подход - абсолютно правильный, но это лишь первый шаг на пути к DDD
>Дегидрация использует конструктор по умолчанию - это раз.
ОтветитьУдалитьТо есть его может вызывать кто угодно? А в чем тогда смысл всего написанного выше про инкапсуляцию?
>Два: откуда в базе возьмется null, если туда гарантированно писали NOT NULL, да еще и constraint стоит на колонке?
Это если в начале проекта задали, то все хорошо. Но бывает так что сначала поле было nullable, а потом оказалось что там нужны данные при вводе. Сделать его notnull нельзя, но логику программы поправить нужно.
@Dmitry Kryuchkov
ОтветитьУдалитьЯ её читал, а вам советую почитать http://askofen.blogspot.com/
Там реальные советы человека, которы много лет создавал программы, а не просто евангелизирует теории.
> Да-да, вы не работали с нормальной ORM, иначе бы знали, КАК объекты достаются из БД.
ОтветитьУдалитьто есть для того чтобы показанный выше код был хоть сколько-нибудь эффективным надо использовать кошерный ORM? Так пишите об этом в предусловии своих постов.
Вот Linq2SQL или EF - достаточно кошерные? А BLToolkit?
@gandjustas
ОтветитьУдалитьПроблема в том, что вы не понимаете, чего вы не понимаете :)
Как рассказать человеку, которые не катался на сноуборде, что значит кататься на сноуборде?
Если вы не используете описанные методы (особенно интересно и подробно описано у Серия статей: Strengthening your domain), то это ваше право, доказать или обосновать вам, почему их использовать надо, тоже самое, что рассказывать как кататься на сноуборде. У нас разный опыт разработки и консультирования IT компаний.
@Dmitry Kryuchkov
ОтветитьУдалитьОткрою секрет: в реальном случае любой код будет гораздо сложнее, чем любой из примеров в этом блоге и всегда будет иметь смысл выносить его в отдельный сервис.
И если а) из PL вызывать сервис, а не из экземпляра класса; б) в сервис передавать не экземпляр класса, а ключ(и) и параметры, то получится очень даже anemic.
@gandjustas
ОтветитьУдалить> Я её читал
Прочитал, все слова понял, смысл не уловил?
> а вам советую почитать http://askofen.blogspot.com
Вы с ним чем-то схожи. Вы открываете новые слоистые архитектуры, он рисует масштабные UML диаграммы и таблицы.
> Там реальные советы человека, которы много лет создавал программы, а не просто евангелизирует теории.
Вышеописанное тоже не теория, всё это взято из практики. Просто это взято не из вашей практики.
>gandjustas
ОтветитьУдалитьТо есть его может вызывать кто угодно? А в чем тогда смысл всего написанного выше про инкапсуляцию?
Сначала прочитайте референсы по NHibernate, приведенные выше, или, хотя бы, попробуйте потыкать какую-нибудь ORM, потом поговорим.
Небольшой спойлер: конструктор по-умолчанию должен быть protected.
>Это если в начале проекта задали, то все хорошо. Но бывает так что сначала поле было nullable, а потом оказалось что там нужны данные при вводе. Сделать его notnull нельзя, но логику программы поправить нужно.
Так не бывает. Если поле стало not-null, то оно стало not-null везде, а не только в коде. Если вы в коде сделали предположение, что теперь поле not-null, а в базе-нет, то это - ваша проблема, но никак не DDD или вышеозначенного примера.
@gandjustas
ОтветитьУдалить> И если а) из PL вызывать сервис, а не из экземпляра класса; б) в сервис передавать не экземпляр класса, а ключ(и) и параметры...
Проблема взаимопонимания с вами описана здесь
> Проблема в том, что вы не понимаете, чего вы не понимаете :)
ОтветитьУдалитьЭто в первую очередь к вам относится. Судя по всему вы просто не сталкивались с реальными проблемами, возможно потому что слишком рано попали на должность техдиректора. Я же успел много чего повидать и попробовать и до сих пор занимаюсь архитектурой и иногда кодированием.
Например DDD пробовал еще 5 лет назад и на делфи, пришлось и свой ORM написать. И уже тогда понял недостатки DDD, которые никакими мощными инструментами не выправляются.
> Как рассказать человеку, которые не катался на сноуборде, что значит кататься на сноуборде?
Именно, поэтому попрошу воздержать от обсуждения SharePoint.
gandjustas,
ОтветитьУдалить>Там реальные советы человека, которы много лет создавал программы, а не просто евангелизирует теории.
Че-то на вид он не старше меня;) Много это сколько? 1? 2? 5? 15?
Я уже более 5 лет применяю принципы DDD и более 4х лет использую TDD в повседневной разработке. Увлекаюсь программированием более 16 лет (мне 26). Это много?
Для меня - нет. Я всегда готов учиться и прислушиваться к мнению окружающих.
Я часто пробую новые подходы и техники, наиболее удачные и полезные приживаются в нашей команде.
@hazzik
ОтветитьУдалитьprotected конструктор означает что я могу сделать наследника, который его вызовет ;)
Вообще использование мощного ORM, вроде NHibernate - сильное предусловие. Сейчас повсеместно на высоконагруженных проектах используются легковесные ORMы.
>Так не бывает. Если поле стало not-null, то оно стало not-null везде, а не только в коде.
Еще как бывает. Данные в базе есть и нету способа их заполнить автоматически. Нельзя просто так в базе убрать флажок nullable.
Это как раз реальное положение вещей, а не воображаемое как примерах.
@hazzik ему за 30 кажись, только к делу это не относится.
ОтветитьУдалить@gandjustas
ОтветитьУдалить>В отличие от рекомендаций вносить проверку на null внутрь класса с данными.
Абсолютно правильное решение, aggregate root отвечает за соблюдение всех инвариантов аггрегата в любой момент времени. Тот факт, что продукт должен иметь наименование - это и есть инвариант. По поводу был null, стал not null - миграционные скрипты уже отменили? Если да, то я как-то упустил этот момент. Более того, если вы в конструкторе написали эту саму проверку на null, то при загрузке даже немигрированных данные экспешена вы не получите - именно это и пытался объяснить вам @hazzik. Однако то, что исключений не возникнет вовсе не означает что стоит мириться с рассинхронизацией логики домена и структурой хранилища данных
>Я её читал, а вам советую почитать http://askofen.blogspot.com/
Я предпочитаю читать блоги людей, которые работают над большими и сложными проектами - Udi Dahan или Jimmy Bogard, например. Блоги людей, пишущих одни и те же калькуляторы "много лет" меня мало интересуют
>Открою секрет: в реальном случае любой код будет гораздо сложнее, чем любой из примеров в этом блоге и всегда будет иметь смысл выносить его в отдельный сервис.
Про сервис я ничего не говорил, он тут вообще не нужен. Если у вас есть несколько стратегий расчета цены - создайте по доменному объекту на каждую (aggregate или value object). Сервис тут абсолютно не нужен.
@Gengzu
ОтветитьУдалитьSRP звучит не что "класс должен иметь одну ответственность" - ибо это определение какое-то ничего не определяющие, а что "класс должен иметь только одну причину для изменения". Я назвал 3 - и общее у них только то, что они связаны с продуктом.
@Dmitry Kryuchkov, Александр Бындю
Я согласен, что ситуации бывают разные, и что тестовый пример очень маленький, чтобы так абстрактно рассуждать.
Меня просто смутило, что дается исходный код, который обозван неправильным, а предварительно текст о том, что большинство *Service - это нарушение принципа SRP. Потом дается решение, которые называется правильным. А по факту в нем нарушений не меньше, а с моей точки зрения и больше.
При этом люди, которые будут читать блог, и учиться чему-то по его статьям, будут видеть слова "Анемичный", "Service", "Неправильно", "Правильно". И делать как "правильно", хотя в реальной ситуации это _чаще всего_ будет как раз неправильно.
@gandjustas
ОтветитьУдалить>Вообще использование мощного ORM, вроде NHibernate - сильное предусловие. Сейчас повсеместно на высоконагруженных проектах используются легковесные ORMы.
Не путайте твиттер и enterprise приложения, у них совершенно разные цели и назначения. Более того, даже использую полновесную ORM можно создать высоконагруженное приложение - если построить масштабируемую архитектуру. И DDD тут как нельзя кстати
Чтобы создать высоконагруженное приложение нужно тянуть из базу как можно меньше, именно она становится узким местом, которое тяжело масшатбировать. А тут очень плохо работают aggregate root и lazy load, которых так любят в DDD.
ОтветитьУдалитьВы не правы - дело не в DDD, а в том как эти данные сохранять. Взгляните на event sourcing - key-value хранилища отлично масштабируются
ОтветитьУдалитьhazzik, не выйдет каменный цветок.
ОтветитьУдалитьhttp://stackoverflow.com/questions/7012319/using-ef-4-1-can-a-complex-type-reference-an-entity-e-g-in-ddd-a-value-object
http://msdn.microsoft.com/en-us/library/bb738472.aspx
Проблем в NH, чтобы ComplexType содержал Reference на Entity, нет, но в EF в ComplexType могут содержаться только scalar property.
Поэтому завязать в объект Money(Complex Type) Currency(Entity) не сделав первым Entity невозможно. Единственный вариант - хранить справочник в коде и мапить на колонку в БД типа varchar или int (неважно).
Только возникает проблема: как получить множество ключей для работы. Далеко не все задачи хорошо ложатся на выборку только по ключу. Поэтому как основу всегда используют РСУБД, а потом оптимизируют с помощью key-value store.
ОтветитьУдалитьНикто в здравом уме не использует его с самого начала, кроме самых простых случаев.
Dmitry Kryuchkov, я об этом и говорил, что фактически расчет будет производиться необходимым классом стратегии, а не содержаться внутри доменного объекта в виде switch или if_else. Т.е. для изменения мы полезем не в Entity.
ОтветитьУдалить> По поводу был null, стал not null - миграционные скрипты уже отменили?
ОтветитьУдалитьИх не всегда можно описать. У меня было пару случаев когда не было способа сделать из nullable колонки notnull. На уровне кода проблема решалась. Старый объект, будучи открытым для редактирования нельзя было сохранить не введя обязательное поле.
> Про сервис я ничего не говорил, он тут вообще не нужен. Если у вас есть несколько стратегий расчета цены - создайте по доменному объекту на каждую (aggregate или value object). Сервис тут абсолютно не нужен.
Стратегия в чистом виде и есть сервис ;) Зачем для них создавать еще aggregate или еще что-то непонятно. Это усложнение на ровном месте.
Документные базы данных для таких выборок с шардингом по тем же самым ключам - опять же масштабируются без особых проблем
ОтветитьУдалить> Сейчас повсеместно на высоконагруженных проектах используются легковесные ORMы.
ОтветитьУдалитьhttp://www.mindscapehq.com/blog/index.php/2011/12/05/5-reasons-not-to-use-a-micro-orm/
@Vasiliy Shiryaev
ОтветитьУдалить> Меня просто смутило, что дается исходный код, который обозван неправильным, а предварительно текст о том, что большинство *Service
Возможно я не раскрыл смысл. Дело не в том, что в сервис выносить ничего не надо, а в том, что со временем сервис становится объектом с 20-30 подобными методами (также как классы типа Helper, Wrapper и т.п.) и в 99% случаев превращается в God-object
>или еще что-то непонятно.
ОтветитьУдалитьНу вот, а говорите книжку читали
>Это усложнение на ровном месте.
Это называется инкапсуляцией бизнес логики в доменной модели. В вашем решении она получается размытой между самой моделью и ненужным слоем сервисов - ну и, соответственно, инкапсуляции тут нет.
@gandjustas
ОтветитьУдалить> Стратегия в чистом виде и есть сервис ;)
Смайлик в конце фразы прикрывает не знание предмета? Опять сюда. Эта фраза достойна вашей же цитаты.
@hazzik
ОтветитьУдалитьХорошая ссылка, но тем не менее практика говори что в высоконагруженных проектах нету DDD и нету тяжелых ORM.
Кстати для некоторых вполне легковесных ORM очень даже работает Linq. Посмотри BLToolkit например.
@Александр Бындю, ясно только одно. Пост получился спорным :)
ОтветитьУдалитьНо, все равно, спасибо :)
@gandjustas
ОтветитьУдалитьДа-да, недавно проводил косультацию компании, где SQL запросы писалися прямо в обратчике OnClick в WinForm, там были тоже аргументы "в высоконагруженных проектах нету DDD" и "нету тяжелых ORM".
Ваши высоконагруженные приложения на сколько высоконагружены? В чем была проблема использования нормальной ORM? В чем тяжеловесность NHibernate?
@Oleg Karpov
ОтветитьУдалить> Пост получился спорным
А вы сами используете описанные в посте способы или оставляете анемичную доменную модель?
@gandjustas
ОтветитьУдалить>практика говори что в высоконагруженных проектах нету DDD
Расскажите это @gregyoung и @abdullin - вот они удивятся :)
@Александр Бындю
ОтветитьУдалитьСмайлик в конце ставлю когда очевидные вещи рассказываю.
Сервис - класс с БЛ, без данных. Стратегия в чистом виде - вынесенное отдельное поведение из класса. То есть тоже класс без данных.
Любому грамотному проектировщику должно быть это очевидно. А то получается что за громкими словами типа aggregate root люди перестают понимать собственно структуру классов и её влияние на программу
> Расскажите это @gregyoung и @abdullin - вот они удивятся :)
ОтветитьУдалитьЧему? Они не делают высоконагруженных приложений. Более того сам Янг на одной из конференций сказал что DDD не подходи для сайтов.
@gandjustas
ОтветитьУдалить> Любому грамотному проектировщику должно быть это очевидно
Вам снова сюда.
Видимо вы считаете себя грамотным проектировщиком? Тогда как вы объясните изобретение вами слоистой архитектуры год назад?
Цитата: "Никакие правки верхнего слоя никак не могут повлиять на нижний". Грамотные проектировщики в курсе таких вещей, а вы предпочитаете: "У меня давно зрел принцип разделения на слои, который я не мог выразить словами. Прочитав вот этот пост я нашел правильную формулировку". Ну ее нашли до вас лет 30 назад. Возможно когда-нибудь вы изобретете DDD и назовете это как-нибудь типа Ориентация на предметную область. Когда это произойдет, напишите пост, пусть все еще раз поржут над MVP от Microsoft.
То, что я описал в этом посте, очевидно и не является открытием. Пост был для начинающих программистов, очень жаль, что он вызвал в вас столько противоречий.
@Александр Бындю, вот код из реального проекта:
ОтветитьУдалитьистория: у распоряжения есть определенная сумма, которую может оплатить сегодня финотдел. есть заявки от ЦФО, которые нужно сегодня оплатить. Объединять их можно по определенным правилам (опустим). Необходимо, чтобы в распоряжение невозможно было добавить заявку, если остаток по распоряжению стал =< 0. Это не расчет цен, конечно, и здесь нет необходимости выносить код в стратегии, т.к. скорее всего алгоритм меняться или добавляться не будет.
Отвечая на твой вопрос, я сделал как описано у тебя в посте, но это не говорит о том, что если бы у меня была другая ситуация, я не вынес бы этот код из объекта Order.
public class Order
....
public void AddRequest(Request request)
{
if (this.HasPayment)
{
var outstandingAmount = request.GetNotPaidAmount();
var notPaidAmount = GetNotPaidAmount();
//если есть еще остаток в распоряжении, который можно включить в оплату заявки
if (notPaidAmount.Amount > 0){
//если недоплата в заявке больше, чем недоплата в распоряжении, то добавляем в распоряжение заявку с остатком из распоряжения, в противном случае остаток из заявки
var share = outstandingAmount > notPaidAmount ? new Share(request, this, notPaidAmount) : new Share(request, this, outstandingAmount);
request.AddPartialPayment(share);
PartialPayment.Add(share);
}
}
}
@gandjustas
ОтветитьУдалить> DDD не подходи для сайтов
Наш 4х летний опыт это опровергает.
отличное решение :)
ОтветитьУдалитьИзвините, но, У ВАС НЕТ ВЫСОКОНАГРУЖЕННОГО ПРИЛОЖЕНИЯ.
ОтветитьУдалитьпочему в #1 было добавлено два метода - ChangePriceTo и SetCategory, вместо вписывания кода в соответствующие сеттеры? Ок, продукт начал сам контролировать смену цены и категории - так зачем выставлять наружу хоть какие-то признаки этого контроля?
ОтветитьУдалитьПрочитайте про entity в какой-нибудь умной книжке по DDD, или хотя бы тут у Фаулера: http://martinfowler.com/bliki/... там ясно сказано - entity - это то, что идентифицируется идентификатором.
ОтветитьУдалитьИ где там про то, что этот идентификатор обязательно одно поле?
нет
ОтветитьУдалитьА у вас есть? Вот стандартный список: http://www.insight-it.ru/highload/. Найдитем там хоть один ORM тяжелее Linq To SQL. Половина - вообще RoR c Active Record.
ОтветитьУдалитьМммм, причем здесь "вытягиваться из хранилища"? В данном случае мы говорили о конкретной реализации на основе List.
ОтветитьУдалитьНе понял к чему этот пример?
ОтветитьУдалитьДа, стало лучше. Спасибо.
ОтветитьУдалитьЯ не понимаю к чему ваш комментарий!
ОтветитьУдалитьЯ и говорю: прежде чем кричать, что ORM и DDD не используются в высоко-нагруженных приложениях, нужно ответить себе на вопрос - а у вас высоко-нагруженное приложение? И ответ, очевидно, - "НЕТ".
Это все-равно, что утверждать, что посаны должны ссать сидя, потому что тётя Клава ссыт сидя. Но, извините, тётя Клава-то ЖЕНЩИНА!
И на последок: DDD это вам не про чатик для миллионов хомячков, суть которого CRUD, это - про приложения для бизнеса, с огромным количеством логики.
Я не говорил, что одно поле, я сказал, что идентификатор любого типа! Причем это может быть какой-нибудь HiLo ключ, суть которого value-object.
ОтветитьУдалитьНо мы же, с вами не извращенцы, делать такие ключи? Или всё-же да?
Я бы посоветовал переопределить методы работы с коллекцией.
ОтветитьУдалитьКаким образом? Что вы имеете ввиду?
ОтветитьУдалить--- Равны могут быть не только Id, идентичность объекта могут представлять несколько полей
ОтветитьУдалить--- Прочитайте про entity в какой-нибудь умной книжке по DDD, или хотя бы тут у Фаулера: http://martinfowler.com/bliki/... там ясно сказано - entity - это то, что идентифицируется идентификатором.--- Я не говорил, что одно поле, я сказал, что идентификатор любого типа
я понял, что возражение было именно относительно того, что идентификатором может быть составной ключ.
Про извращенцев - согласен, что простой ключ намного предпочтительнее, но в жизни всякое бывает.
Не понимаете - уточните, а не пишите про писающую тетю Клаву.
ОтветитьУдалитьМой коментарий к тому, что ORM и DDD - не серебряная пуля.
Вы точно знаете какие приложения у меня или у gandjustas? Ответ, очевидно - НЕТ.
Примеры в посте - тривильные. Никакой оговорки про вагон логики в посте нет. Скорее наоборот, там US, которые встречаются намного чаще хомячковых чатиках/магазинчиках.
Если в вашем стиле: Извините, но НО В ПОСТЕ НЕТ ОГОВОРКИ ПРО ПРИЛОЖЕНИЯ ДЛЯ БИЗНЕСА. С ОГРОМНЫМ КОЛИЧЕСТВОМ ЛОГИКИ.
Есть случаи, когда Anemic проще. Есть приложения, в которых приходится писать почти всю логику на T-SQL. Любое решение "делать A или B" зависит от контекста. Контекст никак в посте не задан. Вы придумали один контекст (куча бизнес логики и обязательно nhibernate), gandjustas - другой (легковесные сайты для хомячков с минимальной логикой). Он пытается до вас донести ситуацию, когда его решение лучше (а она как бы вполне очевидна). А вы - просто пропихиваете свой придуманный контекст в качестве основного доказательства. Такое может себе позволить джуниор, но никак не архитектор, который должен понимать, что it always depends.
Лично у меня в текущем проекте - anemic, легковесный ORM, часть логики на базе (и да, составные ключи в некоторых таблицах) - выгоднее по производительности. Нагрузка не большая, просто данные специфические.
Просил как-то у Александра совета "как переписать на DDD, чтобы не получить проблем с производительностью", долго флудили, решили "оставить anemic".
>Он пытается до вас донести ситуацию, когда его решение лучше.
ОтветитьУдалитьЭто все-равно, что прийти на форум болельщиков Спартака и начать им доказывать, что Зенит круче.
>в высоконагруженных проектах нету DDD и нету тяжелых ORM.
Это равносильно тому, что вы будете рассказывать мне какая Bugatti Veyron крутая, при этом видев ее только на картинке в статье в Википедии.
Перечитайте внимательно мой предыдущий ответ и постарайтесь понять, что пост про DDD. И давайте мыслить в рамках применимости DDD. Если вам хорошо живется с anemic model - пожалуйста, это ваше право. Но пост про DDD.
Всё. вопрос исчерпан.
И да, мой контекст не придуман, а взят из 4 разных по своей природе больших проектов, каждый примерно по 10 человеко-лет.
http://www.infoq.com/interviews/Architecture-Eric-Evans-Interviews-Greg-Young
ОтветитьУдалитьЕсли это не высоконагруженное приложение, тогда у нас с вами разные понятия о высоконагруженных приложениях
Я внимательно перечитал пост Александра.
ОтветитьУдалитьПервая часть - про переход с anemic на ddd.
Подраздел "проблема", если его сократить, сводится к "у вас anemic и это очень плохо!".
Если пост был адресован не тем, кто сейчас сидит с анемиком, как gandjustas, то кому же тогда???
В переводе на ваши абстракции - фанат Спартака написал фанатам Зенита письмо "Спартак круче". И удивляется, почему они не приняли его веру. А второй фанат (вы) стоите рядом и удивляетесь, чего вообще эти зенитовцы понабежали и кидают в вас кирпичами. Письмо ж было про спартак.
4 проекта проекта - это хорошо, но это ваш контекст. Поверьте, я свой контекст тоже не с потолка взял.
Сейчас в комментах видна очень неприятная картина - Александр, написал топик, явно адресованный "тем, кто до сих пор с анемиком". И вместо того, чтобы спокойно доносить свою веру до еретиков, переходит на личности, и обзывает их укуренными велосипедистами. А вы ему помогаете. Александр же консультант, пусть предложит бесплатную консультацию по архитектуре gandjustas-у, и выложит результаты открытый доступ. А то уже год кулаками в воздухе машут.
А были прецеденты?
ОтветитьУдалить+1000
ОтветитьУдалитьНаиболее эпичным утверждением @b1084b6bff5d537d60595e93ceba9177 я считаю вот это:
ОтветитьУдалитьДанные != состояние объекта.
В контексте:
"Нет, ScannerData не является состоянием чего-либо. Он существует потому что существует запись в БД о сканере. Этот класс представляет данные о сканере в других частях программы. Это уже обязанность класса, остальные должны быть отделены.Данные != состояние объекта."хочется сказать ЛОЛШТО? Что же определяет состояние объекта, кроме его данных?
и ещё вопрос, чем по-твоему является объект "данные сканнера"? Он не является доменной сущьностью? Почему? К этому объекту неприменимо поведение?
ОтветитьУдалитьПоинт в том, что он ничем не отличается от любого объекта, как например "диван". Ты зацепился за "Data" в названии. Можно точно так же сказать, что "диван" представляет из себя "диван" в других частях программы и это уже обязанность этого объекта...
Важно, что "данные сканера" _являются_ данными сканера, а не "представляют данные сканера". Являются со всем присущим им поведением. (каким известно только конкретному домену). Представлять данные сканера может кто-то другой. ScannerDataRepresentative например =)
Любая информационная система работает с некоторыми данными, которые имею время жизни больше любого объекта и самой ИС. Так вот именно эти данные наделять поведением - большая ошибка.
ОтветитьУдалитьЧтобы разбираться с вопросом считай что "данные" являются внешними для программы, а состояние внутренним. Если посмотреть GoF то там есть паттерн Memento, который как раз превращает состояние в данные.
Пустая болтология. Ты же SOLID знаешь? Так вот буква S означает SRP, который говорит о том что у любого класса должна быть только одна обязанность, а остальные можно и нужно отделить.
ОтветитьУдалитьТак вот ScannerData имеет одну четко обозначенную обязанность - переносить данные сканера от поставщика этих данных потребителю. И какие бы еще не придумал обязанности их стоит отделить в соответствии с SRP.
Если же ты придумываешь ScannerDataRepresentative , то для начала попробуй описать его обязанности словами и в виде некоторого интерфейса. У тебя получится тот же самый ScannerData.
Куча акронимов и базвордов настолько съели мозги неокрепшим программистам, что они уже не используют даже самое базовые принципы проектирования, а только видят "доменные объекты", "агрегат руты" и прочую лабуду.
Глупый ответ. Ты же понимаешь русский язык? Умеешь отвечать по сути, а не уходить в сторону?
ОтветитьУдалитьScannerData - не имеет чётко обозначенной обязанности переносить данные сканера. По той причине, что требования к этому доменному объекту нигде не описаны и домен не указан. Единственным свидетельством того, что это "доменный объект", а не просто "data transfer object" служит текст поста, следующий за объявлением этого типа.
Приведу пример домена, где эта сущьность может иметь поведение. Домен: "Разработка ПО для сканера", задача: "Реализация постобработчика данных пршедших со сканера" В таких терминах вполне уместен метод scannerData.DoSomethingWithScannedImage();
Могут быть и другие домены и сущьность отыгрывать там разные роли, то что для этой сущьности чётко обозначена роль, ты придумал.
ScannerDataRepresentative:
Описываю его обязанности "предоставлять отсканированные данные". и теперь в виде интерфейса (всего лишь один из бесконечных вариантов в бесконечном варианте доменов)
IScannerDataRepresentative {
ScannerData GetScannerData();
}
>Куча акронимов и базвордов настолько съели мозги неокрепшим программистам, что они уже не используют даже самое базовые принципы проектирования, а только видят "доменные объекты", "агрегат руты" и прочую лабуду.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Предлагаю быть предельно конкретными. Я готов поспорить на счёт того, кто из нас "окрепший" программист, но только не в стиле таких глупых фраз. А то некоторые глупые троли, настолько глубоко возносят своё ЧСВ и неадекватно смотрят на мир, что очень смешно становится.
Вопросы был в том, что определяет состояние объекта, если не его данные? Состояние объекта это данные и есть. То что ты написал в этом сообщении это прееход в другой контекст, я не вижу в этом смысла.
ОтветитьУдалитьСостояние объекта целиком определяется данными, которые этот объект хранит. И данные ScannerData, вопреки твоему утверждению, являются состоянием объекта ScannerData. Как ни крути.
То что фактические данные, которые хранятся в полях этого объекта, являются "некоторыми данными информационной системы, которые имеют время жизни боьше любого объекта и самой ИС" никакого отношения к этому не имеет.
Кстати, можно ли считать последнее придложение, твоей официалной позицией. Т.е. считать, что "доменные объекты" и "агрегат руты" ты считаешь лабудой?
ОтветитьУдалитьЕще раз: во избежание путаницы давай словом "данные" называть то что находится вне объекта\программы, а "состоянием" то что находится внутри него.
ОтветитьУдалитьЕще раз повтори свою мысль с учетом того что я написал.
Да, причем такой, которая подменила многие best practicies проектирования, в том числе ОО-проектирования.
ОтветитьУдалитьА ты попробуй для начала объяснить свою мысль без применения терминологии DDD. Ты увидишь что нету разницы между value-object и domain object в данном случае.
ОтветитьУдалитьТы попадаешь в типичную логическую ошибку. Ты берешь некоторый "четрырехколесный агрегат с двигателем внетреннего сгорания" и говоришь что это "гоночный автомобиль", но выясняется что у него колеса разные, двигатель слабый и салон ужасен, а на проверку оказывается что это "трактор". Но слова "трактор" нету в твоем лексиконе. Ты сам ограничил свой словарный словарный запас.
Ну а теперь я начну глумиться.
ОтветитьУдалить> В таких терминах вполне уместен метод scannerData.DoSomethingWithScannedImage();
Вот ты искренне считаешь что для добавления обработки scannerData надо добавлять метод в сам scannerdata? Ты представляешь сколько способов обработки данных сканера в типичной программе?
> ScannerDataRepresentative:
ОтветитьУдалить> Описываю его обязанности "предоставлять отсканированные данные"
И ты описал провайдер данных. Теперь попробуй потребителя еще опиши. Получится некоторая "стратегия".
Вот у тебя и выйдет нормальный anemic дизайн без лишней связности
http://martinfowler.com/bliki/AnemicDomainModel.html
ОтветитьУдалитьThe fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design, exactly the kind of thing that object bigots like me (and Eric) have been fighting since our early days in Smalltalk. What's worse, many people think that anemic objects are real objects, and thus completely miss the point of what object-oriented design is all about.
Не вижу смысла пытаться объяснить мою мысль без терминологии ДДД. Если ты можешь и можешь в этом объяснении показать отсутствие разницы - welcome.
ОтветитьУдалитьЯ не думаю, что я понимаю к чему ты написал это про логическую ошибку. Также я не склонен считать, что имея нечто общее (автомобиль), я буду считать, что он удовлетворяет любому частном (гоночному автомобилю). Без должных на то оснований. Тем более, если автомобиль, автомобилем не является, а является "нет в моём лексиконе"
Я пожалуй поддержу твоё глумление.
ОтветитьУдалитьДа я искренне считаю, что если в домене так это и происходит и отражает реальность - то да, нужно. Но поскольку мы обсуждаем несуществующие вещи, а это был пример, которые ты просил и домен был выдуман, как и задача, то я понятия не имею сколько способов и каких и чего обработки. И уж тем более, что такое "типичная программа"
При чём тут стратегия? Мы говорим о паттерне "стратегия" или о какой-то другой стратегии?
ОтветитьУдалитьАнемик дизайн у меня никуда не выйдет, ScannerDataRepresentative это не анемик дизайн, это вообще ничего вместе с его потребителями. Это просто объект с одной явной ролью - предоставлять данные сканера.
Подменила? Что это значит? Подменила в твоей жизни, в твоих проектах?
ОтветитьУдалитьНе согласен, я не вижу никакой путаницы. Ну и мало того, я не вижу вообще никакого смысла, повторять свою мысль в тобой поставленных условиях. если хочешь её оспорить, я готов обсудить.
ОтветитьУдалитьВоде бы прописные истины, что объект состоит из данных, описывающих его состояние и операций (методов) описывающих его поведение и identity определяющей уникальность.
Именно. А вот ScannerData у тебя содержит две две обязанности: переносить данные и обрабатывать их. Раздели эти обязанности и это будет anemic дизайн.
ОтветитьУдалитьТо что "данные сканера" имеют какое-то поведение ты считаешь отражением реальности? Прости, но у тебя какая-то другая реальность.
ОтветитьУдалитьТо есть твоя мысль имеет смысл только в терминологии DDD, хотя DDD не является даже полным подмножеством методик проектирования. В том смысле что нельзя по DDD сделать все.
ОтветитьУдалитьНасчет логической ошибки: ты назвал scannerdata объектом домена, на каких основаниях? Наверное на тех что эванс сказал любые данные называть доменом и применять к ним некоторые правила.
Да, это ты назвал форму, но не суть. Суть заключается в назначении объектов. Я рассматриваю назначение, а ты его игнорируешь, рассматривая форму. При этом сам создаешь ложные утверждения, которые потом птыаешь опровергнуть.
ОтветитьУдалитьВ моей - слава богу нет. Но вот этот пост показывает что такое часто случается у других.
ОтветитьУдалитьА значит то что люди за формой персетают видеть суть.
А, простите, зачем это делать?
ОтветитьУдалитьА SRP просто так придуман? Наверное были практические соображения. Если почитать Мартина то он недвусмысленно пишет о том нарушение SRP ведет к тому что увеличивается площадь изменений. Небольшие изменения требований приведут к большим изменениям в коде.
ОтветитьУдалитьВы неверно толкуете SRP, "переносить данные" - это не обязанность, а всего лишь способность объекта хранить или не хранить состояние. SRP следует рассматривать только в контексте поведения объекта. Более того - ваше толкование SRP идет в разрез с ООП и ведет к процедурному стилю программирования.
ОтветитьУдалитьhttp://martinfowler.com/bliki/AnemicDomainModel.html
The fundamental horror of this anti-pattern is that it's so contrary to the basic idea of object-oriented design; which is to combine data and process together. The anemic domain model is really just a procedural style design,
Еще один. Состояние само по себе не нужно, оно нужно для реализации поведения. Вот есть в объекте состояние для хранения каких-то данных сканера. Для какого поведения оно нужно? Это поведение описывается фразой "перенос данных".
ОтветитьУдалитьSRP как и любой другой принцип надо рассматривать только в контексте поведения объекта. Если ты создаешь объект и не можешь ничего сказать о его поведении, то ты зря его создаешь.
Мое толкование SRP совпадает с тем что написал Мартин. И ведет к более стройному дизайну.
Перенос данных - это не поведение. Поведение - это явно написанная логика (читайте - код). Тот факт что у объекта есть 3 property - поведения ему нисколько не добавляет
ОтветитьУдалитьСчего бы? Поведение - то чем пользуются другие объекты. Если объект отдает данные,то это уже поведение. Даже еслиэто 3 свойства.
ОтветитьУдалитьThree properties characterize objects:
ОтветитьУдалитьIdentity: the property of an object that distinguishes it from other objectsState: describes the data stored in the objectBehavior: describes the methods in the object's interface by which the object can be used
http://en.wikipedia.org/wiki/Object_(computer_science)
http://stackoverflow.com/questions/1332704/state-vs-behavior
отличное представление;)
ОтветитьУдалитьПредлагаю свою версию решения примера №1 http://muradovm.blogspot.com/2011/12/blog-post.html
ОтветитьУдалитьВы добавили важное условие "Должна быть возможность оперативно менять ценовую политику.", поэтому решение получилось гораздо более громоздким.
ОтветитьУдалитьУсложнять его можно сколько угодно :) Спасибо за пример.
Это не будет анемик дизайн. Анемик дизайн - это когда поведение, которое принадлежит объекту, выносится в сторонние сервисы. В моём примере такого нет.
ОтветитьУдалитьКто говорил об обработке данных, откуда ты их взял?
Предлагаю читать определения терминов, прежде чем оперировать ими. Тролерская манера общения мне тоже твоя нравится, постоянно игнорировать вопросы, отвечать не о сути, переводить контекст, подменять понятия. Классика =)
Да, я сознательно его добавил. Чтобы проще было жить, когда когда клиент позвонит и скажет: "Слушай, у нас тут акция на кой-какой товар, нам надо подправить эту твою программу".
ОтветитьУдалитьУ меня в практике было и похлеще, когда клиент просил изменить "кое-что" в программе, что совершенно не только не соответствовало первоначальному ТЗ, но и никогда не предполагалось.
У тебя в блоге указано, что ты практикуешь Agile, а гибкий дизайн называешь "усложнением".
олололо, состояние само по себе не нужно? не ты ли с саблей на голо кричишь, что хранить данные (определяющие состояние) это отвесттвенность этого объекта и поведения в нём быть не должно.
ОтветитьУдалитьчто является поведением данного объекта? хранение данных? это "роль в твоих терминах", что тогда поведение? нет поведения? но у него же естть состояние, которое нужно только для поведения, которого нет, вот незадача да?
Как всегда ожидаю ответ не по сути.
Эпично, наличие свойств у объекта это уже поведение. Эпично. Т.е. определять любое поведение у объекта, у которого уже есть публичные свойства - нарушение SRP?
ОтветитьУдалить"У тебя в блоге указано, что ты практикуешь Agile, а гибкий дизайн называешь "усложнением".
ОтветитьУдалитьДело в том, что самому себе не надо усложнять условия. Когда они появляются в виде измененных требований, надо реагировать, но загадывать наперед, что это потребуется - лишнее. Гибкость в Agile - это умение быстро перестраиваться под изменение окружающей среды, а не загадывание наперед и точно не усложнение. Вот тут подробно описано - принцип KISS
За какой формой? О каких best practicies проектирования идёт речь? Можешь сформулировать их? DDD это ни что иное как набор практик проектирования + особый подход к организации работы над проектом.
ОтветитьУдалитьМне не нравятся безосновательные утверждения. Покажи ка мне моё ложное утверждение и попытку опровергнуть, чтобы не быть голословным.
ОтветитьУдалитьИ что ты называешь формой? В чём суть? Ты рассматриваешь назначение? Назначение объекта в неизвестной предметной области - как я понимаю. Т.е. неизвестное назначение объекта. Зачем ты его рассматриваешь?
Эта реальность никому неизвестно, потому что, ещё раз, домен нигде не описан и не указан, а значит объект может быть любым. Мне конечно странно, что такой одарённый человек, не способен, понятть этого. А также не способен придумать домен, в котором у этого объекта может быть поведение.
ОтветитьУдалитьЧто скажешь, если у нас домен это детский мультфильм, которые наглядно показывает работу сканнера, где данные сканера умеют разговаривать на русском языке с другим персонажем мульника "контроллером сканера" и т.п.
В таком домене у этого объекта тоже не будет поведения? Напрмер метод "говорть" scannerData.Tell("something"); в твоём анемичном мире ты бы этот метод вынес в серис? В какой? ScannerDataManager? Если да, то зачем?
Ну и ещё раз вывод: ты сам себе придумал домен, основываясь на имени типа "ScannerData". В придуманном тобой домене у этого объекта нет поведения (да его нет, не смотря на наличие свойств =)), есть только состояние. Но это ты придумал домен, ты начал рассужать в его терминах. Автор поста, просто сказал, что это доменный объект и у него есть поведение. Кто мы такие, чтобы спорить, не зная о каком домене идёт речь?
Соглашусь с http://blog.byndyu.ru/2011/12/domain-driven-design.html#comment-383541636
ОтветитьУдалитьПредставь себе ситуацию, когда ты придумал себе это "усложнение", потратил на него время, а твой заказчик развернул проект в другую сторону и вся фича, вместе с усложнением, стала не нужной, Или еще хлеще: это "усложнение" стало, попросту, мешать.
Станислав Выщепан,
ОтветитьУдалитьВам нужно прочитать Гради Буч "Объектно-ориентированный анализ и проектирование с примерами приложений на С++". Вы не видите границ между разными методами проектирования ПО, а это базис. До прочтения книги я попрошу вас не писать комментарии, а после с удовольствием обсудим разные нюансы предложенной в статье реализации.
Ну тогда как минимум ты "усложнил" свой же пример
ОтветитьУдалитьprivate const int SomeMinPriceFromDomainLogic = 100;
Ну и также нет абсолютно никакой уверенности, что размещение логики
if (Price < SomeMinPriceFromDomainLogic) SetCategory(CategoryType.Regular);
и
if (Category == CategoryType.Regular) Discount = 0;
в типе Product вообще соответствует DSL.
Можно еще раз повторить, что по US "ровно 100 рублей порог и устанавливать только Regular категорию. Мамой клянусь, больше ничего не надо"
Ну тут нет места для обид, правда. Пример должен был показать, что логику можно писать в самом доменном объекте, если она к нему относится, в противоположность анемичной моделе данных.
ОтветитьУдалитьНи мой, ни твой пример не может быть полным и правильным, т.к. у нас есть только одна User Story и мы не видим куда двигается проект, нет контекста. С другой стороны и твой и мой пример показывают, что доменные объекты должны содержать в себе логику, это и была цель статьи.
Я её читал и мейера читал. Только вот много ошибочного в этих книгах. Один пример с датчиками является антипаттерном ОО-проектирования. Я как раз подобными задачами с датчиками занимался на момент прочтения книги и создавать по классу на датчик было самым плохим решением.
ОтветитьУдалитьА зачем столько сложностей? Почему не вызывать в политику на событие записи свойств товара в базу?
ОтветитьУдалитьВообще попрошу впредь не отсылать к разным апологетам. К вашему несчастью я их все читал, но и много других книг прочитал. Поэтому если есть объективные аргументы - пишите, если нет, то мнение буча, мейера, фаулера, еванса и прочих не являются объективными. Кстати то что они все пишут зачастую верно только на искусственных примерах в книгах, но не в реальных приложениях, и то не всегда. И этот блог страдает тем же самым.
ОтветитьУдалитьТогда вам лучше написать свою книгу, потому что ваше понимание ООП отличается от общепризнанных.
ОтветитьУдалитьНет, понимание ООП совпадает. А вот понимание что и когда применять скорее всего не совпадает с бучем, фаулером и эвансом.
ОтветитьУдалитьЕще раз: поведение первично. Если есть свойства - уже есть поведение. Если нету поведения, то есть не можете его определить, то скорее всего зря создавали класс.
ОтветитьУдалитьДобавлять к классу другое поведение, помимо существующего - нарушение SRP.
Я бы опирался на определение Кея, который говори что ООП - это объекты с Identity, обменивающиеся сообщениями друг с другом.
ОтветитьУдалитьСтанислав, в этих книгах выработан общий для программистов язык, с помощью которого можно понимать друг друга. Если вы понимаете по-своему, то только описав свой словарь вы сможете донести мысль. Я вас не понимаю (это не стеб и не шутка), когда вы оперируете понятиями поведение, состояние и т.п., они у вас просто другое значат.
ОтветитьУдалитьМое понимание совпадает со статьей OOP и книгой Гради Буч "Объектно-ориентированный анализ и проектирование с примерами приложений на С++". Скажите, где посмотреть ваше?
Добавил, стало лучше?
ОтветитьУдалитьПоправил
ОтветитьУдалитьСпасибо, стало лучше
ОтветитьУдалитьООП - создание программы в виде объектов, которые обмениваются сообщениями. В большинстве языков обмен сообщениями реализован через вызов методов. Все объекты обладают identity.
ОтветитьУдалитьМножество принимаемых сообщений называется интерфейсом.
Поведение - наблюдаемая реакция объекта на сообщение.
Состояние - совокупность полей, необходимых для реализации поведения.
Кроме состояния есть еще внешние данные для программы. Самое главное что эти данные не имеют однозначного отображения на объекты, как минимум потому что свойство identity для данных не совпадает с identity для ООП.
Инкапсуляция - сокрытие деталей реализации за интерфейсом.
Data hiding - сокрытие данных - блокирование прямого доступа к данным через интерфейс объекта.
Полиморфная переменная - переменная, которая в может иметь разный тип.
Полиморфизм в ООП - отправка сообщения объекту, на который ссылается полиморфная переменная.
Типизированные языки позволяют описывать классы объектов.
Наследование классов создает соотношение тип-подтип (is-a).
Виртуальные методы классов являются механизмом реализации полиморфизма в типизированных языках.
Кто-нить этого не знает? Скорее всего все знают.
А вот отличия заключаются в том как и какие объекты создавать для работы. В практике я опираюсь на системный подход (http://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%BD%D1%8B%D0%B9_%D0%BF%D0%BE%D0%B4%D1%85%D0%BE%D0%B4) и системный анализ (http://ru.wikipedia.org/wiki/%D0%A1%D0%B8%D1%81%D1%82%D0%B5%D0%BC%D0%BD%D1%8B%D0%B9_%D0%B0%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7).
Причем системный подход очень соответствует ООП, о котором писал Кей (автор ООП если что). Он описывал рекурсивную композицию объектов, которая полностью соответствует системному подходу. Но мало писал о сути самих объектов. А вот системный подход оперирует функциями и процессами. Такой подход позволяет выявить объекты в виде наборов функций, которые сразу же хорошо соответствуют SRP и ISP.
Кстати Буч тоже ссылается на системный подход, но почему-то в примерах проектирование начинает снизу вверх. Это приводит к тому что программа становится неустойчивой к изменениям. Например добавление датчика приведет к переписыванию немалой части кода.
>> решение с фабричным методом CreateСкорее с методом создания (Creation Method). Фабричным можно назвать метод, который переопределяется в унаследованных слассах.
ОтветитьУдалитьДа причем тут обиды.
ОтветитьУдалитьЕсли сделать поправку на то, что DSL этого примера (опять же из US) категорически не соответствует представлениям DSL продаж и изменений цены конкретно моим. Ну или может быть для вашего клиента этот проект настолько краткосрочен, что даже инфляция не скажется ценовом пороге. Да, твое решение вполне себе жизнеспособно.
Одно можно сказать с уверенностью. Если пост собрал свыше 200 коментариев бурных обсуждений, то пост-провокация удался на славу ;-)
Все зависит от того в какой бизнес-транзакции будут использоваться объекты класса Product.Ну и наверное не очень хорошо, когда момент времени выполнения какой-то логики должен быть связан с записью в базу. Скорее должно быть наоборот.
ОтветитьУдалитьПереформулирую. Применение политики необходимо только при попытке изменить товар пользователем. Почему бы не вызывать её только в этот момент?
ОтветитьУдалитьМой ответ переформулировать не надо. Пусть останется таким же.
ОтветитьУдалить> что даже инфляция не скажется ценовом пороге
ОтветитьУдалитьДело в том, что как только ценовая политика поменяется, надо будет сделать рефакторинг и сделать ценовую настраиваемой. Пока она не меняется, для этого не надо делать механизмов.
> Если пост собрал свыше 200 коментариев бурных обсуждений, то пост-провокация удался на славу
Это не пост-провокация, а набор простых приемов для ухода от анемичной доменной модели. Я искренне удивлен, что он вызвал столько обсуждений.
То есть вы считаете хорошей идеей не привязывать код к некоторому сценарию исполнения, а выполнять его всегда просто потому что так можно делать? А если завтра станет такой сценарий неприемлем? Например возникнет задача сохранения черновиков, для которых не надо применять политику.
ОтветитьУдалитьЗавтра могут появиться абсолютно любые задачи. В том числе и такие, которые потребуют перепроектировать этот дизайн.
ОтветитьУдалитьНо будет еще страшнее, если какой-то компонент установит новую цену у товара и попытается воспользоваться например категорией товара.
К сожалению, ваше предложение приведет только к тому, что будет возможно установить для товара некорректное состояние.
Да, именно поэтому надо делать как можно меньше. Меньше писать кода, меньше исполнять логики. Вы ведь понимаете что проще добавить вызов, чем убрать его.
ОтветитьУдалитьНасчет того что что-то установит цену товара, а потом воспользуется категорией - неправдоподобный сценарий при любом раскладе. Некорректное состояние всегда можно установить если просто залезть в базу. Программа кстати должна вести себя адекватно в этом случае.
>То есть твоя мысль имеет...
ОтветитьУдалитьПоразительный вывод, потруднись объяснить как ты его сделал? Я подозреваю, что формальная логика не замешана.
>Насчет логической ошибки: ты назвал scannerdata объектом домена, на каких основаниях? Наверное на тех >что эванс сказал любые данные называть доменом и применять к ним некоторые правила.
На тех основаниях, что так написано в посте, который мы комментируем. А Эванс подобной чепухи не говорил.
Разберём по пунктам.
ОтветитьУдалитьЕсли есть свойства - уже есть поведение.
Добавлять к классу другое поведение - нарушение SRP.
Вывод, добавлять к классу у которого есть свойства, любое поведение - нарушение SRP.
Ты согласен с этим? Должен быть, выводы из твоих слов. Да?
О применимости DDD http://gandjustas.blogspot.com/2011/12/ddd.html
ОтветитьУдалить>> и внезапно в базе оказались пустые Name
ОтветитьУдалить>Возможно в Share Point такие "внезапности" норма, но для остального мира так не бывает)))
увы бывает и не только в Share Point. есть еще системы работающие в стыке с другими системами и получающие(синхронизирующие) данные оттуда, и даже если в ТУ и прописано что Name-у быть всегда, то это еще ничего не значит )
про публичные сеттеры. Я полностью поддерживаю идею про public IEnumerable для коллекций, но совсем не радуют методы Set% или Change%To, так как минимум теряется удобство использования тогоже автомаппера плюс код вида product.Price = product.Price * 2; привычнее и нет необходимости искать нужный Change. Внутри класса этот код может быть уже развернут для контроля изменений до:
private double price;
public double Price
{
get { return price };
set
{
price = value;
if (price > 1) {
....
}
}
}
nHibernate при этом маппится как access="field.camelcase"
Обычно изменение любого поля объекта доменной модели должно сопровождаться записью в репозиторий. А по сему скажите, кто ответственнен за эту операцию - сам метод ChangePriceTo или тот, кто его вызывает? Вопрос не холивара ради, а действительно хочется услышать мнения на этот счёт, ибо подозреваю, что большинство хелпер-классов появляются именно по этой причине...
ОтветитьУдалитьЖелательно чтобы и сам метод и объект Entity ничего не знали ни про репозитории ни про UnitOfWork. За сохранение и коммит в репозитории отвечает вызывающая сторона - метод сервиса или метод команды.
ОтветитьУдалитьЭто понятно, что не сам объект пишет в базу (если только вы не использует ActiveRecord). Естественно, что скорее всего используется Unit of Work. Вопрос был о другом: кто должен сделать вызов unitOfWork.MarkDirty(product) после вызова product.ChangePrice(newPrice) - тот кто инициирует изменение цены (например, метод объекта слоя сервисов), или сам продукт сразу после установки новой цены? Если продукт, то как он получит объект unitOfWork, который представляет текущую бизнес транзакцию? Если же тот, кто инициирует операцию, то как гарантировать целостность операции, включающей изменение значения и и маркировка его для записи в репозиторий.
ОтветитьУдалитьОдин из путей решения этой проблемы, который я иногда использую, - это введение в модель особых объектов Business Action, которые реализуют сложные алгоритмы бизнес логики. Как раз те, которые вы предлагаете выносить в публичные методы агрегирующих корней. Сами же объекты-сущности реализуют лишь простые операции, но таким образом, чтобы оставлять объект в консистентном состоянии.
Поясню, опираясь на ваш пример. Метод ChangePrice будет реализован ровно также, как у вас, но он не будет доступен напрямую внешнему по отношению к модели клиенту. Для изменения цены продукта нужно будет создать объект ChangePriceAction, параметризовав его объектом product и вызвать что-то вроде метода Execute. Внутри будут выполнены все операци связанные с модификацией модели и инициированием операций записи в репозиторий. В простейшем случае метод Execute будет выглядеть так:
void Execute(IUnitOfWork unitOfWork)
{
this.Product.ChangePrice(this.NewPrice);
unitOfWork.MarkDirty(this.Product);
}
Что-то похожее описывается вот в этой статье: http://msdn.microsoft.com/ru-ru/magazine/dd882510(en-us).aspx
Хотелось бы услышать ваше мнение на решение подобной проблемы - без использования команд.
>
ОтветитьУдалитькто должен сделать вызов unitOfWork.MarkDirty(product)
Давайте конкретно про NHibernate. Когда вы берете объект из Session, то эта сессия следит за всеми изменениями объекта самостоятельно.
Примерный код:
1) Взяли объект из БД
Product product = session.Get(1);
2) Где-то в коде поменяли поле объекта или изменили связанную коллекцию
product.Name = "new name";
product.Items.Clear();
3) Где-то в коде мы коммитим изменения в сессии
session.Commit();
Сессия сама сохранила "что было" и "что стало" и сгенерировала нужный SQL скрипт.
А что делать в случае если Product порождает новый объект для которого он не является AR(каскады не работают)? Да, модель возможно кривая, но будем считать что это легаси код.
ОтветитьУдалитьПриведите, пожалуйста, пример кода. Можно писать псевдо-кодом, лишь бы было понятно.
ОтветитьУдалитьНапример, что-то вроде.
ОтветитьУдалитьService:
var customer = customerRepository.Get(id);
customer.AddLoan(amount);
Customer:
void AddLoan(Money amount)
{
if(currentLoanSum + amount > RED_LINE_AMOUNT) {
var historyItem = new LoanHistoryItem(this);
// а вот тут так и просится
Something.Persist(historyItem);
}
}
p.s. считаем что модель менять нельзя и протягивать UoW в Customer очень бы тоже не хотелось, потому я использовал Event-ы.
Здесь, в идеале, historyItem должен добавляться в коллекцию HistoryItems, которая принадлежит Customer и замаплена в БД.
ОтветитьУдалитьТ.е. внутри метода AddLoan будет:
historyItems.Add(historyItem);
Тогда UoW сам добавить новый элемент в коллекцию.
> порождает новый объект для которого он не является AR(каскады не работают)
В этом случае, конечно, надо отрефакторить. Если уж совсем никак код нельзя менять, то можно и событие пробросить. Не совсем понятна реалиация Something.Persist(historyItem), имелись ввиду вот такие события http://www.udidahan.com/2009/06/14/domain-events-salvation/ ?