Showing posts with label Domain Driven Design. Show all posts
Showing posts with label Domain Driven Design. Show all posts

Thursday, April 2, 2009

Data Transfer Object - это не лекарство от "болезни" Rich Domain Model.

В последнее время я все более часто встречаюсь с мнением, что Data Transfer Object - это антипаттерн, позволяющий "хоть как-то выживать rich domain model" в суровом мире n-tier приложений и что при использовании анемичной модели эти источники лишней работы вообще не нужны. На деле же выходит, что это совсем не так.

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

Rich Domain Model - поведение распределяется по бизнес-правилам, правилам валидации, спецификациям, стратегиям, доменным сервисам, сущностям, объектам-значениям. Данные в основном хранятся в сущностях и объектах-значениях.

rdm 

Anemic Domain Model  - под этим типом, обычно понимается несколько вариаций организаций бизнес-логики. Классическая - которую сформулировал сам Фаулер, доменные объекты(присутствуют только сущности и объекты-значения, как наборы примитивных данных) не содержат поведения или содержат его недостаточно и основное его скопление находится в сервисах уровня приложения, что делает способ похожим на способы Transaction Script/Table Module, приводящими к процедурному подходу и подходящими лишь для простых приложений.  Именно от этого сходства и остерегает нас добрый Фаулер, упоминая, что каждый шаблон призван организовывать свой определенный объем и определенную сложность логики приложения(хотя,  формально определить эту величину сложно) .

adm1

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

adm2

Скорее всего Фаулер и этот способ назвал бы анемичным, вообщем-то как и Эванс в своей Big Blue Book подчеркивает некорректность данного подхода.

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

Так вот, объекты переноса данных, помимо отсоединения и предоставления возможности сериализации данных, необходимы для передачи нескольких элеметов информации за один вызов, причем эти элементы в 90% случаев отличаются своей структурой, от на первый взгляд эквивалентных объектов домена, хранящих состояние(сущностей и объектов-значений).

Мне  реально очень сложно найти пример, где необходимо использовать Domain Model в n-tier среде и не нужны Data Transfer Object'ы.  Рассмотрим типичную форму "экстенсивно" развивающейся торговой информационной системы:

myImage (1)

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

Я допускаю возможность существования n-tier приложений, реализованных без объектов переноса данных, но это либо временное явление, характерное для не до конца сформированных требований, либо  приложения со  способом организации и инкапсуляции бизнес-логики отличным от способа Domain Model(и парадигмы DDD).

Поэтому, коллеги, используя анемичную модель, тоже не обойтись без объектов переноса данных(Data Transfer Object - DTO).

Sunday, February 15, 2009

Object/Object Mapping посредством AutoMapper.

В своём предыдущем посте на тему отображения объектов на объекты, я упомянул несколько библиотек, позволяющих осуществлять это самое отображение.  Не так давно появилась разработка от Jimmy Bogard'a под названием AutoMapper. AutoMapper - это преобразователь объектов(Domain Objects->DTO(или других простых объектов различного назначения)), основанный на соглашениях об именовании членов классов для осуществления преобразования по-умолчанию и имеющий fluent-API конфигурации для определения специальных стратегий преобразования.  Разработка очень свежая(зелёная), но поскольку для меня этот вопрос еще полностью не закрыт и я уважаю её автора, то несомненно решил посмотреть на неё.

И так не буду оригинальным с выбором доменной модели для экспериментов :)

Domain

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using DotToolkit.Taijutsu.DomainModel;

