Проектируем по DDD. Часть 1: Domain & Application

  • Tutorial
xxx: пока скачаешь одну библиотеку, пока другую, пока их xml конфигом на полметра склеишь, пока маппинг для hibernate настроишь, пока базу нарисуешь, пока веб-сервисы поднимешь
xxx: вроде и hello world пишешь, а уже две недели прошло и всем кажется, что это учетная система для малого бизнеса
ibash.org.ru


В серии из нескольких статей я хотел бы на простом, но имеющим некоторые нюансы, примере рассказать о том, как имея готовые domain и application слои реализовать под них инфраструктуру для хранения и извлечения данных (infrastructure for persistence) используя две различные популярные технологии – Entity Framework Code First и Fluent NHibernate. Если вы хотя бы слышали про три буквы DDD и у вас нет желания послать меня на тоже три, но другие буквы — прошу под кат.



Собственно, само название DDD (Domain-driven development) говорит о том, что разработка начинается и «ведется» предметной областью, а не инфраструктурой и чем-либо ещё. В идеальной ситуации при проектировании вы не должны задумываться о том, какой фрэймворк для хранения данных будет использоваться и как-то менять домен под его возможности – так поступим и мы: в первой статье я опишу пример, под который собственно мы и будем реализовывать инфраструктуру.

Domain Layer


Все мы знаем, что одним из способов организации своего времени является составление TODO листов (планирование задач на конкретные дни). Для этого существует множество программ (да тот же Outlook) – одной из таких программ мы и займёмся. Анализируя эту предметную область можно выделить сразу несколько сущностей:
  • Задача
    • С неопределенным сроком (накопить на новый автомобиль)
    • На конкретную дату
      • На весь день (не курить весь день. PS: я не курю :-))
      • На конкретное время
      • С мелкими подзадачами, причем выполненной считает-ся, если выполнены все подзадачи (зарядка для глаз в течении дня в конкретные часы)
  • Категория (иерархия) в данном примере она будет использоваться только для неопределенных задач.

Обрамив всё это кодом с добавлением спецификаций и контрактов репозитариев получаем такой граф (спасибо CodeMap):

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

Листинги

Агрегаты
TaskBase
    public abstract class TaskBase : Entity
    {
        protected TaskBase(string summary, string desc)
        {
            ChangeSummary(summary);
            ChangeDescription(desc);
        }

        protected TaskBase() { }

        public string Summary { get; private set; }

        public string Description { get; private set; }

        public bool IsComplete { get; protected set; }

        public virtual void Complete()
        {
            if (IsComplete)
                throw new InvalidOperationException("Task is already completed");
            IsComplete = true;
        }

        public void ChangeSummary(string summary)
        {
            Summary = summary;
        }

        public void ChangeDescription(string description)
        {
            Description = description;
        }
    }



IndeterminateTask
    public class IndeterminateTask : TaskBase
    {
        public IndeterminateTask(string summary, string desc, Category category) 
            : base(summary, desc)
        {
            Category = category;
        }

        protected IndeterminateTask() { }

        public Category Category { get; private set; }

        public void ChangeCategory(Category category)
        {
            Category = category;
        }
    }



DailyTask

    public abstract class DailyTask : TaskBase
    {
        protected DailyTask(string summary, string desc, DateTime setupDay) : base(summary, desc)
        {
            DueToDate = setupDay;
        }

        protected DailyTask() { }

        public DateTime DueToDate { get; private set; }

        public void ChangeDueDate(DateTime dueToDate)
        {
            DueToDate = dueToDate;
        }
    }



SingleDailyTask
    public class SingleDailyTask : DailyTask
    {
        public SingleDailyTask(string summary, string desc, DateTime setupDay, bool isWholeDay)
            : base(summary, desc, setupDay)
        {
            IsWholeDay = isWholeDay;
        }

        protected SingleDailyTask() { }

        public bool IsWholeDay { get; private set; }

        public void ChangeIsWholeDay(bool isWholeDay)
        {
            IsWholeDay = isWholeDay;
            if (IsWholeDay)
            {
                ChangeDueDate(DueToDate.Date);
            }
        }
    }



