Monday, October 26, 2009

Common Service Factory.

Common Service Factory – библиотека, позволяющая использовать Dependency Inversion Principle для WCF- сервисов. Основным ее отличием от всех остальных (использующих реализацию определенных IOC-контейнеров) является возможность работы с любым контейнером, т.к. она построена на основе другого полезного проекта Common Service Locator, отсюда сразу следует уточнение, что для используемого вами контейнера должен быть реализован адаптер к локатору служб, однако это не является проблемой, потому что для большинства популярных контейнеров они уже реализованы и их реализация является тривиальной задачей.

У автора достаточно четко описан способ работы с библиотекой, я же хочу показать немного иной способ, используя конкретный пример и контейнер. И так контейнер Castle Windsor и сервис:

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
[ServiceContract(SessionMode = SessionMode.Allowed)]
public interface IMyService
{
[OperationContract]
int DoWork();
}
}

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
[ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)]
public class MyService : IMyService, INotMyService
{
private int balance;
private readonly IBalanceChangingPolicy policy;

public MyService(IBalanceChangingPolicy policy)
{
this.policy = policy;
balance = 0;
}

public int DoWork()
{
balance = policy.Apply(balance);
return balance;
}

public void DoNotWork()
{
}
}
}

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

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public interface IBalanceChangingPolicy
{
int Apply(int balance);
}
}


namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public class DefaultBalanceChangingPolicy : IBalanceChangingPolicy
{
public int Apply(int balance)
{
return ++balance;
}
}
}

Теперь необходимо наш сервис где-то и как-то расположить, для простоты и оригинальности пусть это будет консольное приложение.

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
class Program
{
static void Main()
{
var locator = new WindsorServiceLocator( new WindsorContainer()
.Register(Component.For<IMyService>()
.ImplementedBy<MyService>().LifeStyle.Transient)
.Register(Component.For<IBalanceChangingPolicy>()
.ImplementedBy<DefaultBalanceChangingPolicy>())
);
ServiceLocator.SetLocatorProvider(() => locator);
var host = new ServiceHost(typeof(MyService));
host.Open();
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
host.Close();
}
}
}

Код, приведенный выше, безнадежно прост, единственное что можно отметить: класс ServiceHost это нестандартный класс из System.ServiceModel, по идее это класс из библиотеки Common Service Factory, однако для простоты его использования, я сделал так.

using CommonServiceFactoryServiceHost = CommonServiceFactory.ServiceHost;

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public class ServiceHost : CommonServiceFactoryServiceHost
{
public ServiceHost(Type serviceType, params Uri[] baseAddresses)
: base(serviceType, baseAddresses)
{
}
}
}

Ну и ожидаемо-стандартная конфигурация.

<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior name="Govorov.Nikita.Blog.CommonServiceFactory.MyServiceBehavior">
<serviceDebug includeExceptionDetailInFaults="true"/>
<serviceMetadata policyVersion="Policy15"/>
</behavior>
</serviceBehaviors>
</behaviors>
<services>
<service behaviorConfiguration="Govorov.Nikita.Blog.CommonServiceFactory.MyServiceBehavior" name="Govorov.Nikita.Blog.CommonServiceFactory.MyService">
<endpoint address="" binding="netTcpBinding" contract="Govorov.Nikita.Blog.CommonServiceFactory.IMyService" />
<endpoint address="mex" binding="mexTcpBinding" contract="IMetadataExchange" />
<host>
<baseAddresses>
<add baseAddress="net.tcp://localhost:9998"/>
</baseAddresses>
</host>
</service>
</services>
</system.serviceModel>

Запустив сервис, сгенерируем клиентский код и  напишем вызовы сервиса.

namespace Govorov.Nikita.Blog.CommonServiceFactory.Client
{
internal class Program
{
private static void Main()
{
ServiceClientScope<IMyService>.Use(service =>
{
Console.WriteLine(service.DoWork());
Console.WriteLine(service.DoWork());
Console.WriteLine(service.DoWork());
});

Console.ReadKey();
}
}
}

ServiceClientScope – статический класс, позволяющий обойти неприятность допущенную WCF-разработчиками в методе Dispose канала.