namespace Govorov.Nikita.Blog.AutoMapper.DomainModel
{
public class Customer : Entity<long>
{
private IProductDiscountStrategy discountStrategy = new DefaultDiscountStrategy {Percent = 25};
private string name;

protected Customer()
{
}

public Customer(string name)
{
this.name = name;
}

public virtual string Name
{
get { return name; }
protected set { name = value; }
}

public virtual IProductDiscountStrategy DiscountStrategy
{
get { return discountStrategy; }
set { discountStrategy = value; }
}
}

public interface IProductDiscountStrategy
{
decimal Discount(Product product);
}

public class DefaultDiscountStrategy : IProductDiscountStrategy
{
private short percent = 10;

public virtual short Percent
{
get { return percent; }
set { percent = value; }
}

#region IProductDiscountStrategy Members

public decimal Discount(Product product)
{
return new Random().Next()%2 == 0 ? (product.Price*Percent/100) + product.Price : product.Price;
}

#endregion
}

[Serializable]
public class Order : Entity<Guid>, IAggregateRoot
{
private IList<OrderItem> orderItems = new List<OrderItem>();
private Customer customer;

protected Order()
{
}

public Order(Customer owner)
{
this.customer = owner;
}

public virtual Customer Customer
{
get { return customer; }
protected set { customer = value; }
}

protected virtual IList<OrderItem> OrderItems
{
get { return orderItems; }
set { orderItems = value; }
}

public virtual ReadOnlyCollection<OrderItem> Items
{
get { return new ReadOnlyCollection<OrderItem>(OrderItems); }
}

public virtual void IncludeToOrder(short count, Product product)
{
OrderItems.Add(new OrderItem(Customer.DiscountStrategy.Discount(product), count, product));
}

public virtual void IncludeToOrder(Product product)
{
IncludeToOrder(1, product);
}

public virtual decimal GetOrderTotal()
{
return (from item in OrderItems select item.Price).Sum();
}

public virtual int GetProductsCount()
{
return (from item in OrderItems select item.Count).Sum(selector => selector);
}
}

[Serializable]
public class OrderItem : Entity<Guid>
{
private short count;
private decimal price;
private Product product;
private DateTime сreationDate;

protected OrderItem()
{
}

public OrderItem(Product product)
: this(product.Price, 1, product)
{
}

public OrderItem(decimal price, Product product)
: this(price, 1, product)
{
}

public OrderItem(decimal price, short count, Product product)
{
this.price = price;
this.count = count;
this.product = product;
сreationDate = DateTime.Now;
}


public virtual DateTime СreationDate
{
get { return сreationDate; }
protected set { сreationDate = value; }
}

public virtual short Count
{
get { return count; }
protected set { count = value; }
}

public virtual decimal Price
{
get { return price; }
protected set { price = value; }
}

public virtual Product Product
{
get { return product; }
protected set { product = value; }
}
}

[Serializable]
public class ProductKey
{
private long code;
private Vendor vendor;

protected ProductKey()
{
}

public ProductKey(long code, Vendor vendor)
{
this.code = code;
this.vendor = vendor;
}

public virtual long Code
{
get { return code; }
protected set { code = value; }
}

public virtual Vendor Vendor
{
get { return vendor; }
protected set { vendor = value; }
}
}


[Serializable]
public class Product : Entity<ProductKey>
{
private string name;
private decimal price;

protected Product()
{
}

public Product(ProductKey key, string name, decimal price)
{
this.key = key;
this.name = name;
this.price = price;
}

public virtual string Name
{
get { return name; }
protected set { name = value; }
}

public virtual decimal Price
{
get { return price; }
protected set { price = value; }
}
}

[Serializable]
public class Vendor : Entity<Guid>
{
private string title;

protected Vendor()
{
}

public Vendor(string title)
{
this.title = title;
}

public virtual string Title
{
get { return title; }
protected set { title = value; }
}
}
}

преобразовать это нужно в следующее:

Data 

using System;
using System.Collections.Generic;
using System.Text;

