DDD (Domain Driven Design), как обрабатывать изменения состояния объекта и инкапсулировать бизнес-правила, требующие обработки большого количества данных

public class Person
{
    public IList<String> SpecialBirthPlaces;
    public static readonly DateTime ImportantDate;
    public String BirthPlace {get;set;}

    public DateTime BirthDate
    {
        set
        {
            if (BirthPlace!=null && 
                value < ImportantDate && 
                SpecialBirthPlaces.Contains(BirthPlace))
            {
                BirthPlace = DataBase.GetBirthPlaceFor(BirthPlace, value);
            }
        }
    }
}

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

Однако у меня есть 2 проблемы, реализующих его:

  • Это правило изменяет состояние (свойство) сущности домена, и мне нужно отразить это изменение в пользовательском интерфейсе. Моя модель домена - POCO. Я мог бы поместить эту логику в ViewModel, но это неправильно, потому что это не логика UI. Это важное правило домена, которое мне нужно захватить.

  • Мой список SpecialBirthPlaces довольно большой, и я не хочу заполнять его каждый раз, когда я получаю клиента из базы данных. Кроме того, мне нужно получить замену для места рождения, когда правило будет выполнено. Как я уже сказал, список специальных мест рождения и замены для этого очень велик и хранится в БД.

Как реализовать логику, которая мне нужна в стиле DDD?

Ответ 1

Способ, которым я инкапсулировал эту проблему, которая является отслеживанием изменений, относится к шаблону Unit of Work. У меня есть свои DDD-репозитории, связанные с единицей работы, и я могу запросить единицу работы для любого набора объектов, которые я получаю из любого из репозиториев, чтобы увидеть, какие изменения были изменены.

Что касается большой коллекции, она выглядит как только для чтения. Один из способов справиться с этим - предварительно загрузить и кешировать его локально, если он когда-либо будет доступен, тогда репозитории могут запускать запросы против версии в памяти. Я использую NHibernate, и с этим легко справиться. Если он слишком велик для хранения в ОЗУ (например, 100 Мбайт или более), вам, вероятно, понадобятся специальные репозитории для него, поэтому запрос SpecialBirthPlaces.Contains(BirthPlace) выполняется в базе данных (возможно, в хранимой процедуре, га!). Вероятно, вы захотите выразить SpecialBirthPlaces как репозиторий сущностей, а не просто большую коллекцию строк, что позволит шаблону "Запрос" освободить вас от необходимости загружать всю вещь.

После этого длинного повествования, вот пример:

public class BirthPlace
{
    public String Name { get; set; }
} 

public class SpecialBirthPlace : BirthPlace
{
}

public class Person 
{
    public static readonly DateTime ImportantDate;
    public BirthPlace BirthPlace { get; set; } 

    public DateTime BirthDate 
    { 
        get; private set;
    } 

    public void CorrectBirthDate(IRepository<SpecialBirthPlace> specialBirthPlaces, DateTime date)
    {
        if (BirthPlace != null && date < ImportantDate && specialBirthPlaces.Contains(BirthPlace)) 
        { 
            BirthPlace = specialBirthPlaces.GetForDate(date); 
        }
    }
} 

Наличие метода, в котором вы проходите исправленную дату рождения, является лучшим дизайном, поскольку он сообщает вам параметры, необходимые для фактической корректировки даты рождения: репозиторий (например, сбор) объектов SpecialBirthPlace и правильная дата. Этот явный договор дает понять, что делает домен, и делает бизнес понятным, просто читая контракты с сущностями, где его скрывает вся коллекция в состоянии объекта.

Теперь, когда мы сделали BirthPlace в сущности, мы можем видеть, что может быть еще одна оптимизация, чтобы сделать модель домена немного более плоской. Нам не нужно специализироваться на BirthPlace, но нам нужно указать, является ли оно особенным. Мы можем добавить свойство к объекту (некоторые люди жалуются на свойства объектов домена, но я этого не делаю, поскольку он упрощает запросы, особенно с LINQ), чтобы указать, является ли он особенным. Тогда мы можем полностью избавиться от запроса Contains:

public class BirthPlace
{
    public BirthPlace(String name, Boolean isSpecial = false)
    {
        Name = name;
        IsSpecial = isSpecial
    } 

    public String Name { get; private set; }
    public Boolean IsSpecial { get; private set; }
}

public class Person 
{
    public static readonly DateTime ImportantDate;
    public BirthPlace BirthPlace { get; set; } 