MultipleDailyTask
    public class MultipleDailyTask : DailyTask
    {
        public MultipleDailyTask(string summary, string desc, DateTime setupDay, IEnumerable<DateTime> dueToDates)
            : base(summary, desc, setupDay)
        {
            ChangeSubtasks(dueToDates.ToList());
        }

        protected MultipleDailyTask() { }

        public virtual ICollection<Subtask> Subtasks { get; set; }

        public override void Complete()
        {
            throw new NotSupportedException();
        }
        
        public void CompleteSubtask(DateTime subtaskDueDate)
        {
            if (Subtasks == null)
                throw new InvalidOperationException();
            
            var subtask = Subtasks.FirstOrDefault(i => i.DueTime == subtaskDueDate);
            if (subtask == null)

                throw new InvalidOperationException();
            subtask.Complete(DateTime.Now);
            var hasUncompleted = Subtasks.Any(i => i.CompletedAt == null);
            if (!hasUncompleted)
            {
                base.Complete();
            }
        }

        public bool HasUncompletedSubtasks
        {
            get { return Subtasks != null && Subtasks.Any(i => i.CompletedAt == null); }
        }

        public int CompletionPercentage
        {
            get
            {
                var totalSubtasks = Subtasks.Count;
                var completedSubtasks = Subtasks.Count(i => i.CompletedAt.HasValue);
                if (totalSubtasks == 0 || totalSubtasks == completedSubtasks) 
                    return 100;

                return (int) Math.Round(completedSubtasks * 100.0 / totalSubtasks, 0);
            }
        }

        public void ChangeSubtasks(ICollection<DateTime> subtasksDueToDates)
        {
            var times = subtasksDueToDates.Select(i => i.ToTime());

            if (Subtasks == null)
            {
                Subtasks = times.Select(i => new Subtask(i)).ToList();
                return;
            }

            var oldSubtasks = Subtasks.ToList();
            var newSubtasks = times.ToList();

            //removing no longer exist items
            foreach (var oldSubtask in oldSubtasks)
            {
                if (!newSubtasks.Contains(oldSubtask.DueTime))
                {
                    Subtasks.Remove(oldSubtask);
                }
            }

            //adding new
            foreach (var newSubtask in newSubtasks)
            {
                if (Subtasks.All(i => i.DueTime != newSubtask))
                {
                    Subtasks.Add(new Subtask(newSubtask));
                }
            }
        }
    }



Subtask
    public class Subtask : Entity
    {
        public DateTime DueTime { get; private set; }

        public DateTime? CompletedAt { get; private set; }

        public Subtask(DateTime dueTime)
        {
            DueTime = dueTime;
        }

        public void Complete(DateTime completedAt)
        {
            CompletedAt = completedAt;
        }

        protected Subtask() { }
    }



Category
    public class Category : Entity
    {
        public Category(string name, Category parentCategory)
        {
            Name = name;
            ParentCategory = parentCategory;
        }

        protected Category() { }

        public string Name { get; private set; }

        public virtual ICollection<IndeterminateTask> Tasks { get; set; }

        public virtual ICollection<Category> ChildrenCategories { get; set; }
        
        public virtual Category ParentCategory { get; private set; }

        public void ChangeName(string name)
        {
            Name = name;
        }

        public void ChangeParentCategory(Category category)
        {
            ParentCategory = category;
        }
    }



Репозитории
    public interface IRepository
    {
    }

    public interface ITaskRepository : IRepository
    {
        IEnumerable<TaskBase> AllMatching(Specification<TaskBase> specification);

        void Add(TaskBase taskBase);

        void Remove(TaskBase taskBase);

        TaskBase Get(Guid taskId);
    }
	
    public interface ICategoryRepository : IRepository
    {
        IEnumerable<Category> All();

        void Add(Category category);

        void Remove(Category category);

        Category Get(Guid id);
    }


