📜 ⬆️ ⬇️

We design on DDD. Part 1: Domain & Application

xxx: while you download one library, while another, while you glue the xml config for half a meter, while you configure the mapping for hibernate, while you draw the base, while you lift the web services
xxx: like and hello world you write, and two weeks have passed and it seems to everyone that this is an accounting system for small business
ibash.org.ru


In a series of several articles, I would like to tell you about a simple, but with some nuances, example of how having a ready domain and application layers to implement an infrastructure for data storage and retrieval (infrastructure for persistence) using two different popular technologies - Entity Framework Code First and Fluent NHibernate . If you at least heard about the three letters DDD and you do not have the desire to send me to the same three, but other letters - please under the cat.



Actually, the very name DDD (Domain-driven Development) says that the development begins and is “conducted” by the subject area, and not by infrastructure or anything else. In an ideal situation when designing, you should not think about which data storage framework will be used and somehow change the domain under its capabilities - so we will: in the first article I will describe an example by which we will actually implement the infrastructure.
')

Domain layer


We all know that one of the ways to organize your time is to create TODO sheets (planning tasks for specific days). For this, there are many programs (yes, the same Outlook) - we will deal with one of these programs. Analyzing this subject area, you can select several entities at once:

Framing it all with code, adding specifications and repository contracts, we get this graph (thanks to CodeMap):

I agree that the names are not the most successful (especially “MultipleDailyTask”, but unfortunately there is no other thought). This graph does not provide information about the attribute composition of objects, but instead shows the subject area as a whole. The following code listings will correct this mistake:

Listings

Aggregates
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; } } 



Repositories
  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); } 


Specs
  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); } } 



About specifications and repositories can be found in my other article .
As you can see, the example is simple, but there are nuances — the inheritance of business objects (and some of the heirs have their connections to other objects), reciprocal relationships, the category hierarchy (the connection itself) can all (and deliver) some itch one place in the implementation of infrastructure. You should also note that the IRepository is empty, and not full of all sorts of methods such as AllMatching, GetById, Delete, Add, etc. - so in fact it is better: for each specific repository, only the necessary methods should be determined. Why a generic repository is evil, you can read it here .

Application & Distributed Services Layers


In our example, the application layer will be represented by two assemblies - one for defining DTO objects, and the second for DTO <-> Entities services and adapters, and the distributed services layer is a web empty application with one WCF service (facade) that simply forwards the service methods application level using the same DTO (the benefit of WCF DataContractSerializer does not require any attributes and can work with class hierarchies)
As an example, consider two methods of service: deleting a task and getting all tasks for the current month.

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(); } 

And prokibyvaetsya 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(); } } 

As you can see, the Application layer does all the hard work: invoking repositories, wrapping things up in a transaction, and converting entities to DTO (using a great tool, AutoMapper). Wrapping service methods in LifetimeScope in Distributed Services allows us to initialize repositories with a common UnitOfWork object (single instance per lifetime scope).
Actually enough boring text and water - that's the source .

Infrastructure


The purpose of this article was not an explanation of what DDD is - there are many books for this and in the article format it doesn’t fit in - I wanted to pay attention to the implementation of the infrastructure, and this article is only an introduction, so the continuation follows ...

Source: https://habr.com/ru/post/173893/


All Articles