namespace Govorov.Nikita.Blog.AutoMapper.Data
{
[Serializable]
public class UserInterfaceOrderData
{
public virtual string CustomerName { get; set; }
public virtual int UniqueProductTypeCount { get; set; }
public virtual decimal OrderTotal { get; set; }
public virtual int ProductsCount { get; set; }
public virtual int ItemsCount { get; set; }
public virtual IList<UserInterfaceOrderItemData> Items { get; set; }

public override string ToString()
{
var builder = new StringBuilder().Append(Environment.NewLine);
foreach (var item in Items)
{
builder.Append(item).Append(Environment.NewLine);
}
return string.Format(" CustomerName: {0},\n UniqueProductTypeCount: {1},\n OrderTotal: {2},\n ProductsCount: {3},\n ItemsCount: {4},\n Items: {5}", CustomerName, UniqueProductTypeCount, OrderTotal, ProductsCount, ItemsCount, builder);
}
}


[Serializable]
public class UserInterfaceOrderItemData
{
public virtual decimal Price { get; set; }
public virtual string ProductName { get; set; }
public virtual string FullProductName { get; set; }
public virtual long ProductCode { get; set; }
public virtual string ProductVendor { get; set; }
public virtual short Count { get; set; }
public virtual DateTime СreationDate { get; set; }

public override string ToString()
{
return string.Format(" ProductName: {1},\n Price: {0},\n FullProductName: {2},\n ProductCode: {3},\n ProductVendor: {4},\n Count: {5},\n СreationDate: {6}", Price, ProductName, FullProductName, ProductCode, ProductVendor, Count, СreationDate);
}
}
}

Код осуществляющий конфигурцию маппера для данного преобразования будет выглядеть следующим образом:

Mapper.CreateMap<Order, UserInterfaceOrderData>()
.ForMember(dto => dto.UniqueProductTypeCount,
map => map.MapFrom(ord =>(from item in ord.Items select item.Product).Distinct().Count()));

Mapper.CreateMap<OrderItem, UserInterfaceOrderItemData>()
.ForMember(dto=>dto.ProductCode,map=>map.MapFrom(ordIt=>ordIt.Product.Key.Code))
.ForMember(dto => dto.ProductVendor, map => map.MapFrom(ordIt => ordIt.Product.Key.Vendor.Title))
.ForMember(dto => dto.FullProductName, map => map.MapFrom(
ordIt => new StringBuilder(ordIt.Product.Key.Vendor.Title)
.Append(" ").Append(ordIt.Product.Name)));

Mapper.AssertConfigurationIsValid(); //Очень полезный метод.

На самом деле выглядит неплохо, но и до идеала явно не дотягивает. Стоит учитывать что на данный момент доступна лишь 0.2.0 Alpha, поэтому очень многого не хватает.  Если проект будет продвигаться такими же темпами, то к первой версии мне кажется он будет единственным пригодным оо маппером для .net (если мои поиски меня не подвели).

Маленькое тестовое приложение, позволяюще наглядно посмотреть  на преобразование:

using System;
using System.Linq;
using System.Text;
using AutoMapper;
using Govorov.Nikita.Blog.AutoMapper.Data;
using Govorov.Nikita.Blog.AutoMapper.DomainModel;

namespace Govorov.Nikita.Blog.AutoMapper
{
internal class Program
{
private static void Main()
{
var microsoft = new Vendor("Microsoft");

var windowsXP = new Product(new ProductKey(1234, microsoft), "Windows XP", 100);
var windowsVista = new Product(new ProductKey(4321, microsoft), "Windows Vista", 200);
var windows7 = new Product(new ProductKey(789, microsoft), "Windows 7", 150);

var customer = new Customer("Горбушка") {DiscountStrategy = new DefaultDiscountStrategy {Percent = 10}};

var order = new Order(customer);

order.IncludeToOrder(windowsXP);
order.IncludeToOrder(windowsVista);
order.IncludeToOrder(windows7);
order.IncludeToOrder(2, windowsXP);


Mapper.CreateMap<Order, UserInterfaceOrderData>()
.ForMember(dto => dto.UniqueProductTypeCount,
map => map.MapFrom(ord =>(from item in ord.Items select item.Product).Distinct().Count()));

Mapper.CreateMap<OrderItem, UserInterfaceOrderItemData>()
.ForMember(dto=>dto.ProductCode,map=>map.MapFrom(ordIt=>ordIt.Product.Key.Code))
.ForMember(dto => dto.ProductVendor, map => map.MapFrom(ordIt => ordIt.Product.Key.Vendor.Title))
.ForMember(dto => dto.FullProductName, map => map.MapFrom(
ordIt => new StringBuilder(ordIt.Product.Key.Vendor.Title)
.Append(" ").Append(ordIt.Product.Name)));

Mapper.AssertConfigurationIsValid(); //Очень полезный метод.

var uiOrderData = Mapper.Map<Order, UserInterfaceOrderData>(order);

Console.WriteLine(uiOrderData);
Console.ReadLine();
}
}
}

