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



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