    public DateTime BirthDate 
    { 
        get; private set;
    } 

    public void CorrectBirthDate(IRepository<BirthPlace> birthPlaces, DateTime date)
    {
        if (BirthPlace != null && date < ImportantDate && BirthPlace.IsSpecial) 
        { 
            BirthPlace = birthPlaces.GetForDate(date); 
        }
    }
} 

Ответ 2

Я думаю, что утверждения "Мне нужно отразить это изменение в пользовательском интерфейсе" и "Это важное правило домена, которое мне нужно захватить", описывают две разные проблемы. Понятно, что первое нужно решить; не ясно, что второй делает.

Если другие части вашей модели домена должны знать об изменениях здесь, вам следовало бы взглянуть на события домена (например, реализация Udi Dahan). Вы также можете использовать это, чтобы установить свойство BirthPlace, когда BirthDate устанавливается, даже асинхронно, если это потенциально длительная операция.

В противном случае давайте просто рассмотрим проблему пользовательского интерфейса. Прежде всего, в моей модели домена я хотел бы, чтобы каждый объект был абстрагирован как интерфейс. Если вы этого не сделаете, вам может понадобиться хотя бы сделать некоторые свойства virtual. Я бы также использовал слой абстракции для генерации/возврата моих объектов, таких как IoC/factory/repository. Я считаю, что этот слой выходит за пределы самой модели домена.

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

Что делать, если мы украшаем рассматриваемую сущность реализацией, реализующей INotifyPropertyChanged? Мы могли бы сделать это в нашем репозитории, который, как мы установили, находится за пределами домена, поэтому мы не будем модифицировать сама модель домена, только используя композицию для обертывания сущностей с функциональными возможностями, которые требуется системе вне модели домена. Для повторного пересчета пересчет BirthPlace остается проблемой модели домена, в то время как логика уведомления UI остается проблемой вне модели домена.

Он будет выглядеть примерно так:

public class NotifyPerson : IPerson, INotifyPropertyChanged
{
    readonly IPerson _inner;

    public NotifyPerson(IPerson inner) // repository puts the "true" domain entity here
    {
        _inner = inner;
    }

    public DateTime BirthDate
    {
        set 
        {
            if(value == _inner.BirthDate)
                return;

            var previousBirthPlace = BirthPlace;
            _inner.BirthDate = value;
            Notify("BirthDate");

            if(BirthPlace != previousBirthPlace) 
                Notify("BirthPlace");
        }
    }

    void Notify(string property)
    {
        var handler = PropertyChanged;
        if(handler != null) handler(this, new PropertyChangedEventArgs(property));
    }
}

Если вы не используете интерфейсы, вы просто наследуете от Person и переопределяете свойство BirthDate, вызывая членов на base вместо _inner.

Ответ 3

Ниже приведен пример реализации. Эта реализация состоит из нескольких уровней: уровня домена, уровня обслуживания и уровня представления. Эта цель уровня обслуживания заключается в том, чтобы вывести функциональные возможности вашего домена на другие уровни, такие как уровень представления или веб-сервис. С этой целью его методы соответствуют конкретным командам, которые могут обрабатываться уровнем домена. В частности, у нас есть команда изменить день рождения. Кроме того, в этой реализации используется версия Udi Dahan инфраструктура событий домена. Это делается для того, чтобы отделить объект домена от бизнес-логики, связанной с изменением дня рождения. Это можно рассматривать как преимущество и недостаток. Недостатком является то, что ваша общая бизнес-логика распространяется на несколько классов. Преимущество в том, что вы получаете большую гибкость в том, как вы обрабатываете события домена. Кроме того, этот подход более расширяем, поскольку вы можете добавить подписчиков в BirthDateChangedEvent, которые выполняют вспомогательные функции. Еще одно преимущество, которое способствовало обоснованию реализации Udi, заключается в том, что ваш объект Person больше не должен знать о каких-либо репозиториях, которые выглядят вне сферы действия объекта домена. В целом, эта реализация требует довольно небольшой инфраструктуры, однако, если вы планируете инвестировать значительные средства в свой домен, то это стоит первоначальной проблемы. Также обратите внимание, что эта реализация предполагала уровень представления ASP.NET MVC. В пользовательском пользовательском интерфейсе логика представления должна измениться, и ViewModel должен будет предоставить уведомления об изменениях.

/// <summary>
/// This is your main entity, while it may seem anemic, it is only because 
/// it is simplistic.
/// </summary>
class Person
{
    public string Id { get; set; }
    public string BirthPlace { get; set; }