Вывод:

CustomerName: Горбушка,
UniqueProductTypeCount: 3,
OrderTotal: 605,
ProductsCount: 5,
ItemsCount: 4,
Items:
ProductName: Windows XP,
Price: 110,
FullProductName: Microsoft Windows XP,
ProductCode: 1234,
ProductVendor: Microsoft,
Count: 1,
СreationDate: 14.02.2009 22:11:08
ProductName: Windows Vista,
Price: 220,
FullProductName: Microsoft Windows Vista,
ProductCode: 4321,
ProductVendor: Microsoft,
Count: 1,
СreationDate: 14.02.2009 22:11:08
ProductName: Windows 7,
Price: 165,
FullProductName: Microsoft Windows 7,
ProductCode: 789,
ProductVendor: Microsoft,
Count: 1,
СreationDate: 14.02.2009 22:11:08
ProductName: Windows XP,
Price: 110,
FullProductName: Microsoft Windows XP,
ProductCode: 1234,
ProductVendor: Microsoft,
Count: 2,
СreationDate: 14.02.2009 22:11:08

Надеюсь, что появится возможность конфигурации и через xml, наподобии как это сделано в otis-lib. Мне нравится fluent-api(привет fluent-nhibernate, Loquacious, fluent-spring, StructureMap и всем остальным), но все таки мне кажется долна быть возможность выбирать.

Monday, January 26, 2009

Value Object и валидация.

Объекты-значения(Value Object) - являются простым, но не смотря на это, одним из ключевых понятий при организации бизнес-логики приложения с помощью паттерна Domain Model. Объекты-значения  -  это простые объекты модели предметной области, равенство которых не основано на равенстве идентификторов(в отличие от сущностей). Равенство объектов-значений определяется на основе равенства значений полей(характеристик) соответсвующего объекта. Классический пример объект-значение Money:

using System;

namespace Govorov.Nikita.Blog.ValueObjectValidation
{
public class Money : IEquatable<Money>
{
private readonly double amout;
private readonly string currency;

public Money(double amout, string currency)
{
if (string.IsNullOrEmpty(currency))
{
throw new ArgumentException(Resource.CurrencyMustBeDefinedMessage);
}

if (amout < 0)
{
throw new ArgumentException(Resource.AmoutMustBePossitiveMessage);
}

this.amout = amout;
this.currency = currency;
}

public virtual double Amout
{
get { return amout; }
}

public virtual string Currency
{
get { return currency; }
}

#region IEquatable<Money> Members

public bool Equals(Money obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.amout == amout && Equals(obj.currency, currency);
}

#endregion

public override bool Equals(object obj)
{
return Equals(obj as Money);
}

public override int GetHashCode()
{
unchecked
{
return (amout.GetHashCode()*397) ^ currency.GetHashCode();
}
}

public static bool operator ==(Money left, Money right)
{
return Equals(left, right);
}

public static bool operator !=(Money left, Money right)
{
return !Equals(left, right);
}
}
}

Поскольку эти объекты следует делать неизменными(immutable), для простоты работы с ними, то соответственно все характеристики объекта-значения должны передаваться агрументами в его конструктор, после вызова которого должен получится готовый к работе объект-значение. Таким образом валидация производится один раз при создании объекта, это является одним из критериев последующей облегченной схемы работы с ним(в отличии от сущностей(Entity)). Т.е. если сущность Субъет(Person) дожна обязательно иметь  имя(Name) - объект значение, все что мы должны сделать, это проверить, то, что у Person установлено Name, потому что сам объект-значние Name не может быть некорректным(иначе мы бы не смогли его создать). Но практически всегда необходимо производить упреждающую проверку(получить информацию, почему мы не можем создать объект-значение при определенных параметрах). Вообщем-то возможен  обычный перехват исключения валидации(н.п. ValidationException) при создании объекта или использование специальных статических методов упреждающей проверки BrokenRulesPreventingConstruction(arg1, arg2, ...), как предлагает Colin Jack в своем блоге.