Спецификации
    public static class CategorySpecifications
    {
        public static Specification<Category> Name(string name)
        {
            return new DirectSpecification<Category>(category => category.Name == name);
        }
    }
	
    public static class TaskSpecifications
    {
        public static Specification<TaskBase> CompletedTask()
        {
            return new DirectSpecification<TaskBase>(task => task.IsComplete);
        }

        public static Specification<TaskBase> DueToDateRange(DateTime startDateIncl, DateTime endDateIncl)
        {
            var spec = IsDailyTask();
            spec &= new DirectSpecification<TaskBase>(task => ((DailyTask)task).DueToDate >= startDateIncl && ((DailyTask)task).DueToDate <= endDateIncl);
            return spec;
        }

        public static Specification<TaskBase> IsIndeterminatedTask()
        {
            return new DirectSpecification<TaskBase>(task => task is IndeterminateTask);
        }

        public static Specification<TaskBase> IsDailyTask()
        {
            return new DirectSpecification<TaskBase>(task => task is DailyTask);
        }
    }



Про спецификации и репозитарии можно прочитать в другой моей статье.
Как вы видите – пример простой, но есть нюансы – наследование бизнес-объектов (причем некоторые из наследников имеют свои связи на другие объекты), обоюдные связи, иерархия категорий (связь самой на себя) – всё это может (и доставит) некоторый зуд в одном месте при реализации инфраструктуры. Ещё стоит обратить внимание, что IRepository пуст, а не полон всевозможных методов типа AllMatching, GetById, Delete, Add и т.п. – так на деле лучше: для каждого конкретного репозитория определять только необходимые методы. Почему обобщенный репозиторий — это зло, можно почитать тут.

Application & Distributed Services Layers


Cлой приложения в нашем примере будет представлен двумя сборками – одна для определения DTO объектов, а вторая – для сервисов и адаптеров DTO <-> Entities, а слой распределенных сервисов представляет собой web empty application с одним WCF сервисом (фасадом) который просто пробрасывает методы сервисов уровня приложения используя те же DTO (благо WCF DataContractSerializer не требует наличия каких-либо атрибутов и умеет работать с иерархиями классов)
В качестве примера рассмотрим два метода сервиса: удаление задачи и получение всех задач на текущий месяц.

Application layer:

public void RemoveTask(Guid taskId)
{
    using (var unitOfWork = UnitOfWorkFactory.Create())
    {
        var task = _tasksRepository.Get(taskId);
        if (task == null)
            throw new Exception();

        _tasksRepository.Remove(task);
        unitOfWork.Commit();
    }
}

public IEnumerable<DailyTaskDTO> GetMonthTasks()
{
    var nowDate = DateTime.Now;
    var monthStartDate = new DateTime(nowDate.Year, nowDate.Month, 1);
    var monthEndDate = new DateTime(nowDate.Year, nowDate.Month, DateTime.DaysInMonth(nowDate.Year, nowDate.Month));

    var specification = TaskSpecifications.DueToInDateRange(monthStartDate, monthEndDate);

    var tasks = _tasksRepository.AllMatching(specification).ToList();
    return tasks.OfType<DailyTask>().ProjectedAsCollection<DailyTaskDTO>().ToArray();
}

И прокидывается в Distributed Services

public void RemoveTask(Guid taskId)
{
    using (ILifetimeScope container = BeginLifetimeScope())
    {
        container.Resolve<TaskService>().RemoveTask(taskId);
    }
}

public List<DailyTaskDTO> GetMonthTasks()
{
    using (ILifetimeScope container = BeginLifetimeScope())
    {
        return container.Resolve<TaskService>().GetMonthTasks().ToList();
    }
}