    DateTime birthDate;

    public DateTime BirthDate
    {
        get { return this.birthDate; }
        set
        {
            if (this.birthDate != value)
            {
                this.birthDate = value;
                DomainEvents.Raise(new BirthDateChangedEvent(this.Id));
            }
        }
    }
}

/// <summary>
/// Udi Dahan implementation.
/// </summary>
static class DomainEvents
{
    public static void Raise<TEvent>(TEvent e) where TEvent : IDomainEvent
    {
    }
}

interface IDomainEvent { }

/// <summary>
/// This is the interesting domain event which interested parties subscribe to 
/// and handle in special ways.
/// </summary>
class BirthDateChangedEvent : IDomainEvent
{
    public BirthDateChangedEvent(string personId)
    {
        this.PersonId = personId;
    }

    public string PersonId { get; private set; }
}

/// <summary>
/// This can be associated to a Unit of Work.
/// </summary>
interface IPersonRepository
{
    Person Get(string id);
    void Save(Person person);
}

/// <summary>
/// This can implement caching for performance.
/// </summary>
interface IBirthPlaceRepository
{
    bool IsSpecial(string brithPlace);
    string GetBirthPlaceFor(string birthPlace, DateTime birthDate);
}

interface IUnitOfWork : IDisposable
{
    void Commit();
}

static class UnitOfWork
{
    public static IUnitOfWork Start()
    {
        return null;
    }
}

class ChangeBirthDateCommand
{
    public string PersonId { get; set; }
    public DateTime BirthDate { get; set; }
}

/// <summary>
/// This is the application layer service which exposes the functionality of the domain 
/// to the presentation layer.
/// </summary>
class PersonService
{
    readonly IPersonRepository personDb;

    public void ChangeBirthDate(ChangeBirthDateCommand command)
    {
        // The service is a good place to initiate transactions, security checks, etc.
        using (var uow = UnitOfWork.Start())
        {
            var person = this.personDb.Get(command.PersonId);
            if (person == null)
                throw new Exception();

            person.BirthDate = command.BirthDate;

            // or business logic can be handled here instead of having a handler.

            uow.Commit();
        }
    }
}

/// <summary>
/// This view model is part of the presentation layer.
/// </summary>
class PersonViewModel
{
    public PersonViewModel() { }

    public PersonViewModel(Person person)
    {
        this.BirthPlace = person.BirthPlace;
        this.BirthDate = person.BirthDate;
    }

    public string BirthPlace { get; set; }
    public DateTime BirthDate { get; set; }
}

/// <summary>
/// This is part of the presentation layer.
/// </summary>
class PersonController
{
    readonly PersonService personService;
    readonly IPersonRepository personDb;

    public void Show(string personId)
    {
        var person = this.personDb.Get(personId);
        var viewModel = new PersonViewModel(person);
        // UI framework code here.
    }

    public void HandleChangeBirthDate(string personId, DateTime birthDate)
    {
        this.personService.ChangeBirthDate(new ChangeBirthDateCommand { PersonId = personId, BirthDate = birthDate });
        Show(personId);
    }
}

interface IHandle<TEvent> where TEvent : IDomainEvent
{
    void Handle(TEvent e);
}

/// <summary>
/// This handler contains the business logic associated with changing birthdates. This logic may change
/// and may depend on other factors.
/// </summary>
class BirthDateChangedBirthPlaceHandler : IHandle<BirthDateChangedEvent>
{
    readonly IPersonRepository personDb;
    readonly IBirthPlaceRepository birthPlaceDb;
    readonly DateTime importantDate;

    public void Handle(BirthDateChangedEvent e)
    {
        var person = this.personDb.Get(e.PersonId);
        if (person == null)
            throw new Exception();

        if (person.BirthPlace != null && person.BirthDate < this.importantDate)
        {
            if (this.birthPlaceDb.IsSpecial(person.BirthPlace))
            {
                person.BirthPlace = this.birthPlaceDb.GetBirthPlaceFor(person.BirthPlace, person.BirthDate);
                this.personDb.Save(person);
            }
        }
    }
}

Ответ 4

ИМО наилучшим образом подходит для создания хранимой процедуры в вашем db и объекта метки в событии с измененным значением, чтобы вызвать его при совершении изменений в вызове db (SaveChanges()). ObjectContext.ExecuteFunction - ваш друг в этом случае.

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

Изменить: Извините за не ответ на DDD.