В своем базовом объекте-значения(Supertype) для валидации я использую нечто среднее между обозначенными выше подходами:

using System;
using DotToolkit.Taijutsu.Common.Extension;

namespace Govorov.Nikita.Blog.ValueObjectValidation
{
internal class Program
{
private static void Main(string[] args)
{
var person = new Person();
var nameCreationResult =
Name.TryTo(() => new Name("Никита", "Говоров")).IfCreated(vo => person.Name = vo);

Console.WriteLine(person);

if (!nameCreationResult)
{
nameCreationResult.BrokenRules.ForEach(rule => Console.WriteLine(rule.Description));
Console.ReadLine();
return;
}


Console.WriteLine(string.Format("Имя: '{0}', Фамилия: '{1}'",
nameCreationResult.ValueObject.FirstName,
nameCreationResult.ValueObject.LastName));

Console.WriteLine(((Name) nameCreationResult).ToString());

Console.ReadLine();
}
}
}

Прототипы:

using System;
using DotToolkit.Taijutsu.DomainModel;
using DotToolkit.Taijutsu.DomainModel.Rule.Validation;

namespace Govorov.Nikita.Blog.ValueObjectValidation
{
public class Person : Entity<Guid>
{
//Проверка обязательности установки этого свойства
//не имеет отношения к теме валидации объектов-значений
public Name Name { get; set; }

public override string ToString()
{
return string.Format("Key: {0}, Name: {1}", Key, Name);
}
}

public class Name : ValueObject<Name>
{
private readonly string firstName = string.Empty;
private readonly string lastName = string.Empty;

public Name(string firstName, string lastName)
{
if (DateTime.Now.Millisecond%2 == 0)
{
throw new ValidationException(Resource.NameValidationExceptionMessage);
}

this.firstName = firstName;
this.lastName = lastName;
}

public virtual string FirstName
{
get { return firstName; }
}

public virtual string LastName
{
get { return lastName; }
}

public override bool Equals(Name obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return Equals(obj.firstName, firstName) && Equals(obj.lastName, lastName);
}

public override int GetHashCode()
{
unchecked
{
return (firstName.GetHashCode() * 397) ^ lastName.GetHashCode();
}
}

public override string ToString()
{
return string.Format("FirstName: {0}, LastName: {1}", firstName, lastName);
}
}
}

Преимущества такого супертипа объекта-значения:

  • при определении объекта-значения позволяет не забыть переопределить методы определения равенства;

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

Код базового класса Value Object(и некоторых сопутствующих):


using System;
using System.Collections.Generic;
using DotToolkit.Taijutsu.DomainModel.Rule.Validation;

namespace DotToolkit.Taijutsu.DomainModel
{
[Serializable]
public abstract class ValueObject<TValueObject> : IEquatable<TValueObject>, IDomainObject
where TValueObject : ValueObject<TValueObject>
{
#region IEquatable<TValueObject> Members

public abstract bool Equals(TValueObject other);

#endregion

public static ValueObjectCreationResult TryTo(Func<TValueObject> ctor)
{
try
{
return new ValueObjectCreationResult(ctor());
}
catch (ValidationException validationException)
{
return new ValueObjectCreationResult(validationException);
}
}

public override bool Equals(object obj)
{
return Equals(obj as TValueObject);
}

public abstract override int GetHashCode();

public static bool operator ==(ValueObject<TValueObject> left, ValueObject<TValueObject> right)
{
return ReferenceEquals(left, null) ? ReferenceEquals(right, null) : left.Equals((TValueObject)right);
}

public static bool operator !=(ValueObject<TValueObject> left, ValueObject<TValueObject> right)
{
return !(left == right);
}

#region Nested type: ValueObjectCreationResult

public class ValueObjectCreationResult : ValidationResult
{
private readonly ValidationException validationException;
private readonly TValueObject valueObject;
private IList<IValidation> brokenRules;

public ValueObjectCreationResult(TValueObject valueObject) : base(null)
{
this.valueObject = valueObject;
}

public ValueObjectCreationResult(ValidationException validationException)
: base(null)
{
this.validationException = validationException;
}

protected virtual ValidationException ValidationException
{
get { return validationException; }
}

public virtual TValueObject ValueObject
{
get { return valueObject; }
}

public override IList<IValidation> BrokenRules
{
get
{
return brokenRules ??
(brokenRules =
(ValidationException != null ? ValidationException.BrokenRules : new List<IValidation>()));
}
}

public virtual ValueObjectCreationResult IfCreated(Action<TValueObject> setter)
{
if (ValueObject != null)
{
setter(ValueObject);
}
return this;
}

public static explicit operator TValueObject(ValueObjectCreationResult x)
{
if (x.ValidationException != null)
{
throw x.ValidationException;
}
return x.ValueObject;
}
}

#endregion
}
}

