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

в 11:26, , рубрики: .net, DDD, domain-driven design, Программирование, проектирование, Проектирование и рефакторинг, метки: , , ,

xxx: там порог вхождения выше
xxx: тебе на asp.net'е hello world настрогать за 10 минут научиться можно (8 минут на запуск студии, 2 на кодинг)
xxx: на java такое не прокатит
xxx: пока скачаешь одну библиотеку, пока другую, пока их xml конфигом на полметра склеишь, пока маппинг для hibernate настроишь, пока базу нарисуешь, пока веб-сервисы поднимешь
xxx: вроде и hello world пишешь, а уже две недели прошло и всем кажется, что это учетная система для малого бизнеса
© ibash.org.ru/quote.php?id=15446

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

image

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

Domain Layer

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

  • Задача
    • С неопределенным сроком (накопить на новый автомобиль)
    • На конкретную дату
      • На весь день (не курить весь день. PS: я не курю :-))
      • На конкретное время
      • С мелкими подзадачами, причем выполненной считает-ся, если выполнены все подзадачи (зарядка для глаз в течении дня в конкретные часы)
  • Категория (иерархия) в данном примере она будет использоваться только для неопределенных задач.

Обрамив всё это кодом с добавлением спецификаций и контрактов репозитариев получаем такой граф (спасибо CodeMap):
image
Я согласен, что названия не самые удачные (особенно “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
    {
        IUnitOfWork UnitOfWork { get; }
    }

    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 содержит лишь одно свойство – ссылку на экземпляр UnitOfWork, а не определение всевозможных методов типа 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 transaction = TransactionFactory.Create())
    {
        var task = _tasksRepository.Get(taskId);
        if (task == null)
            throw new Exception();

        _tasksRepository.Remove(task);
        _tasksRepository.UnitOfWork.Commit();
        transaction.Complete();
    }
}

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 — для этого есть много книг и в формате статьи это никак не уместится — я хотел уделить внимание реализации инфраструктуры, а конкретно эта статья лишь введение, так что продолжение следует…

Автор: Nagg

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js