namespace Govorov.Nikita.Blog.CommonServiceFactory.Client
{
public static class ServiceClientScope<T>
{
private static readonly ChannelFactory<T> ChannelFactory = new ChannelFactory<T>("*");

public static void Use(Action<T> codeBlock)
{
var proxy = (IClientChannel) ChannelFactory.CreateChannel();
bool success = false;
try
{
codeBlock((T) proxy);
proxy.Close();
success = true;
}
finally
{
if (!success)
{
proxy.Abort();
}
}
}
}
}

Запустив клиентское приложение мы получим три цифры 1 на консоль, теперь можно попробовать создать класс:

namespace Govorov.Nikita.Blog.CommonServiceFactory
{
public class NonDefaultBalanceChangingPolicy : IBalanceChangingPolicy
{
public int Apply(int balance)
{
return (balance + 2);
}
}

изменить код  bootstrapper’а:

var locator = new WindsorServiceLocator(new WindsorContainer()
.Register(Component.For<IMyService>()
.ImplementedBy<MyService>().LifeStyle.Transient)
.Register(Component.For<IBalanceChangingPolicy>()
.ImplementedBy<NonDefaultBalanceChangingPolicy>())
);

и снова запустить оба приложения, получим три цифры 2 на консоль, если изменить InstanceContextMode на PerSession получим 2,4,6.

Из типовых сценариев использования так же присутствует возможность использования конвенций для выбора интерфейса, по которому будет выбран объект из контейнера(т.е. из контейнера можно выбирать объект сервиса не по контракту(по-умолчанию используется I+ServiceType), а по любому другому интерфейсу). Не смотря на то что  это уже добавляет немного запутанности, так же присутствуют дополнительные опасные места:

  1. Cледует обращать внимание на используемый InstanceContextMode и тип жизни вашего сервиса в IOC-контейнере(LifeStyle для Castle Windsor, и т.п. для др.). Например, не получится использовать сервис в режиме InstanceContextMode.Single т.к.  для таких сервисов  в WCF зашит вызов конструктора по-умолчанию(и если у вашего сервиса есть конструктор по-умолчанию, даже приватный, сервис будет создан с помощью этого конструктора без каких-либо инъекций). Если вам необходимо использовать сервис в режиме InstanceContextMode.Single, возможен вариант с использованием конструктора ServiceHost(object singletonInstance, params System.Uri[] baseAddresses) класса ServiceHost из System.ServiceModel, куда передается уже сконфигурированный объект из контейнера, так же можно оставить InstanceContextMode по-умолчанию(PerSession) и определить объект в контейнере как singleton.
  2. Не следует передавать в ServiceHost  один тип сервиса (new ServiceHost(typeof(MyService))), а регистрировать в контейнере другой(Register(Component.For<IMyService>().ImplementedBy<MyService2>().LifeStyle.Transient), это может  приводить к ошибкам, т.к. WCF зарегистрирует для вашего сервиса данные из атрибутов(например, ServiceBehavior) именно от того типа который был передан в конструктор ServiceHost’а.

Ну и в заключении, для использования библиотеки под IIS(WPAS), в разметке *.svc файла  необходимо указать следующую директиву, например:

<%@ ServiceHost Language="C#" Debug="true" Service="WebApplication.MyService, WebApplication" CodeBehind="MyService.svc.cs" Factory="CommonServiceFactory.ServiceHostFactory, CommonServiceFactory" %>

Friday, July 24, 2009

yUML – сервис создания и публикации UML-диаграмм.

Сегодня обнаружил интересный онлайн сервис создания и публикации диаграмм классов, вариантов использования, активностей – yUML. C помощью простого, но в тоже время выразительного DSL можно создавать диаграммы, например, классов:

[Customer]<>-orders*>[Order]

[Order]++-0..*>[LineItem]

[Order]-[note:Aggregate root.]

b2693ca[1]

 

Далее, диаграмму можно сохранить как PNG, JPG, PDF или опубликовать:

http://yuml.me/diagram/scruffy/class/[Customer]1-0..*[Address]  или  http://yuml.me/1e0d7c03

Пример диаграммы вариантов использования:

Thursday, April 2, 2009

Плагин для разработки голосом!

Сегодня, вышел новый плагин для IntelliJ IDEA.  Плагин позволяет управлять процессом разработки голосом. Скачать его можно от сюда. :)))

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



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