//--------------------------------------------//

using System;
using System.Collections.Generic;
using System.Linq;

namespace DotToolkit.Taijutsu.DomainModel.Rule.Validation
{
public class ValidationResult
{
private readonly IList<IValidation> brokenRules;

public ValidationResult(IList<IValidation> brokenRules)
{
this.brokenRules = brokenRules;
}

public virtual IList<IValidation> BrokenRules
{
get { return brokenRules; }
}

public virtual ValidationResult FilterBrokenRulesBy(Predicate<IValidation> filter)
{
return new ValidationResult((from rule in brokenRules where filter(rule) select rule).ToList());
}

public static implicit operator bool(ValidationResult result)
{
return result.BrokenRules.Count == 0;
}
}
}



А как вы производите валидацию объектов-значений?

Thursday, September 11, 2008

Сервисы в Domain-Driven дизайне.

В предметной области (домене) встречаются операции, которые не являются частью сущностей (Entity) или простых объектов-значений (Value Objects). Некоторые из них, по сути, представляют некоторое действие или деятельность, но следуя парадигме Domain Driven Design (DDD), где все является объектами, эти действия необходимо представить как объекты. Распространенной ошибкой является вынесение логики (поведения) из сущностей и объектов-значений, и помещение ее в классы сервисов. Сложные операции, в итоге, переполняют объект, искажая его первоначальную роль, поэтому операция в сервисе должна объединять и координировать логику из других доменных объектов, связывая объекты таким образом, что бы представляемые ими концепции, могли рассматриваться отдельно. В отличие от сущностей и объектов-значений сервисы объявлены в терминах того, что они могут сделать для клиента. Имена сервисов строятся на основании деятельности, которую они представляют, в отличие от сущностей.

Основными характеристиками хорошего сервиса являются:

  • операции сервиса принадлежат концепции домена (предметной области), но не являются частью сущности или объекта-значения;
  • интерфейс определяется в терминах других элементов домена (предметной области);
  • сервис не хранит состояние (stateless).

Примеры сервисов домена: IOrderProcessor, IProductFinder, IFundsTransferService.

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

  • cервисы приложения(Application);
  • сервисы предметной область(Domain);
  • инфраструктурные сервисы(Infrastructure).

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

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

Рассмотрим пример разбиения сервисов по слоям:


Application:
Funds Transfer App Service
- обрабатывает входные данные (например запрос в формате XML);
- обращается к сервису домена для осуществления перевода;
- ждет подтверждения;
- решает посылать ли нотификацию через инфраструктурный сервис.

Domain:
Funds Transfer Domain Service
- взаимодействует с необходимыми объектами Account и Ledger, вызывая соответствующую логику предметной области(домена);
- предоставляет результат (разрешен ли перевод или нет и т.п.).

Infrastructure:
Send Notification Service
- посылает e-mail'ы, факсы и осуществляет другую нотификацию поддерживаемую приложением.

Литература: "Domain-Driven Design: Tackling Complexity in the Heart of Software" by Eric Evans.