Domain-Driven Design: Простые способы улучшить дизайн ПО

1 декабря 2011 г.

Недавно я проводил консультацию, в ходе которой заметил, что можно эффективно использовать несколько простых приемов для улучшения дизайна ПО. Несмотря на то, что приемы и правда простые, эффект получает очень сильным. Эти приемы давно известны и применяются в повседневной работе, особенно теми, кто уже использует 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

Domain-Driven Design: создание домена

Domain-Driven Design: aggregation root

206 комментариев:

  1. 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 и вся инкапсуляция оказывается мнимой.

    ОтветитьУдалить
  2. И кстати, в чем реальная потребность наследовать все доменные объекты от IEntity?

    ОтветитьУдалить
  3. 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 и ни с чем больше.

    ОтветитьУдалить
  4. Парсер съел угловые скобочки у IRepository
    должно быть так: IRepository{TEntity} where TEntity: IEntity

    ОтветитьУдалить
  5. Sergey Zwezdin - по поводу перенесения логики установки в Setter.

    Есть такое соглашение в среде .net разработчиков: свойства не должны вызывать побочных эффектов и должны быть независимыми от порядка вызовов.

    ОтветитьУдалить
  6. @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());
    }

    ОтветитьУдалить
  7. Александр Бындю,

    Кроме параметризованных конструкторов мне еще нравиться решение с фабричным методом Create, который инкапулирует всю логику создания объекта. Конструктор по умолчанию, при этом будет protected (для обеспечения правильной работы NHibernate, если это entity) или private.

    Мне это решение кажется более удачным, в виду того простого факта, что рефакторить методы в ReSharper намного удобнее, чем конструкторы.

    Также данный подход убирает возможность пользоваться инициализаторами, которые зачастую только мешают, а не помогают.

    Инициализаторы объектов + конструктор по-умолчанию я предпочитаю использовать для DTO и прочих ViewModel - объектов без логики, т.к. в этом случае использование параметризированного конструктора или фабричного метода только добавляет лишнего кода и не приносит никакой пользы.

    ОтветитьУдалить
  8. Александр Бындю,
    Сергей, скорее всего, имел ввиду варварское приведение типов:

    var roles = (ICollection{Role})account.Roles;

    ОтветитьУдалить
  9. @hazzik

    > Кроме параметризованных конструкторов мне еще нравиться решение с фабричным методом Create...

    +1

    Отличная идея!

    ОтветитьУдалить
  10. @hazzik

    > Сергей, скорее всего, имел ввиду варварское приведение типов

    Ого! От программистов, которые готовы так делать и рефлексии можно ожидать)))

    ОтветитьУдалить
  11. Как я понимаю, ты консультировал каких-то разработчиков и внезапно обнаружил, что у них даже инкапсуляция не выполняется? ))) Как знакомо!
    Я вот тоже недавно видел код бизнес-логики, который скорее всего был написан теми людьми, что пять минут назад программировали микроконтроллеры.
    Пора бы и мне уж написать в блог: "Проводя консультацию по одному из проектов, я заметил, что неплохо было бы использовать классы..."

    ОтветитьУдалить
  12. @Мурадов Мурад

    > Как я понимаю, ты консультировал каких-то разработчиков и внезапно обнаружил, что у них даже инкапсуляция не выполняется?

    Дело не в инкапсуляции, а движению от анемичной доменной модели к более насыщенной, к формированию DSL.

    > Проводя консультацию по одному из проектов, я заметил, что неплохо было бы использовать классы...

    :)

    ОтветитьУдалить
  13. @Александр Бындю
    > Дело не в инкапсуляции, а движению от анемичной доменной модели к более насыщенной, к формированию DSL.
    Мне почему-то кажется, что люди, которые оставили публичные сеттеры в *** и написали ***Service/Helper, про анемичную доменную модель и DSL впервые услышали от тебя )))

    Кстати, тебе удается узнать, что происходит с проектом после твоей консультации? Было бы интересно узнать.

    ОтветитьУдалить
  14. По поводу 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-ы наружу отдавать, но тогда давайте называть вещи своими именами. :)

    ОтветитьУдалить
  15. по поводу выбора в пользу IEnumerable - это просто обычное использование полезного в работе принципа You ain't gonna need it. Для вашей логики достаточно возможностей IEnumerable? так зачем использовать IList

    ОтветитьУдалить
  16. 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 нет.

    ЗЫ: как-то я вдоволь натрахался с кодом, который делал вот такую вот "честную" инкапсуляцию коллекций.

    ОтветитьУдалить
  17. Здесь 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? :)

    ОтветитьУдалить
  18. menozgrande,

    Здесь у вас по-факту нарушение принипов из-за неправильного отображения реляционных данных в объекты, по-простому можно сказать, что это сделано "влоб".

    И Money и Currency должны быть ValueObject, и замаплены (если вы используете NHibernate) как component, при этом, тот факт, что они лежат в отдельной таблице никакой роли не играет.

    Попробую нарисовать правильный мапинг и выложу гист.

    ОтветитьУдалить
  19. hazzik, использую EF code-first. Если можно, то прошу учесть. :)
    В nhibernate у меня не было с этим проблем в другом проекте, а здесь... беда :)
    был бы благодарен за поправку на моем примере :)

    ОтветитьУдалить
  20. так выглядит mapping currency:
    http://sapafinance.codeplex.com/SourceControl/changeset/view/88fd0f581e94#SapaFinance.Domain.EntityFramework%2fMappings%2fCurrencyConfiguration.cs

    ОтветитьУдалить
  21. Допустим, у нас есть 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.

    ОтветитьУдалить
  22. @Oleg Karpov, к сожалению ни с EF ни EF CodeFirst не работал так плотно, по-этому не помогу.

    ОтветитьУдалить
  23. @Oleg Karpov, могу предложить небольшой workaround для EF, если вы хотите соблюсти чистоту кода для внешнего наблюдателя: Money не наследовать от AbstractEntity, добавить ему приватное(или публичное - не важно) свойство id и замапить его в базу (ну если EF позволяет делать это без поля, то еще круче). Тогда для EF оно будет как-бы entity, а для стороннего наблюдателя как value-object.

    ОтветитьУдалить
  24. @Sergey Zwezdin

    > Господа, вы уж определяйтесь у вас инкапсуляция или только ее видимость.

    Вы в своих проектах отдаете List наружу?

    ОтветитьУдалить
  25. if (roles.Any(x => x == role))
    throw new AccountStateException()

    Зачем здесь лямбда когда можно .Contains(role)?

    ОтветитьУдалить
  26. @ASK

    Можно и так, не принципиально. Пример исправлял ошибку в дизайне ПО.

    ОтветитьУдалить
  27. Ржунимагу. Значит отделение данных от методов работы с ними нарушает SRP, а помещение всего в один класс нет?

    ОтветитьУдалить
  28. @gandjustas

    > Значит отделение данных от методов работы с ними нарушает SRP

    Отделение данных от методов работы с ними? Что это значит? При чем здесь SRP? Что вы имеет ввиду под "помещение всего"? Чего "всего"?

    ОтветитьУдалить
  29. Решение для "№1 Убираем публичные сеттеры" просто неверно. Так как сервис выполняет более одного действия: устанавливает price и category. А в "правильном решении" два метода, которые по сути сеттеры свойств и работают несогласовано.

    Кстати класс сервиса как раз инкапсулирует логику, это гораздо важнее чем инкапсуляция данных.

    ОтветитьУдалить
  30. @gandjustas

    > Значит отделение данных от методов работы с ними нарушает SRP...

    Вопросы те же. Отделение данных от методов работы с ними? Что это значит? При чем здесь SRP? Что вы имеет ввиду под "помещение всего"? Чего "всего"?

    > А в "правильном решении" два метода...

    Это называет корень агрегации, который отвечает за правильное состояние домена приложения. Слышали про такое понятие?

    > Кстати класс сервиса как раз инкапсулирует логику, это гораздо важнее чем инкапсуляция данных.

    Что такое инкапсуляция логику? Что значит инкапсуляция данные? Почему первое важнее второго?

    ОтветитьУдалить
  31. > Отделение данных от методов работы с ними?
    Это значит что данные отдельно - методы отдельно (в другом классе)

    > При чем здесь SRP?
    SRP говорит что у класса есть ровно одна неотделимая обязанность, а остальные отделимые и ДОЛЖНЫ быть отделены.

    Если рассмотреть первый блок код, то видно что ScanerData занимается переносом данных их хранилища. Это и есть неотделимая обязанность, без нее scannerdata не существовал бы. Остальные обязанности - отделимые и находятся в отдельных классах.

    ОтветитьУдалить
  32. @gandjustas

    Я не устану переспрашивать.

    Вопросы те же. Отделение данных от методов работы с ними? Что это значит?

    При чем здесь SRP? Не что такое SRP, а как это связано "Значит отделение данных от методов работы с ними нарушает SRP"

    Что вы имеет ввиду под "помещение всего"? Чего "всего"?

    Это называет корень агрегации, который отвечает за правильное состояние домена приложения. Слышали про такое понятие?

    Что такое инкапсуляция логики? Что значит инкапсуляция данных? Почему первое важнее второго?

    > Это значит что данные отдельно - методы отдельно (в другом классе)

    Я правильно понимаю, что вы хотите объект разделить на объект-состояние (данные) и объект поведение (методы)? Таким образом, не нарушив SRP?

    ОтветитьУдалить
  33. Корень аггрегации тут не при чем. В "паравильном решении" предложенный интерфейс класса никак не позволяет поддерживать бизнес-логику.

    Из этого должно быть понятно что инкапсуляция логики - сокрытие деталей за интерфейсом - важнее чем инкапсуляция данных. Так как только инкапсуляция логики позволяет поддерживать корректность реализации БЛ

    ОтветитьУдалить
  34. > Вопросы те же. Отделение данных от методов работы с ними? Что это значит?

    Я уже ответил на этот вопрос выше. Читайте внимательно и вдумчиво.


    > Я правильно понимаю, что вы хотите объект разделить на объект-состояние (данные) и объект поведение (методы)? Таким образом, не нарушив SRP?

    Нет, ScannerData не является состоянием чего-либо. Он существует потому что существует запись в БД о сканере. Этот класс представляет данные о сканере в других частях программы. Это уже обязанность класса, остальные должны быть отделены.

    Данные != состояние объекта.

    ОтветитьУдалить
  35. @gandjustas

    В предыдущем комментарии гораздо больше вопросов, не игнорируйте их.

    А вот это я считаю надо в рамку: "Из этого должно быть понятно что инкапсуляция логики - сокрытие деталей за интерфейсом - важнее чем инкапсуляция данных. Так как только инкапсуляция логики позволяет поддерживать корректность реализации БЛ"

    ООП стремилось дать объекту состояние и поведение, а вы в очередной раз (как это было с открытием слоистой архитектуры) изобрели велосипед с квадратными колесами. Читайте мат. часть OOP.

    ОтветитьУдалить
  36. "№2 Добавляем параметризованный конструктор" - решение правильное в теории, но не на практике.

    На практике обязательность поля может меняться со временем и внезапно в базе оказались пустые Name, а конструктор требует непустого. Программа начинает падать.

    По сути вместо кода надо как раз написать anemic класс и использовать внешнюю валидацию.

    ОтветитьУдалить
  37. > В предыдущем комментарии гораздо больше вопросов, не игнорируйте их.

    Я на них уже ответил.

    > ООП стремилось дать объекту состояние и поведение
    Только сначала поведение, а потом уже состояние, его реализующее. А тут попытка представить что данные это состояние и натянуть на него сверху поведение.

    ОтветитьУдалить
  38. @gandjustas

    Еще так много ваших гипотиз требуют ответов.

    > и внезапно в базе оказались пустые Name

    Возможно в Share Point такие "внезапности" норма, но для остального мира так не бывает)))

    ОтветитьУдалить
  39. @gandjustas

    К предыдущим вопросам, прибавляется:

    > Только сначала поведение, а потом уже состояние, его реализующее.

    Что это значит? Сначала поведение? Т.е. всегда сначала поведение? Нельзя "натягивать" сверху поведение?))

    Я сохранил эту страницу на всякий случай, чтобы вы не удалили свои комментарии))

    ОтветитьУдалить
  40. "№3 Заменяем List, IList, ICollection и т.п. на IEnumerable" кроме технической ошибки с выставлением List, как IEnumerable, которая банально решается с помощью .AsReadOnly есть более тяжелая ошибка.

    historyEntries обязаны вытягиваться из хранилища вместе с account, причем historyEntries будет расти непрерывно, и баналльные операции вроде проверки что account принадлежит к какой-то роли будут тормозить. Причем нету этому никаких оправданий.

    ОтветитьУдалить
  41. >поведение ... состояние, его реализующее.
    Прости, ржал 5 минут.

    ОтветитьУдалить
  42. > Что это значит? Сначала поведение? Т.е. всегда сначала поведение? Нельзя "натягивать" сверху поведение?))

    Да, именно так. Сначала надо придумать что делает программа, а потом уже формировать структуры данных с которыми она работает. Тут парадигма не имеет значения, это общее правило.

    ОтветитьУдалить
  43. @gandjustas

    > historyEntries обязаны вытягиваться из хранилища вместе с account, причем historyEntries будет расти непрерывно

    Что это за процесс непрерывно роста? Как это происходит?

    > баналльные операции вроде проверки что account принадлежит к какой-то роли будут тормозить

    Скорее примеры! Обозначьте причины тормозов.

    > Причем нету этому никаких оправданий

    Есть

    ОтветитьУдалить
  44. > Возможно в Share Point такие "внезапности" норма, но для остального мира так не бывает)))

    То есть ты можешь гарантировать что обязательность поля сразу же задается при проектировании и никогда не меняется? Я тебе просто не верю.

    ОтветитьУдалить
  45. > Что это за процесс непрерывно роста? Как это происходит?

    Вызывается addrole

    >Скорее примеры! Обозначьте причины тормозов.
    Вытягивание historyEntries из хранилища.

    ОтветитьУдалить
  46. @gandjustas

    > То есть ты можешь гарантировать что обязательность поля сразу же задается при проектировании и никогда не меняется? Я тебе просто не верю.

    Давай в верю-неверю поиграем)) Так, ладно, отключаю стеб.

    > обязательность поля

    Что такое обязанность поля?

    > задается при проектировании и никогда не меняется?

    Меняется, это называется рефакторинг. Конструктор при этом тоже меняется. Есть довольно хорошее предложение - создать фабричный метод, чтобы было проще рефакторить.

    @gandjustas, список вопросов еще жив, добейте их.

    ОтветитьУдалить
  47. >Что такое обязанность поля?
    Как минимум то что оно должно быть не null, логично?

    >Меняется, это называется рефакторинг. Конструктор при этом тоже меняется. Есть довольно хорошее предложение - создать фабричный метод, чтобы было проще рефакторить.

    Да пожалуйста. У тебя было поле, которое сохраняет null, ты добавил проверку на не-null.

    Ты достаешь данные из базы и пытаешься сформировать класс и внезапно в конструктор передается null.

    На все вопросы я уже ответил, достаточно вдумчиво прочитать комменты. К сожалению нету времени писать более подробно.

    ОтветитьУдалить
  48. gandjustas,

    >historyEntries обязаны
    Кому обязаны?

    Если в этом вашем ШирнутомПоинте и нельзя добавлять элементы в коллецкцию, не подгрузив ее, то в NHibernate можно (почитайте тут: http://nhforge.org/doc/nh/en/index.html)

    ОтветитьУдалить
  49. @gandjustas

    Ладно, пора прервать этот бред)))

    > Вытягивание historyEntries из хранилища

    По вашим словам я могу сразу понять, что вы не понимаете DDD (потяно, что и ООП), не работали с NHibernate и уж точно не применяли CQRS.

    @gandjustas, лучше сидите на SharePoint, где можно писать как получится))) Ваши best practicies и наши best practicies это совсем разные вещи.

    Все кто прочитали комментарии от @gandjustas, будьте осторожны - это самые явные ошибки в понимании ООП и проектирования в целом.

    ОтветитьУдалить
  50. Я, кажется, понимаю, что @gandjustas хочет сказать. Пример №1 про класс Product. Получается, что в этом доменном типе хранится информация как о продукте, так и о ценовой политике. 2 обязанности.
    Хотел еще вчера про это сказать. Но оформить мысль помогли только сейчас :-)

    ОтветитьУдалить
  51. gandjustas,
    >Ты достаешь данные из базы и пытаешься сформировать класс и внезапно в конструктор передается null.

    Дегидрация использует конструктор по умолчанию - это раз. Два: откуда в базе возьмется null, если туда гарантированно писали NOT NULL, да еще и constraint стоит на колонке?

    Или вы имеете привычку в базу руками лазить?

    ОтветитьУдалить
  52. @gandjustas

    > Ты достаешь данные из базы и пытаешься сформировать класс и внезапно в конструктор передается null.

    Да-да, вы не работали с нормальной ORM, иначе бы знали, КАК объекты достаются из БД.

    ОтветитьУдалить
  53. Для Сергея пример из референса по 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!

    ОтветитьУдалить
  54. @Мурадов Мурад

    > Получается, что в этом доменном типе хранится информация как о продукте, так и о ценовой политике.

    Спасибо, что обратили внимание. Я подумаю про изменение кода, вы тоже предложите свой вариант.

    ОтветитьУдалить
  55. @Мурадов Мурад

    При решении надо исходить из предложенной User Story.

    ОтветитьУдалить
  56. @Maksim Galkin

    >Нетрудно заметить, что если программировать, как положено, от интерфейса

    Зачем нужны интерфейсы в доменной модели?

    >Стоит добавить, что если уж идти на все эти ухищрения, то и тип T нужно делать полностью immutable. Иначе пользователю интерфейса

    Вы понимаете различия между проектированием фреймворка и проектированием доменной модели?

    ОтветитьУдалить
  57. @gandjustas

    Найдите время прочитать эту книгу - откроете для себя много нового
    http://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215

    ОтветитьУдалить
  58. Maksim Galkin,

    Вы неправы. Прочитайте в чем отличие фреймворка от библиотеки, хотя бы тут: http://stackoverflow.com/questions/148747/what-is-the-difference-between-a-framework-and-a-library

    Что такое доменная модель - в книге, приведенной выше.

    Не надо кричать о том, в чем вы, очевидно, не понимаете.

    ОтветитьУдалить
  59. Александр Бындю комментирует...

    @Мурадов Мурад

    > Получается, что в этом доменном типе хранится информация как о продукте, так и о ценовой политике.

    Спасибо, что обратили внимание. Я подумаю про изменение кода, вы тоже предложите свой вариант.

    В нашем проекте есть разные типы резервов товара, наследуемые от IStorable. Соответственно техника резервирования товара для таких IEntity будет разная. Такую логику помещать в домен нельзя. То же касается расчета цен. Чаще всего, при простом варианте расчета можно нагрузить доменный объект, но в реальности, позже, этот код мы вынуждены выносить, т.к. расчет цен на разные типы товара может быть разный за счет других объектов типа "общая акция", которая не привязана к товару. В данном конкретном примере нельзя сказать точно кто прав, т.к. с одной стороны мы не видим всех частей проекта, с другой, если верить исключительно коду, то да, можно вынести расчет внутрь доменного объекта во избежание предварительного усложения. Ну и что, что нарушаем немного принципы SRP. Они - не самоцель, но о них надо помнить постоянно.

    hazzik, спасибо за комментарий, я его изучу. И, вообще, надо спать ночью, а не код писать в 2:47 :) Спасибо тебе )

    ОтветитьУдалить
  60. Хочу вставить свои 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.

    ОтветитьУдалить
  61. Попробую донести мысль gandjustas с другого ракурса.

    Возьмем ваше правильное решение класса Product.
    Вы ввели два метода - ChangePriceTo и SetCategory, чтобы добиться инкапсуляции.

    Теперь вопрос - что общего у этих методов, кроме того, чтобы они как-то работают с полями заказа?

    Вопрос дальше - если изменяется логика установки цены, какой класс вы будете менять?

    Еще опрос - если изменяется логика установки категории, какой класс вы будете изменять?

    И напоследок - если изменится набор информации, которую хранит Продукт, какой класс вы будете изменять?

    Итого - 3 причины для того, чтобы изменить класс, в то время как SRP требует одну, и как это было в ProductService, например.

    Исключить последнюю причину - невозможно, ибо это все приложение построено на том, что непосредственно ваша доменная модель сохраняется в базу.

    А дальше будет хуже - ваши объекты будут обрастать методами, между собой не связанными - то есть очень слабое сцепление( http://en.wikipedia.org/wiki/Cohesion_(computer_science) ).

    Отсюда вариантов исключить нарушение принципа SRP не так много - делать модель анемичной или между сущностями, что хранятся в базе и доменной моделью делать прослойку.

    ОтветитьУдалить
  62. @Oleg Karpov

    >Соответственно техника резервирования товара для таких IEntity будет разная. Такую логику помещать в домен нельзя. То же касается расчета цен.

    Как раз наоборот - это можно и нужно делать. Если вы этого не сделаете - получите классическую anemic domain model. Только естественно не нужно пихать эту логику в ваш IStorable - стоит выделить для этого отдельный агрегат, дочерними классами которого и будут ваши стратегии. Вообще весь DDD построен как раз на том, чтобы мыслить не в терминах таблиц и прочих хранилищ, а в терминах бизнеса. Think different

    ОтветитьУдалить
  63. А с каких пор логика расчета цена продукта, зависящая от состояния этого самого продукта, находящаяся соответственно в этом продукте, является нарушением SRP?

    Все перечисленные вами причины для изменения касаются изменения цены и только её, а значит и единственной ответственности, а значит SRP не нарушено.

    ОтветитьУдалить
  64. Я полностью поддерживаю идеи @Dmitry Kryuchkov и @hazzik. От себя тоже добавлю.

    @Vasiliy Shiryaev

    Дело в том, что User Story была обозначена. Предложенная реализация полностью ее реализует, открывая наружу только метод ChangePriceTo. Если бы была другая User Story как-то задевающая эту логику, то безусловно надо было бы выносить работу с скидками в другие объекты и т.д. и т.д.

    Я предлагаю не усложнять раньше времени, а приведенный пример лишь демонстрирует, что доменные объекты могут и должны иметь поведение.

    Если вам интересно развитие этого кода в более сложный проект, то предлагаю вам написать ряд User Story, которые мы все вместе реализуем и посмотрим на результат.

    ОтветитьУдалить
  65. @Александр Бындю
    Когда собеседник не согласен то надо непременно искать проблемы в собеседнике. Я предпочитаю так не делать.

    Я прекрасно понимаю что такое ООП, но самое главное что понимаю границы его применимости.

    Что касается SharePoint, то там как раз нельзя писать "как получится", на разных задачах один и тот же подход может дать противоположные результаты. Именно SharePoint учит продумывать заранее последствия тех или иных принятых решений. В отличие от рекомендаций вносить проверку на null внутрь класса с данными.

    ОтветитьУдалить
  66. @Vasiliy Shiryaev

    Я согласен, что в данном конкретном случае класс Product перегружен обязанностями. В реальном мире стратегий выбора категорий и определения цены может быть не 2 и не 5, а гораздо больше. Вариантов того, как такую ситуация успешно обработать 2 - либо новые агрегаты, либо новые value object. Что следует использовать в данном конкретном случае - неясно, ибо бизнес логики у тестового примера не много, а доменная модель строится исключительно по правилам бизнеса. В целом описанный в посте подход - абсолютно правильный, но это лишь первый шаг на пути к DDD

    ОтветитьУдалить
  67. >Дегидрация использует конструктор по умолчанию - это раз.
    То есть его может вызывать кто угодно? А в чем тогда смысл всего написанного выше про инкапсуляцию?

    >Два: откуда в базе возьмется null, если туда гарантированно писали NOT NULL, да еще и constraint стоит на колонке?
    Это если в начале проекта задали, то все хорошо. Но бывает так что сначала поле было nullable, а потом оказалось что там нужны данные при вводе. Сделать его notnull нельзя, но логику программы поправить нужно.

    ОтветитьУдалить
  68. @Dmitry Kryuchkov
    Я её читал, а вам советую почитать http://askofen.blogspot.com/
    Там реальные советы человека, которы много лет создавал программы, а не просто евангелизирует теории.

    ОтветитьУдалить
  69. > Да-да, вы не работали с нормальной ORM, иначе бы знали, КАК объекты достаются из БД.

    то есть для того чтобы показанный выше код был хоть сколько-нибудь эффективным надо использовать кошерный ORM? Так пишите об этом в предусловии своих постов.

    Вот Linq2SQL или EF - достаточно кошерные? А BLToolkit?

    ОтветитьУдалить
  70. @gandjustas

    Проблема в том, что вы не понимаете, чего вы не понимаете :)

    Как рассказать человеку, которые не катался на сноуборде, что значит кататься на сноуборде?

    Если вы не используете описанные методы (особенно интересно и подробно описано у Серия статей: Strengthening your domain), то это ваше право, доказать или обосновать вам, почему их использовать надо, тоже самое, что рассказывать как кататься на сноуборде. У нас разный опыт разработки и консультирования IT компаний.

    ОтветитьУдалить
  71. @Dmitry Kryuchkov

    Открою секрет: в реальном случае любой код будет гораздо сложнее, чем любой из примеров в этом блоге и всегда будет иметь смысл выносить его в отдельный сервис.

    И если а) из PL вызывать сервис, а не из экземпляра класса; б) в сервис передавать не экземпляр класса, а ключ(и) и параметры, то получится очень даже anemic.

    ОтветитьУдалить
  72. @gandjustas

    > Я её читал

    Прочитал, все слова понял, смысл не уловил?

    > а вам советую почитать http://askofen.blogspot.com

    Вы с ним чем-то схожи. Вы открываете новые слоистые архитектуры, он рисует масштабные UML диаграммы и таблицы.

    > Там реальные советы человека, которы много лет создавал программы, а не просто евангелизирует теории.

    Вышеописанное тоже не теория, всё это взято из практики. Просто это взято не из вашей практики.

    ОтветитьУдалить
  73. >gandjustas

    То есть его может вызывать кто угодно? А в чем тогда смысл всего написанного выше про инкапсуляцию?

    Сначала прочитайте референсы по NHibernate, приведенные выше, или, хотя бы, попробуйте потыкать какую-нибудь ORM, потом поговорим.

    Небольшой спойлер: конструктор по-умолчанию должен быть protected.

    >Это если в начале проекта задали, то все хорошо. Но бывает так что сначала поле было nullable, а потом оказалось что там нужны данные при вводе. Сделать его notnull нельзя, но логику программы поправить нужно.

    Так не бывает. Если поле стало not-null, то оно стало not-null везде, а не только в коде. Если вы в коде сделали предположение, что теперь поле not-null, а в базе-нет, то это - ваша проблема, но никак не DDD или вышеозначенного примера.

    ОтветитьУдалить
  74. @gandjustas

    > И если а) из PL вызывать сервис, а не из экземпляра класса; б) в сервис передавать не экземпляр класса, а ключ(и) и параметры...

    Проблема взаимопонимания с вами описана здесь

    ОтветитьУдалить
  75. > Проблема в том, что вы не понимаете, чего вы не понимаете :)
    Это в первую очередь к вам относится. Судя по всему вы просто не сталкивались с реальными проблемами, возможно потому что слишком рано попали на должность техдиректора. Я же успел много чего повидать и попробовать и до сих пор занимаюсь архитектурой и иногда кодированием.

    Например DDD пробовал еще 5 лет назад и на делфи, пришлось и свой ORM написать. И уже тогда понял недостатки DDD, которые никакими мощными инструментами не выправляются.


    > Как рассказать человеку, которые не катался на сноуборде, что значит кататься на сноуборде?

    Именно, поэтому попрошу воздержать от обсуждения SharePoint.

    ОтветитьУдалить
  76. gandjustas,

    >Там реальные советы человека, которы много лет создавал программы, а не просто евангелизирует теории.
    Че-то на вид он не старше меня;) Много это сколько? 1? 2? 5? 15?

    Я уже более 5 лет применяю принципы DDD и более 4х лет использую TDD в повседневной разработке. Увлекаюсь программированием более 16 лет (мне 26). Это много?
    Для меня - нет. Я всегда готов учиться и прислушиваться к мнению окружающих.

    Я часто пробую новые подходы и техники, наиболее удачные и полезные приживаются в нашей команде.

    ОтветитьУдалить
  77. @hazzik
    protected конструктор означает что я могу сделать наследника, который его вызовет ;)
    Вообще использование мощного ORM, вроде NHibernate - сильное предусловие. Сейчас повсеместно на высоконагруженных проектах используются легковесные ORMы.

    >Так не бывает. Если поле стало not-null, то оно стало not-null везде, а не только в коде.
    Еще как бывает. Данные в базе есть и нету способа их заполнить автоматически. Нельзя просто так в базе убрать флажок nullable.

    Это как раз реальное положение вещей, а не воображаемое как примерах.

    ОтветитьУдалить
  78. @hazzik ему за 30 кажись, только к делу это не относится.

    ОтветитьУдалить
  79. @gandjustas

    >В отличие от рекомендаций вносить проверку на null внутрь класса с данными.

    Абсолютно правильное решение, aggregate root отвечает за соблюдение всех инвариантов аггрегата в любой момент времени. Тот факт, что продукт должен иметь наименование - это и есть инвариант. По поводу был null, стал not null - миграционные скрипты уже отменили? Если да, то я как-то упустил этот момент. Более того, если вы в конструкторе написали эту саму проверку на null, то при загрузке даже немигрированных данные экспешена вы не получите - именно это и пытался объяснить вам @hazzik. Однако то, что исключений не возникнет вовсе не означает что стоит мириться с рассинхронизацией логики домена и структурой хранилища данных

    >Я её читал, а вам советую почитать http://askofen.blogspot.com/
    Я предпочитаю читать блоги людей, которые работают над большими и сложными проектами - Udi Dahan или Jimmy Bogard, например. Блоги людей, пишущих одни и те же калькуляторы "много лет" меня мало интересуют

    >Открою секрет: в реальном случае любой код будет гораздо сложнее, чем любой из примеров в этом блоге и всегда будет иметь смысл выносить его в отдельный сервис.

    Про сервис я ничего не говорил, он тут вообще не нужен. Если у вас есть несколько стратегий расчета цены - создайте по доменному объекту на каждую (aggregate или value object). Сервис тут абсолютно не нужен.

    ОтветитьУдалить
  80. @Gengzu
    SRP звучит не что "класс должен иметь одну ответственность" - ибо это определение какое-то ничего не определяющие, а что "класс должен иметь только одну причину для изменения". Я назвал 3 - и общее у них только то, что они связаны с продуктом.

    @Dmitry Kryuchkov, Александр Бындю
    Я согласен, что ситуации бывают разные, и что тестовый пример очень маленький, чтобы так абстрактно рассуждать.
    Меня просто смутило, что дается исходный код, который обозван неправильным, а предварительно текст о том, что большинство *Service - это нарушение принципа SRP. Потом дается решение, которые называется правильным. А по факту в нем нарушений не меньше, а с моей точки зрения и больше.
    При этом люди, которые будут читать блог, и учиться чему-то по его статьям, будут видеть слова "Анемичный", "Service", "Неправильно", "Правильно". И делать как "правильно", хотя в реальной ситуации это _чаще всего_ будет как раз неправильно.

    ОтветитьУдалить
  81. @gandjustas

    >Вообще использование мощного ORM, вроде NHibernate - сильное предусловие. Сейчас повсеместно на высоконагруженных проектах используются легковесные ORMы.

    Не путайте твиттер и enterprise приложения, у них совершенно разные цели и назначения. Более того, даже использую полновесную ORM можно создать высоконагруженное приложение - если построить масштабируемую архитектуру. И DDD тут как нельзя кстати

    ОтветитьУдалить
  82. Чтобы создать высоконагруженное приложение нужно тянуть из базу как можно меньше, именно она становится узким местом, которое тяжело масшатбировать. А тут очень плохо работают aggregate root и lazy load, которых так любят в DDD.

    ОтветитьУдалить
  83. Вы не правы - дело не в DDD, а в том как эти данные сохранять. Взгляните на event sourcing - key-value хранилища отлично масштабируются

    ОтветитьУдалить
  84. 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 (неважно).

    ОтветитьУдалить
  85. Только возникает проблема: как получить множество ключей для работы. Далеко не все задачи хорошо ложатся на выборку только по ключу. Поэтому как основу всегда используют РСУБД, а потом оптимизируют с помощью key-value store.

    Никто в здравом уме не использует его с самого начала, кроме самых простых случаев.

    ОтветитьУдалить
  86. Dmitry Kryuchkov, я об этом и говорил, что фактически расчет будет производиться необходимым классом стратегии, а не содержаться внутри доменного объекта в виде switch или if_else. Т.е. для изменения мы полезем не в Entity.

    ОтветитьУдалить
  87. > По поводу был null, стал not null - миграционные скрипты уже отменили?
    Их не всегда можно описать. У меня было пару случаев когда не было способа сделать из nullable колонки notnull. На уровне кода проблема решалась. Старый объект, будучи открытым для редактирования нельзя было сохранить не введя обязательное поле.

    > Про сервис я ничего не говорил, он тут вообще не нужен. Если у вас есть несколько стратегий расчета цены - создайте по доменному объекту на каждую (aggregate или value object). Сервис тут абсолютно не нужен.
    Стратегия в чистом виде и есть сервис ;) Зачем для них создавать еще aggregate или еще что-то непонятно. Это усложнение на ровном месте.

    ОтветитьУдалить
  88. Документные базы данных для таких выборок с шардингом по тем же самым ключам - опять же масштабируются без особых проблем

    ОтветитьУдалить
  89. > Сейчас повсеместно на высоконагруженных проектах используются легковесные ORMы.

    http://www.mindscapehq.com/blog/index.php/2011/12/05/5-reasons-not-to-use-a-micro-orm/

    ОтветитьУдалить
  90. @Vasiliy Shiryaev

    > Меня просто смутило, что дается исходный код, который обозван неправильным, а предварительно текст о том, что большинство *Service

    Возможно я не раскрыл смысл. Дело не в том, что в сервис выносить ничего не надо, а в том, что со временем сервис становится объектом с 20-30 подобными методами (также как классы типа Helper, Wrapper и т.п.) и в 99% случаев превращается в God-object

    ОтветитьУдалить
  91. >или еще что-то непонятно.
    Ну вот, а говорите книжку читали

    >Это усложнение на ровном месте.
    Это называется инкапсуляцией бизнес логики в доменной модели. В вашем решении она получается размытой между самой моделью и ненужным слоем сервисов - ну и, соответственно, инкапсуляции тут нет.

    ОтветитьУдалить
  92. @gandjustas

    > Стратегия в чистом виде и есть сервис ;)

    Смайлик в конце фразы прикрывает не знание предмета? Опять сюда. Эта фраза достойна вашей же цитаты.

    ОтветитьУдалить
  93. @hazzik
    Хорошая ссылка, но тем не менее практика говори что в высоконагруженных проектах нету DDD и нету тяжелых ORM.

    Кстати для некоторых вполне легковесных ORM очень даже работает Linq. Посмотри BLToolkit например.

    ОтветитьУдалить
  94. @Александр Бындю, ясно только одно. Пост получился спорным :)
    Но, все равно, спасибо :)

    ОтветитьУдалить
  95. @gandjustas

    Да-да, недавно проводил косультацию компании, где SQL запросы писалися прямо в обратчике OnClick в WinForm, там были тоже аргументы "в высоконагруженных проектах нету DDD" и "нету тяжелых ORM".

    Ваши высоконагруженные приложения на сколько высоконагружены? В чем была проблема использования нормальной ORM? В чем тяжеловесность NHibernate?

    ОтветитьУдалить
  96. @Oleg Karpov

    > Пост получился спорным

    А вы сами используете описанные в посте способы или оставляете анемичную доменную модель?

    ОтветитьУдалить
  97. @gandjustas

    >практика говори что в высоконагруженных проектах нету DDD

    Расскажите это @gregyoung и @abdullin - вот они удивятся :)

    ОтветитьУдалить
  98. @Александр Бындю
    Смайлик в конце ставлю когда очевидные вещи рассказываю.

    Сервис - класс с БЛ, без данных. Стратегия в чистом виде - вынесенное отдельное поведение из класса. То есть тоже класс без данных.

    Любому грамотному проектировщику должно быть это очевидно. А то получается что за громкими словами типа aggregate root люди перестают понимать собственно структуру классов и её влияние на программу

    ОтветитьУдалить
  99. > Расскажите это @gregyoung и @abdullin - вот они удивятся :)

    Чему? Они не делают высоконагруженных приложений. Более того сам Янг на одной из конференций сказал что DDD не подходи для сайтов.

    ОтветитьУдалить
  100. @gandjustas

    > Любому грамотному проектировщику должно быть это очевидно

    Вам снова сюда.

    Видимо вы считаете себя грамотным проектировщиком? Тогда как вы объясните изобретение вами слоистой архитектуры год назад?

    Цитата: "Никакие правки верхнего слоя никак не могут повлиять на нижний". Грамотные проектировщики в курсе таких вещей, а вы предпочитаете: "У меня давно зрел принцип разделения на слои, который я не мог выразить словами. Прочитав вот этот пост я нашел правильную формулировку". Ну ее нашли до вас лет 30 назад. Возможно когда-нибудь вы изобретете DDD и назовете это как-нибудь типа Ориентация на предметную область. Когда это произойдет, напишите пост, пусть все еще раз поржут над MVP от Microsoft.

    То, что я описал в этом посте, очевидно и не является открытием. Пост был для начинающих программистов, очень жаль, что он вызвал в вас столько противоречий.

    ОтветитьУдалить
  101. @Александр Бындю, вот код из реального проекта:
    история: у распоряжения есть определенная сумма, которую может оплатить сегодня финотдел. есть заявки от ЦФО, которые нужно сегодня оплатить. Объединять их можно по определенным правилам (опустим). Необходимо, чтобы в распоряжение невозможно было добавить заявку, если остаток по распоряжению стал =< 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);
    }
    }
    }

    ОтветитьУдалить
  102. @gandjustas

    > DDD не подходи для сайтов

    Наш 4х летний опыт это опровергает.

    ОтветитьУдалить
  103. отличное решение :)

    ОтветитьУдалить
  104. Извините, но, У ВАС НЕТ ВЫСОКОНАГРУЖЕННОГО ПРИЛОЖЕНИЯ.

    ОтветитьУдалить
  105. почему в #1 было добавлено два метода - ChangePriceTo и SetCategory, вместо вписывания кода в соответствующие сеттеры? Ок, продукт начал сам контролировать смену цены и категории - так зачем выставлять наружу хоть какие-то признаки этого контроля?

    ОтветитьУдалить
  106. Прочитайте про entity в какой-нибудь умной книжке по DDD, или хотя бы тут у Фаулера: http://martinfowler.com/bliki/... там ясно сказано - entity - это то, что идентифицируется идентификатором.


    И где там про то, что этот идентификатор обязательно одно поле?

    ОтветитьУдалить
  107. А у вас есть? Вот стандартный список: http://www.insight-it.ru/highload/. Найдитем там хоть один ORM тяжелее Linq To SQL. Половина - вообще RoR c Active Record.

    ОтветитьУдалить
  108. Мммм, причем здесь "вытягиваться из хранилища"? В данном случае мы говорили о конкретной реализации на основе List.

    ОтветитьУдалить
  109. Я не понимаю к чему ваш комментарий! 

    Я и говорю: прежде чем кричать, что ORM и DDD не используются в высоко-нагруженных приложениях, нужно ответить себе на вопрос - а у вас высоко-нагруженное приложение? И ответ, очевидно, - "НЕТ".

    Это все-равно, что утверждать, что посаны должны ссать сидя, потому что тётя Клава ссыт сидя. Но, извините, тётя Клава-то ЖЕНЩИНА!

    И на последок: DDD это вам не про чатик для миллионов хомячков, суть которого CRUD, это - про приложения для бизнеса, с огромным количеством логики.

    ОтветитьУдалить
  110. Я не говорил, что одно поле, я сказал, что идентификатор любого типа! Причем это может быть какой-нибудь HiLo ключ, суть которого value-object.

    Но мы же, с вами не извращенцы, делать такие ключи? Или всё-же да?

    ОтветитьУдалить
  111. Сверчков Евгений9 декабря 2011 г. в 20:31

    Я бы посоветовал переопределить методы работы с коллекцией.

    ОтветитьУдалить
  112. Каким образом? Что вы имеете ввиду?

    ОтветитьУдалить
  113. --- Равны могут быть не только Id, идентичность объекта могут представлять несколько полей
    --- Прочитайте про entity в какой-нибудь умной книжке по DDD, или хотя бы тут у Фаулера: http://martinfowler.com/bliki/... там ясно сказано - entity - это то, что идентифицируется идентификатором.--- Я не говорил, что одно поле, я сказал, что идентификатор любого типа

    я понял, что возражение было именно относительно того, что идентификатором может быть составной ключ.
    Про извращенцев - согласен, что простой ключ намного предпочтительнее, но в жизни всякое бывает.

    ОтветитьУдалить
  114. Не понимаете - уточните, а не пишите про писающую тетю Клаву.

    Мой коментарий к тому, что ORM и DDD - не серебряная пуля.

    Вы точно знаете какие приложения у меня или у gandjustas? Ответ, очевидно - НЕТ.
    Примеры в посте - тривильные. Никакой оговорки про вагон логики в посте нет. Скорее наоборот, там US, которые встречаются намного чаще хомячковых чатиках/магазинчиках.

    Если в вашем стиле: Извините, но НО В ПОСТЕ НЕТ ОГОВОРКИ ПРО ПРИЛОЖЕНИЯ ДЛЯ БИЗНЕСА. С ОГРОМНЫМ КОЛИЧЕСТВОМ ЛОГИКИ.

    Есть случаи, когда Anemic проще. Есть приложения, в которых приходится писать почти всю логику на T-SQL. Любое решение "делать A или B" зависит от контекста. Контекст никак в посте не задан. Вы придумали один контекст (куча бизнес логики и обязательно nhibernate), gandjustas - другой (легковесные сайты для хомячков с минимальной логикой). Он пытается до вас донести ситуацию, когда его решение лучше (а она как бы вполне очевидна). А вы - просто пропихиваете свой придуманный контекст в качестве основного доказательства. Такое может себе позволить джуниор, но никак не архитектор, который должен понимать, что it always depends.
     
    Лично у меня в текущем проекте - anemic, легковесный ORM, часть логики на базе (и да, составные ключи в некоторых таблицах) - выгоднее по производительности. Нагрузка не большая, просто данные специфические.

    Просил как-то у Александра совета "как переписать на DDD, чтобы не получить проблем с производительностью", долго флудили, решили "оставить anemic".

    ОтветитьУдалить
  115. >Он пытается до вас донести ситуацию, когда его решение лучше.
    Это все-равно, что прийти на форум болельщиков Спартака и начать им доказывать, что Зенит круче.

    >в высоконагруженных проектах нету DDD и нету тяжелых ORM.
    Это равносильно тому, что вы будете рассказывать мне какая Bugatti Veyron крутая, при этом видев ее только на картинке в статье в Википедии.

    Перечитайте внимательно мой предыдущий ответ и постарайтесь понять, что пост про DDD. И давайте мыслить в рамках применимости DDD. Если вам хорошо живется с anemic model - пожалуйста, это ваше право. Но пост про DDD.

    Всё. вопрос исчерпан.

    И да, мой контекст не придуман, а взят из 4 разных по своей природе больших проектов, каждый примерно по 10 человеко-лет.

    ОтветитьУдалить
  116. http://www.infoq.com/interviews/Architecture-Eric-Evans-Interviews-Greg-Young

    Если это не высоконагруженное приложение, тогда у нас с вами разные понятия о высоконагруженных приложениях

    ОтветитьУдалить
  117. Я внимательно перечитал пост Александра.
    Первая часть - про переход с anemic на ddd. 
    Подраздел "проблема", если его сократить, сводится к "у вас anemic и это очень плохо!". 

    Если пост был адресован не тем, кто сейчас сидит с анемиком, как gandjustas, то кому же тогда???

    В переводе на ваши абстракции - фанат Спартака написал фанатам Зенита письмо "Спартак круче". И удивляется, почему они не приняли его веру. А второй фанат (вы) стоите рядом и удивляетесь, чего вообще эти зенитовцы понабежали и кидают в вас кирпичами. Письмо ж было про спартак.

    4 проекта проекта - это хорошо, но это ваш контекст. Поверьте, я свой контекст тоже не с потолка взял.

    Сейчас в комментах видна очень неприятная картина - Александр, написал топик, явно адресованный "тем, кто до сих пор с анемиком". И вместо того, чтобы спокойно доносить свою веру до еретиков, переходит на личности, и обзывает их укуренными велосипедистами. А вы ему помогаете. Александр же консультант, пусть предложит бесплатную консультацию по архитектуре gandjustas-у, и выложит результаты открытый доступ. А то уже год кулаками в воздухе машут.

    ОтветитьУдалить
  118. Наиболее эпичным утверждением @b1084b6bff5d537d60595e93ceba9177 я считаю вот это:

    Данные != состояние объекта.

    В контексте:
    "Нет, ScannerData не является состоянием чего-либо. Он существует потому что существует запись в БД о сканере. Этот класс представляет данные о сканере в других частях программы. Это уже обязанность класса, остальные должны быть отделены.Данные != состояние объекта."хочется сказать ЛОЛШТО? Что же определяет состояние объекта, кроме его данных? 

    ОтветитьУдалить
  119. и ещё вопрос, чем по-твоему является объект "данные сканнера"? Он не является доменной сущьностью? Почему? К этому объекту неприменимо поведение? 

    Поинт в том, что он ничем не отличается от любого объекта, как например "диван". Ты зацепился за "Data" в названии. Можно точно так же сказать, что "диван" представляет из себя "диван" в других частях программы и это уже обязанность этого объекта... 

    Важно, что "данные сканера" _являются_ данными сканера, а не "представляют данные сканера". Являются со всем присущим им поведением. (каким известно только конкретному домену). Представлять данные сканера может кто-то другой. ScannerDataRepresentative например =)

    ОтветитьУдалить
  120. Станислав Выщепан10 декабря 2011 г. в 06:41

    Любая информационная система работает с некоторыми данными, которые имею время жизни больше любого объекта и самой ИС. Так вот именно эти данные наделять поведением - большая ошибка.

    Чтобы разбираться с вопросом считай что "данные" являются внешними для программы, а состояние внутренним. Если посмотреть GoF то там есть паттерн Memento, который как раз превращает состояние в данные.

    ОтветитьУдалить
  121. Станислав Выщепан10 декабря 2011 г. в 06:49

    Пустая болтология. Ты же SOLID знаешь? Так вот буква S означает SRP, который говорит о том что у любого класса должна быть только одна обязанность, а остальные можно и нужно отделить.

    Так вот ScannerData имеет одну четко обозначенную обязанность - переносить данные сканера от поставщика этих данных потребителю. И какие бы еще не придумал обязанности их стоит отделить в соответствии с SRP.

    Если же ты придумываешь ScannerDataRepresentative , то для начала попробуй описать его обязанности словами и в виде некоторого интерфейса. У тебя получится тот же самый ScannerData.

    Куча акронимов и  базвордов настолько съели мозги неокрепшим программистам, что они уже не используют даже самое базовые принципы проектирования, а только видят "доменные объекты", "агрегат руты" и прочую лабуду.

    ОтветитьУдалить
  122. Глупый ответ. Ты же понимаешь русский язык? Умеешь отвечать по сути, а не уходить в сторону?

    ScannerData - не имеет чётко обозначенной обязанности переносить данные сканера. По той причине, что требования к этому доменному объекту нигде не описаны и домен не указан. Единственным свидетельством того, что это "доменный объект", а не просто "data transfer object" служит текст поста, следующий за объявлением этого типа. 

    Приведу пример домена, где эта сущьность может иметь поведение. Домен: "Разработка ПО для сканера", задача: "Реализация постобработчика данных пршедших со сканера" В таких терминах вполне уместен метод scannerData.DoSomethingWithScannedImage(); 

    Могут быть и другие домены и сущьность отыгрывать там разные роли, то что для этой сущьности чётко обозначена роль, ты придумал.

    ScannerDataRepresentative:
    Описываю его обязанности "предоставлять отсканированные данные". и теперь в виде интерфейса (всего лишь один из бесконечных вариантов в бесконечном варианте доменов)
    IScannerDataRepresentative {
    ScannerData  GetScannerData();
    }

    >Куча акронимов и  базвордов настолько съели мозги неокрепшим программистам, что они уже не используют даже самое базовые принципы проектирования, а только видят "доменные объекты", "агрегат руты" и прочую лабуду.

    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    Предлагаю быть предельно конкретными. Я готов поспорить на счёт того, кто из нас "окрепший" программист, но только не в стиле таких глупых фраз. А то некоторые глупые троли, настолько глубоко возносят своё ЧСВ и неадекватно смотрят на мир, что очень смешно становится.

    ОтветитьУдалить
  123. Вопросы был в том, что определяет состояние объекта, если не его данные? Состояние объекта это данные и есть. То что ты написал в этом сообщении это прееход в другой контекст, я не вижу в этом смысла. 

    Состояние объекта целиком определяется данными, которые этот объект хранит. И данные ScannerData, вопреки твоему утверждению, являются состоянием объекта ScannerData. Как ни крути.

    То что фактические данные, которые хранятся в полях этого объекта, являются "некоторыми данными информационной системы, которые имеют время жизни боьше любого объекта и самой ИС" никакого отношения к этому не имеет.

    ОтветитьУдалить
  124. Кстати, можно ли считать последнее придложение, твоей официалной позицией. Т.е. считать, что "доменные объекты" и "агрегат руты" ты считаешь лабудой?

    ОтветитьУдалить
  125. Станислав Выщепан10 декабря 2011 г. в 07:25

    Еще раз: во избежание путаницы давай словом "данные" называть то что находится вне объекта\программы, а "состоянием" то что находится внутри него.

    Еще раз повтори свою мысль с учетом того что я написал.

    ОтветитьУдалить
  126. Станислав Выщепан10 декабря 2011 г. в 07:27

    Да, причем такой, которая подменила многие best practicies проектирования, в том числе ОО-проектирования.

    ОтветитьУдалить
  127. Станислав Выщепан10 декабря 2011 г. в 07:33

    А ты попробуй для начала объяснить свою мысль без применения терминологии DDD. Ты увидишь что нету разницы между value-object и domain object в данном случае. 

    Ты попадаешь в типичную логическую ошибку. Ты берешь некоторый "четрырехколесный агрегат с двигателем внетреннего сгорания" и говоришь что это "гоночный автомобиль", но выясняется что у него колеса разные, двигатель слабый и салон ужасен, а на проверку оказывается что это "трактор". Но слова "трактор" нету в твоем лексиконе. Ты сам ограничил свой словарный словарный запас.

    ОтветитьУдалить
  128. Станислав Выщепан10 декабря 2011 г. в 07:34

    Ну а теперь я начну глумиться.

    > В таких терминах вполне уместен метод scannerData.DoSomethingWithScannedImage();

    Вот ты искренне считаешь что для добавления обработки scannerData надо добавлять метод в сам scannerdata? Ты представляешь сколько способов обработки данных сканера в типичной программе?

    ОтветитьУдалить
  129. Станислав Выщепан10 декабря 2011 г. в 07:36

    > ScannerDataRepresentative:
    > Описываю его обязанности "предоставлять отсканированные данные"
    И ты описал провайдер данных. Теперь попробуй потребителя еще опиши. Получится некоторая "стратегия".

    Вот у тебя и выйдет нормальный anemic дизайн без лишней связности

    ОтветитьУдалить
  130. 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.

    ОтветитьУдалить
  131. Не вижу смысла пытаться объяснить мою мысль без терминологии ДДД. Если ты можешь и можешь в этом объяснении показать отсутствие разницы - welcome.

    Я не думаю, что я понимаю к чему ты написал это про логическую ошибку. Также я не склонен считать, что имея нечто общее (автомобиль), я буду считать, что он удовлетворяет любому частном (гоночному автомобилю). Без должных на то оснований. Тем более, если автомобиль, автомобилем не является, а является "нет в моём лексиконе"

    ОтветитьУдалить
  132. Я пожалуй поддержу твоё глумление. 

    Да я искренне считаю, что если в домене так это и происходит и отражает реальность - то да, нужно. Но поскольку мы обсуждаем несуществующие вещи, а это был пример, которые ты просил и домен был выдуман, как и задача, то я понятия не имею сколько способов и каких и чего обработки. И уж тем более, что такое "типичная программа"

    ОтветитьУдалить
  133. При чём тут стратегия? Мы говорим о паттерне "стратегия" или о какой-то другой стратегии?

    Анемик дизайн у меня никуда не выйдет, ScannerDataRepresentative  это не анемик дизайн, это вообще ничего вместе с его потребителями. Это просто объект с одной явной ролью - предоставлять данные сканера.

    ОтветитьУдалить
  134. Подменила? Что это значит? Подменила в твоей жизни, в твоих проектах?

    ОтветитьУдалить
  135. Не согласен, я не вижу никакой путаницы. Ну и мало того, я не вижу вообще никакого смысла, повторять свою мысль в тобой поставленных условиях. если хочешь её оспорить, я готов обсудить.

    Воде бы прописные истины, что объект состоит из данных, описывающих его состояние и операций (методов) описывающих его поведение и identity определяющей уникальность.

    ОтветитьУдалить
  136. Станислав Выщепан10 декабря 2011 г. в 17:06

    Именно. А вот  ScannerData у тебя содержит две две обязанности: переносить данные и обрабатывать их. Раздели эти обязанности и это будет anemic дизайн.

    ОтветитьУдалить
  137. Станислав Выщепан10 декабря 2011 г. в 17:07

    То что "данные сканера" имеют какое-то поведение ты считаешь отражением реальности? Прости, но у тебя какая-то другая реальность.

    ОтветитьУдалить
  138. Станислав Выщепан10 декабря 2011 г. в 17:10

    То есть твоя мысль имеет смысл только в терминологии DDD, хотя DDD не является даже полным подмножеством методик проектирования. В том смысле что нельзя по DDD сделать все. 

    Насчет логической ошибки: ты назвал scannerdata объектом домена, на каких основаниях? Наверное на тех что эванс сказал любые данные называть доменом и применять к ним некоторые правила.

    ОтветитьУдалить
  139. Станислав Выщепан10 декабря 2011 г. в 17:13

    Да, это ты назвал форму, но не суть. Суть заключается в назначении объектов. Я рассматриваю назначение, а ты его игнорируешь, рассматривая форму. При этом сам создаешь ложные утверждения, которые потом птыаешь опровергнуть.

    ОтветитьУдалить
  140. Станислав Выщепан10 декабря 2011 г. в 17:14

    В моей - слава богу нет. Но вот этот пост показывает что такое часто случается у других.

    А значит то что люди за формой персетают видеть суть.

    ОтветитьУдалить
  141. А, простите, зачем это делать?

    ОтветитьУдалить
  142. Станислав Выщепан10 декабря 2011 г. в 19:18

    А SRP просто так придуман? Наверное были практические соображения. Если почитать Мартина то он недвусмысленно пишет о том нарушение SRP ведет к тому что увеличивается площадь изменений. Небольшие изменения требований приведут к большим изменениям в коде.

    ОтветитьУдалить
  143. Вы неверно толкуете 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, 

    ОтветитьУдалить
  144. Станислав Выщепан10 декабря 2011 г. в 20:02

    Еще один. Состояние само по себе не нужно, оно нужно для реализации поведения. Вот есть в объекте состояние для хранения каких-то данных сканера. Для какого поведения оно нужно? Это поведение описывается фразой "перенос данных". 

    SRP как и любой другой принцип надо рассматривать только в контексте поведения объекта. Если ты создаешь объект и не можешь ничего сказать о его поведении, то ты зря его создаешь.

    Мое толкование SRP совпадает с тем что написал Мартин. И ведет к более стройному дизайну.

    ОтветитьУдалить
  145. Перенос данных - это не поведение. Поведение - это явно написанная логика (читайте - код). Тот факт что у объекта есть 3 property - поведения ему нисколько не добавляет

    ОтветитьУдалить
  146. Станислав Выщепан10 декабря 2011 г. в 20:16

    Счего бы? Поведение - то чем пользуются другие объекты. Если объект отдает данные,то это уже поведение. Даже еслиэто 3 свойства.

    ОтветитьУдалить
  147. 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

    ОтветитьУдалить
  148. отличное представление;)

    ОтветитьУдалить
  149. Предлагаю свою версию решения примера №1 http://muradovm.blogspot.com/2011/12/blog-post.html

    ОтветитьУдалить
  150. Вы добавили важное условие "Должна быть возможность оперативно менять ценовую политику.", поэтому решение получилось гораздо более громоздким.  

    Усложнять его можно сколько угодно :) Спасибо за пример.

    ОтветитьУдалить
  151. Это не будет анемик дизайн. Анемик дизайн - это когда поведение, которое принадлежит объекту, выносится в сторонние сервисы. В моём примере такого нет.

    Кто говорил об обработке данных, откуда ты их взял? 

    Предлагаю читать определения терминов, прежде чем оперировать ими. Тролерская манера общения мне тоже твоя нравится, постоянно игнорировать вопросы, отвечать не о сути, переводить контекст, подменять понятия. Классика =)

    ОтветитьУдалить
  152. Да, я сознательно его добавил. Чтобы проще было жить, когда когда клиент позвонит и скажет: "Слушай, у нас тут акция на кой-какой товар, нам надо подправить эту твою программу".
    У меня в практике было и похлеще, когда клиент просил изменить "кое-что" в программе, что совершенно не только не соответствовало первоначальному ТЗ, но и никогда не предполагалось.
    У тебя в блоге указано, что ты практикуешь Agile, а гибкий дизайн называешь "усложнением".

    ОтветитьУдалить
  153. олололо, состояние само по себе не нужно? не ты ли с саблей на голо кричишь, что хранить данные (определяющие состояние) это отвесттвенность этого объекта и поведения в нём быть не должно.

    что является поведением данного объекта? хранение данных? это "роль в твоих терминах", что тогда поведение? нет поведения? но у него же естть состояние, которое нужно только для поведения, которого нет, вот незадача да?

    Как всегда ожидаю ответ не по сути.

    ОтветитьУдалить
  154. Эпично, наличие свойств у объекта это уже поведение. Эпично. Т.е. определять любое поведение у объекта, у которого уже есть публичные свойства - нарушение SRP?

    ОтветитьУдалить
  155. "У тебя в блоге указано, что ты практикуешь Agile, а гибкий дизайн называешь "усложнением".

    Дело в том, что самому себе не надо усложнять условия. Когда они появляются в виде измененных требований, надо реагировать, но загадывать наперед, что это потребуется - лишнее. Гибкость в Agile - это умение быстро перестраиваться под изменение окружающей среды, а не загадывание наперед и точно не усложнение. Вот тут подробно описано - принцип KISS

    ОтветитьУдалить
  156. За какой формой? О каких best practicies проектирования идёт речь? Можешь сформулировать их? DDD это ни что иное как набор практик проектирования + особый подход к организации работы над проектом.

    ОтветитьУдалить
  157. Мне не нравятся безосновательные утверждения. Покажи ка мне моё ложное утверждение и попытку опровергнуть, чтобы не быть голословным.

    И что ты называешь формой? В чём суть? Ты рассматриваешь назначение? Назначение объекта в неизвестной предметной области - как я понимаю. Т.е. неизвестное назначение объекта. Зачем ты его рассматриваешь?

    ОтветитьУдалить
  158. Эта реальность никому неизвестно, потому что, ещё раз, домен нигде не описан и не указан, а значит объект может быть любым. Мне конечно странно, что такой одарённый человек, не способен, понятть этого. А также не способен  придумать домен, в котором у этого объекта может быть поведение.

    Что скажешь, если у нас домен это детский мультфильм, которые наглядно показывает работу сканнера, где данные сканера умеют разговаривать на русском языке с другим персонажем мульника "контроллером сканера" и т.п.

    В таком домене у этого объекта тоже не будет поведения? Напрмер метод "говорть" scannerData.Tell("something"); в твоём анемичном мире ты бы этот метод вынес в серис? В какой? ScannerDataManager? Если да, то зачем?

    Ну и ещё раз вывод: ты сам себе придумал домен, основываясь на имени типа "ScannerData". В придуманном тобой домене у этого объекта нет поведения (да его нет, не смотря на наличие свойств =)), есть только состояние. Но это ты придумал домен, ты начал рассужать в его терминах. Автор поста, просто сказал, что это доменный объект и у него есть поведение. Кто мы такие, чтобы спорить, не зная о каком домене идёт речь?

    ОтветитьУдалить
  159. Соглашусь с http://blog.byndyu.ru/2011/12/domain-driven-design.html#comment-383541636 

    Представь себе ситуацию, когда ты придумал себе это "усложнение", потратил на него время, а твой заказчик развернул проект в другую сторону и вся фича, вместе с усложнением, стала не нужной, Или еще хлеще: это "усложнение" стало, попросту, мешать.

    ОтветитьУдалить
  160. Станислав Выщепан,

    Вам нужно прочитать Гради Буч "Объектно-ориентированный анализ и проектирование с примерами приложений на С++". Вы не видите границ между разными методами проектирования ПО, а это базис. До прочтения книги я попрошу вас не писать комментарии, а после с удовольствием обсудим разные нюансы предложенной в статье реализации.

    ОтветитьУдалить
  161. Ну тогда как минимум ты "усложнил" свой же пример
    private const int SomeMinPriceFromDomainLogic = 100;

    Ну и также нет абсолютно никакой уверенности, что размещение логики
    if (Price < SomeMinPriceFromDomainLogic)            SetCategory(CategoryType.Regular);
    и
    if (Category == CategoryType.Regular)            Discount = 0;
    в типе Product вообще соответствует DSL.
    Можно еще раз повторить, что по US "ровно 100 рублей порог и устанавливать только Regular категорию. Мамой клянусь, больше ничего не надо"

    ОтветитьУдалить
  162. Ну тут нет места для обид, правда. Пример должен был показать, что логику можно писать в самом доменном объекте, если она к нему относится, в противоположность анемичной моделе данных.

    Ни мой, ни твой пример не может быть полным и правильным, т.к. у нас есть только одна User Story и мы не видим куда двигается проект, нет контекста. С другой стороны и твой и мой пример показывают, что доменные объекты должны содержать в себе логику, это и была цель статьи.

    ОтветитьУдалить
  163. Станислав Выщепан11 декабря 2011 г. в 01:43

    Я её читал и мейера читал. Только вот много ошибочного в этих книгах. Один пример с датчиками является антипаттерном ОО-проектирования. Я как раз подобными задачами с датчиками занимался на момент прочтения книги и создавать по классу на датчик было самым плохим решением.

    ОтветитьУдалить
  164. Станислав Выщепан11 декабря 2011 г. в 01:50

    А зачем столько сложностей? Почему не вызывать в политику на событие записи свойств товара в базу?

    ОтветитьУдалить
  165. Станислав Выщепан11 декабря 2011 г. в 01:53

    Вообще попрошу впредь не отсылать к разным апологетам. К вашему несчастью я их все читал, но и много других книг прочитал. Поэтому если есть объективные аргументы - пишите, если нет, то мнение буча, мейера, фаулера, еванса и прочих не являются объективными. Кстати то что они все пишут зачастую верно только на искусственных примерах в книгах, но не в реальных приложениях, и то не всегда. И этот блог страдает тем же самым.

    ОтветитьУдалить
  166. Тогда вам лучше написать свою книгу, потому что ваше понимание ООП отличается от общепризнанных.

    ОтветитьУдалить
  167. Станислав Выщепан11 декабря 2011 г. в 02:33

    Нет, понимание ООП совпадает. А вот понимание что и когда применять скорее всего не совпадает с бучем, фаулером и эвансом.

    ОтветитьУдалить
  168. Станислав Выщепан11 декабря 2011 г. в 03:19

    Еще раз: поведение первично. Если есть свойства - уже есть поведение. Если нету поведения, то есть не можете его определить, то скорее всего зря создавали класс.

    Добавлять к классу другое поведение, помимо существующего - нарушение SRP.

    ОтветитьУдалить
  169. Станислав Выщепан11 декабря 2011 г. в 03:59

    Я бы опирался на определение Кея, который говори что ООП - это объекты с Identity, обменивающиеся сообщениями друг с другом.

    ОтветитьУдалить
  170. Станислав, в этих книгах выработан общий для программистов язык, с помощью которого можно понимать друг друга. Если вы понимаете по-своему, то только описав свой словарь вы сможете донести мысль. Я вас не понимаю (это не стеб и не шутка), когда вы оперируете понятиями поведение, состояние и т.п., они у вас просто другое значат.

    Мое понимание совпадает со статьей OOP и книгой Гради Буч "Объектно-ориентированный анализ и проектирование с примерами приложений на С++". Скажите, где посмотреть ваше?

    ОтветитьУдалить
  171. Добавил, стало лучше?

    ОтветитьУдалить
  172. Станислав Выщепан11 декабря 2011 г. в 15:49

    ООП - создание программы в виде объектов, которые обмениваются сообщениями. В большинстве языков обмен сообщениями реализован через вызов методов. Все объекты обладают 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.

    Кстати Буч тоже ссылается на системный подход, но почему-то в примерах проектирование начинает снизу вверх. Это приводит к тому что программа становится неустойчивой к изменениям. Например добавление датчика приведет к переписыванию немалой части кода.

    ОтветитьУдалить
  173. >> решение с фабричным методом CreateСкорее с методом создания (Creation Method). Фабричным можно назвать метод, который переопределяется в унаследованных слассах.

    ОтветитьУдалить
  174. Да причем тут обиды.
    Если сделать поправку на то, что DSL этого примера (опять же из US) категорически не соответствует представлениям DSL продаж и изменений цены конкретно моим. Ну или может быть для вашего клиента этот проект настолько краткосрочен, что даже инфляция не скажется ценовом пороге. Да, твое решение вполне себе жизнеспособно.
    Одно можно сказать с уверенностью. Если пост собрал свыше 200 коментариев бурных обсуждений, то пост-провокация удался на славу   ;-)

    ОтветитьУдалить
  175. Все зависит от того в какой бизнес-транзакции будут использоваться объекты класса Product.Ну и наверное не очень хорошо, когда момент времени выполнения какой-то логики должен быть связан с записью в базу. Скорее должно быть наоборот.

    ОтветитьУдалить
  176. Станислав Выщепан12 декабря 2011 г. в 04:58

    Переформулирую. Применение политики необходимо только при попытке изменить товар пользователем. Почему бы не вызывать её только в этот момент?

    ОтветитьУдалить
  177. Мой ответ переформулировать не надо. Пусть останется таким же.

    ОтветитьУдалить
  178. > что даже инфляция не скажется ценовом пороге

    Дело в том, что как только ценовая политика поменяется, надо будет сделать рефакторинг и сделать ценовую настраиваемой. Пока она не меняется, для этого не надо делать механизмов.

    > Если пост собрал свыше 200 коментариев бурных обсуждений, то пост-провокация удался на славу
    Это не пост-провокация, а набор простых приемов для ухода от анемичной доменной модели. Я искренне удивлен, что он вызвал столько обсуждений.

    ОтветитьУдалить
  179. Станислав Выщепан12 декабря 2011 г. в 17:57

    То есть вы считаете хорошей идеей не привязывать код к некоторому сценарию исполнения, а выполнять его всегда просто потому что так можно делать? А если завтра станет такой сценарий неприемлем? Например возникнет задача сохранения черновиков, для которых не надо применять политику.

    ОтветитьУдалить
  180. Завтра могут появиться абсолютно любые задачи. В том числе и такие, которые потребуют перепроектировать этот дизайн.
    Но будет еще страшнее, если какой-то компонент установит новую цену у товара и попытается воспользоваться например категорией товара.
    К сожалению, ваше предложение приведет только к тому, что будет возможно установить для товара некорректное состояние.

    ОтветитьУдалить
  181. Станислав Выщепан12 декабря 2011 г. в 18:27

    Да, именно поэтому надо делать как можно меньше. Меньше писать кода, меньше исполнять логики. Вы ведь понимаете что проще добавить вызов, чем убрать его.

    Насчет того что что-то установит цену товара, а потом воспользуется категорией - неправдоподобный сценарий при любом раскладе. Некорректное состояние всегда можно установить если просто залезть в базу. Программа кстати должна вести себя адекватно в этом случае.

    ОтветитьУдалить
  182. >То есть твоя мысль имеет...
    Поразительный вывод, потруднись объяснить как ты его сделал? Я подозреваю, что формальная логика не замешана.

    >Насчет логической ошибки: ты назвал scannerdata объектом домена, на каких основаниях? Наверное на тех >что эванс сказал любые данные называть доменом и применять к ним некоторые правила.

    На тех основаниях, что так написано в посте, который мы комментируем. А Эванс подобной чепухи не говорил.

    ОтветитьУдалить
  183. Разберём по пунктам.

    Если есть свойства - уже есть поведение.
    Добавлять к классу другое поведение - нарушение SRP.

    Вывод, добавлять к классу у которого есть свойства, любое поведение - нарушение SRP.

    Ты согласен с этим? Должен быть, выводы из твоих слов. Да?

    ОтветитьУдалить
  184. Станислав Выщепан13 декабря 2011 г. в 03:17

    О применимости DDD http://gandjustas.blogspot.com/2011/12/ddd.html

    ОтветитьУдалить
  185. >> и внезапно в базе оказались пустые 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"

    ОтветитьУдалить
  186. Обычно изменение любого поля объекта доменной модели должно сопровождаться записью в репозиторий. А по сему скажите, кто ответственнен за эту операцию - сам метод ChangePriceTo или тот, кто его вызывает? Вопрос не холивара ради, а действительно хочется услышать мнения на этот счёт, ибо подозреваю, что большинство хелпер-классов появляются именно по этой причине...

    ОтветитьУдалить
  187. Желательно чтобы и сам метод и объект Entity ничего не знали ни про репозитории ни про UnitOfWork. За сохранение и коммит в репозитории отвечает вызывающая сторона  - метод сервиса или метод команды.

    ОтветитьУдалить
  188. Это понятно, что не сам объект пишет в базу (если только вы не использует 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

    Хотелось бы услышать ваше мнение на решение подобной проблемы - без использования команд.

    ОтветитьУдалить

  189. кто должен сделать вызов unitOfWork.MarkDirty(product)

    Давайте конкретно про NHibernate. Когда вы берете объект из Session, то эта сессия следит за всеми изменениями объекта самостоятельно.

    Примерный код:
    1) Взяли объект из БД
    Product product = session.Get(1);

    2) Где-то в коде поменяли поле объекта или изменили связанную коллекцию
    product.Name = "new name";
    product.Items.Clear();

    3) Где-то в коде мы коммитим изменения в сессии
    session.Commit();

    Сессия сама сохранила "что было" и "что стало" и сгенерировала нужный SQL скрипт.

    ОтветитьУдалить
  190. А что делать в случае если Product порождает новый объект для которого он не является AR(каскады не работают)? Да, модель возможно кривая, но будем считать что это легаси код.

    ОтветитьУдалить
  191. Приведите, пожалуйста, пример кода. Можно писать псевдо-кодом, лишь бы было понятно.

    ОтветитьУдалить
  192. Например, что-то вроде.

    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-ы.

    ОтветитьУдалить
  193. Здесь, в идеале, historyItem должен добавляться в коллекцию HistoryItems, которая принадлежит Customer и замаплена в БД.
    Т.е. внутри метода AddLoan будет:

    historyItems.Add(historyItem);

    Тогда UoW сам добавить новый элемент в коллекцию.

    > порождает новый объект для которого он не является AR(каскады не работают)
    В этом случае, конечно, надо отрефакторить. Если уж совсем никак код нельзя менять, то можно и событие пробросить. Не совсем понятна реалиация Something.Persist(historyItem), имелись ввиду вот такие события http://www.udidahan.com/2009/06/14/domain-events-salvation/ ?

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...