Как вы видите Application layer занимается всей черной работой: вызов репозитариев, оборачивание в транзакции и конвертирование сущностей в DTO (используя замечательное средство – AutoMapper). Оборачивание методов сервиса в LifetimeScope в Distributed Services дает нам возможность инициализировать репозитории общим объектом UnitOfWork (single instance per lifetime scope).
Собственно хватит скучного текста и воды – вот исходники.

Инфраструктура


Целью данной статьи не было объяснение что такое DDD — для этого есть много книг и в формате статьи это никак не уместится — я хотел уделить внимание реализации инфраструктуры, а конкретно эта статья лишь введение, так что продолжение следует…
Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 15
  • 0
    Не сочтите за занудство, но Fluent NHibernate не является законченной обособленной технологией, которую можно хоть как-то сравнивать с EF и прочими ORM.
    • 0
      Не очень понял что вы хотели этим сказать. На мой взгляд nHibernate в целях code first (реализации репозитария и uow) по возможностям на голову выше EF.
      • –1
        Да, но он конфигурируется при помощи XML-маппингов. Что неудобно.
        Поэтому сторонние разработчики подсуетились и предоставили fluent API, генерирующее эти маппинги за вас. Назвали своё творение Fluent NHibernate и распространяют его отдельной сборкой, завязанной на конкретную версию NH.
        • +1
          Не вижу проблемы :-)
          • +1
            Fluent NHibernate != NHibernate. Первое — конфигуратор, второе — ORM. Поборники чистоты изложения (типа меня) будут обоснованно придираться
          • +3
            Простите за занудство, но у NHibernate уже больше года есть свой встроенный собственный маппинг через код.

            mapper.Class<LocalizationEntry>(m =>
                    {
                        m.ComponentAsId(x => x.Id, n =>
                        {
                            n.Property(x => x.Culture);
                            n.Property(x => x.EntityId);
                            n.Property(x => x.Property);
                            n.Property(x => x.Type);
                        });
            
                        m.Property(t => t.Message, c =>
                        {
                            c.NotNullable(true);
                            c.Length(400);
                        });
                    });
            


            Что касается возможностей и способностей NHibernate, то пока что он более Mature и функционален чем EF, но, думаю, к 7й версии ЕФ надогонит :(. Подзабросили что-то последнее время девелопмент хибера
            • 0
              Спасибо, не знал. Почитаю :)

              Я то не спорил что NH лучше\хуже EF. Просто автор ошибся, упомянув в статье Fluent NHibernate и я ему на это указал.
            • 0
              В NHibernate маппинги же можно вроде с помощью атрибутов делать?
              • 0
                Можно, но это практически никому не нужно, persistence ignorance же.
                • 0
                  Да и, по-моему, реализаций этих аттрибутов у NH было столько, что многие уже запутались…
        • 0
          И собственно не очень понятна всё же цель приложения: предметную область описали, спроектировали. А затем сразу перешли к WCF, требующему DTO на каждый чих. Зачем? Это будет не конечное приложение, а API, доступное через веб? Или вы описываете только backend для клиент-серверного приложения? Без этого уточнения банально не понятен раздел «Application & Distributed Services Layers»
          • 0
            Описана часто используемая архитектура из 5 слоев, некоторые даже называют это DDDD (Distributed DDD). Пока описан только бэкэнд без представления, Distributed services layer я добавил в эту статью мимоходом т.к. он очень тонкий.
            • 0
              Так укажите это во введении! В DDD по определению слоёв только четыре и может быть это только для узкого круга людей 5й слой — «часто используемая архитектура».
          • 0
            Не плохо бы хранить даты в UTC.
            • 0
              Хорошая статья, но как-то не комильфо давать прямой доступ к коллекциям дочерних элементов сущности, а меж тем реализация этого сама по себе достаточно интересная тема, хоть и на первый взгляд простая.

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