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



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

Sunday, December 14, 2008

Artorius: абстрактному синтаксическому дереву(AST) быть в NHibernate.

В репозиторий неофициальных дополнений к NHibernate одним из его ведущих разработчиков Fabio Maulo был добавлен проект Artorius. Проект призван решить давнюю проблему - отсутствие  абстрактного синтаксического дерева внутреннего языка запросов. В текущей версии обе основных технологии запросов HQL и Criteria API используют свои уникальные методы трансляции в SQL. Предполагается реализация внутреннего nHQL-AST, именно в который и будут преобразовываться запросы созданные с помощью доступных технологий: HQL( имеются ввиду строковые запросы), Criteria API, LINQ или любого другого языка запросов(кроме нативного SQL).

На текущий момент в репозитории есть определенние грамматики HQL в форме Бэкуса — Наура и реализация ast, при создании ast из строки hql используется модифицированный парсер GOLD:

image

Все тесты проекта на данный момент  направлены на тестирование грамматики, но как видно по коду разработка самого дерева тоже уже ушла  достаточно далеко, что позволяет сделать вывод: ast в NHibernate быть.

Tuesday, December 2, 2008

Visual Studio Team System 2008 Database Edition GDR - RTM

Visual Studio Team System 2008 Database Edition GDR - RTM - очень полезная новость, для тех кто разрабатывает для SQL Server в этой среде(я отношусь к этим людям). Поскольку это практически новый релиз инструмента, в нем очень много новых(недостающих) возможностей. Всем кто с ним работает советую "обновить" (установить) версию.

Feature list

  • Project System
    • Full support for SQL Server 2000, SQL Server 2005 and SQL Server 2008 support.
      • All database objects are now modeled, therefore they are compared and part of deployment. The side-effect of this is that there are no more objects that need to be stored in pre- and post-deployment scripts.
    • Server vs. Database projects
      • This change allows you to model the different behavior of shared and server level objects inside SQL Server vs. user database level objects. 
    • Partial projects
      • Partial project allow you to share implementation between projects, with a single definition of the code
    • Composite projects
      • Composite projects extend database references, allowing you to add objects from other project to the name database schema namespace. So you can compose your database from other projects and/or DBSCHEMA file references
    • Database references using literals, in addition to SQLCMD token support
      • The addition of literal support facilitates that you do not have to change your code of 3-part names, when do do not require the flexibility of name independent deployment
    • Reference support for XML Schema Collections to include .XSD files
      • Enables single sourcing of XML Schema Collections from an .XSD file which itself can be used by other developers or tools.
    • Reference support of SQL-CLR projects and / or SQL-CLR assemblies
      • Enables the inclusion of a SQL-CLR project or a binary, to represent a SQL-CLR assembly. Like XSD references, the goal is single sourcing of artifacts inside the development environment
    • Import Schema and Import Script is now available through DTE
      • This allows the creation of macros or programmatic add-in to drive these common tasks.
    • No more "DesignDB"
      • The change to a fully model driven implementation makes the need for a local SQL Server database instance obsolete. Having a sandbox for your development environment is still am advised practice though, so you can test the changes independently and in an isolated environment.
  • Build
    • Build validates the consistency of the schema model and produces all the files that you need to deploy the database
  • Deploy
    • Deploy takes the output from the build stage and deploys this information based on the settings supplied
    • Deployment settings are now isolated in to the .SQLDEPLOYMENT file, which allows you to standardize and re-use the deployment settings
    • Most of the "Schema Compare" comparison options are now available as the deployment options, to increase the symmetry between the two
    • VSDBCMD.EXE, is a command line deployment tool, which enables the standalone deployment of Database Project
  • Schema Compare:
    • Compares any combination of live databases, Database Projects and .DBSCHEMA files
    • Session level option support, which can be persisted and re-used
    • Object type level filtering allow you to exclude object types from the comparison
    • You can now quickly navigate back and forward between difference via the toolbar and optionally configurable keyboard shortcuts
    • Substitution of SQLCMD variables, allows for the comparison of projects that rely on the use of SQLCMD variables
    • Schema Compare can now be executed through DTE
  • Data Compare
  • Refactoring:
    • New refactoring types: Expand Wildcards and Fully-qualify Names (in addition to Rename, Move Schema and 3/4 part name refactoring)
    • The patented "preservation of intend" functionality makes is possible that the Deployment Engine will deploy renames and move schema operations as actual renames and move schema operations, instead of a DROP / ADD operation.
    • Extensibility enables the creation of your own refactoring types (operations) and refactoring targets.
  • Database Unit Testing
    • Added support for SQL Server 2008
  • Data Generation
    • BulkCopy based inserts in addition to the existing INSERT data sync
    • Sequential Databound Generator is now included in the product
  • T-SQL Static Code Analysis
    • Execute analysis using MS Build and/or as part of the VS IDE build process
    • Including the ability to write you own T-SQL Static Code Analysis rules
  • Extensibility
    • Public full fidelity .NET based T-SQL parser for SQL Server 2000, 2005 and 2008.
    • Public .NET based SQL Script DOM, for programmatically generation of SQL code (equivalent to the Code DOM in .NET)
    • Access to the Schema Model from within Visual Studio, this enables for example ability to write code generators based on the schema model.
    • Feature extensibility:
      • T-SQL Static Code Analysis rules
      • Refactoring types, these are the refactoring operations
      • Refactoring targets, these are the sources that you want to update as part of a refactoring operation
      • Data Generators
      • Data Distributions
      • Test Conditions

Monday, December 1, 2008

Ресурс на тему паттернов, антипаттернов и рефакторинга.

SourceMaking - ресурс, посвященный аспектам разработки программного обеспечения, перечисленным в названии темы сообщения. Таких ресурсов немало в сети, но на этом информация хорошо структурирована и красиво оформлена.

Улыбнули изображения категорий антипаттернов:

 

 

                    Антипаттерны разработки

 

 

 

 

 

 

                    Антипаттерны архитектуры

 

 

 

 

 

 

                    Антипаттерны управления проектом

 

 

 

Не могу не согласиться с Дмитрием Лобасевым: "Ребята здорово постарались!".

Wednesday, November 26, 2008

Autumn of Agile Screencast Series.

Летом Stephen A. Bohlen (вчера, кстати, он стал членом команды NHibernate) записал серию скинкастов "Summer of NHibernate", подробно демонстрирующую основные возможности NHibernate. Так же в ней демонстируются способы эффективного тестирования с помощью TestDriven.NET и MbUnit  и способы организации тестов для кода доступа к данным с использованием NDbUnit. Я думаю данная серия будет интересна многим разработчикам, даже не работающим с NHibernate.

Autumn

В сентябре же Stephen начал новую серию - "Autumn of Agile". В этой серии он рассматривает основные agile-принципы разработки изнутри (со стороны процесса разработки). Мне кажется получится очень достойная серия, советую смотреть.

На протяжении серии автор не только рассматривает подходы и техники, но и демонструет использование конкретных инструментальных средств. Практически со всеми средствами,  демонстрируемыми автором, я был ранее знаком, кроме TargetProcess, которая, после просмотра нескольких демонстраций, показалась мне очень интересной(на данный момент изучаю ее у себя в "инкубаторе").

Saturday, November 22, 2008

Породы программистов(часть 2).

В сообщении Породы программистов(часть 1) были перечислены основные породы программистов, здесь же будут перечислены редкие.

Волшебник

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

Минималист

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

Аналогист

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

Трюкач

Трюкачи слишком увлекаются разными технологическими трюками.Они постоянно осваивают разные новинки, но результат от этого не улучшается. По правде говоря, нас всех в той или иной степени привлекают забавные технологические приемы. Я вот, например, помню мой первый компьютер. Он был аналоговым, и, аоворачивая диски, я переключал ветви в предустановленном аппаратном алгоритме. Эта штука была похожа на гипертрафированную логарифмическую линейку. В общем, я до сих пор люблю забавляться со всякими высокотехнологическими штуковинами.Если вам приходится работать с трюкачами, попытайтесь направить их увлечение игрушками на решение их первоочередной задачи, а именно на производство бизнес-решения. Если им удалось втиснуть на экран, который, как предполагается, будет работать с разрешением 800х600, 30 разных элементов пользовательского интерфейса, это еще совершенно не обозначает, что они решили сво задачу в соответствии с реальными потребностями пользователей. Трюкачи, при всех их познаниях в технологии, часто не могут усвоить конечное назначение программы. Полагая, сто их функции ограничиваются забавами с разными интрументальными средствами, они отказываются учитывать те аспекты программирования, благодаря которым мы не затрачиваем на сопровождение титанических усилий.

Разгильдяй

Что сказать о разгильдяях? Некторые люди небрежны, и это проявляется в коде, который они создают.Они не обращают внимания на такие мелочи, как правильное написание имен переменных и правила венгерской нотации. Зачастую качественно выполнять свои обязанности им мешают проблемы личного плана. Тому, как пишется эффективный код, их нужно учить. Они любят начать с одного стиля, а через процедуру - другую перейти к новому. Читать их код очень утомительно - иногда поздними ночами его приходится переписывать, поскольку иначе есть риск не успеть к сроку сдачи проекта. Если вы не справились с задачей по их вине, прошу вас: отнеситесь к ним снисходительно. В конце концов, они просто отъявленные разгильдяи, котрых лучше всего пересадить в отдел бета-тестирования. Хотя нет - так вы просто заморозите проблему, в итоге она все равно может может проявиться. Если разгильдяй действительно любит писать код, при условии, что вы уделяете достаточно внимания, он имеет шансы реабелитироваться. Всех, кому это не удается, нужно просто пнуть под зад или познакомить с консультантом по трудоустройству.

Тормоз

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

Любитель

Любители очень хотят стать настоящими программистами. Тщательно изучив какой-нибудь инструмент написания макрокоманд, они возводят себя в ранг хакеров. Единственная причина, по которой они бросают уютные места в отделах поддержки пользователей и тестирования, заключается в том, что, по их мнению, быть программистом - это очень круто. Да, мы, действительно, крутые, но, по большому счету, это лишь побочное следствие нашей основной деятельности. Любителям не хватает образования, но по мере их обучения мы должны пристально за ними следить и лишь при условии определенных достижений с их стороны поручать им работу над критически важными приложениями. Узнав на собственном опыте, как трудно заниматься программированием и какое серьезное внимание к деталям требуется программисту, любители часто разочаровываются в своем выборе. Они отказываются признавать превосходство объектно-ориентированных методов над процедурной парадигмой - и все потому, что нужное прозрение их еще не посетило. В защиту любителей вспомним замечательное высказывание: Любители построили ковчег, профессионалы построили титаник. На самом деле иногда свежий, не зашоренный взгляд начинающего программиста очень помогает нам - старым брезгливым технорям.

Профан

Программист-профан - это тот, кто называется тупицей. Хуже всего, когда профан не догадывается о своей тупости. Остерегайтесь таких людей. Иногда они могут достаться вам в наследство от предыдущих руководителей, но сами, я вас прошу, никогда их не нанемайте. У меня нет никаких предубеждений относительно умственно ущербных людей, но я твердо уверен, что в профессии требующей постоянного самосовершенствования и обучения, таким не место. Если человек невежда, но хочет стать лучше, - дайте ему шанс. Отправьте его, например, в отдел тестирования - иногда не отличающиеся выдающимися умственными способностями пользователи находят себя в отлове багов. ще одно одно соображение насчет глупости: на самом деле мы все постоянно страдаем от несовершенства того, что находится между клавиатурой и стулом. Но, в конце концов, если бы для написания кода не требовались мозги, этим бы занимались все без разбора, так ведь? Я советаю не путать невежество с глупостью. Невежество исправимо, а с глупостью лучше просто не связываться. Если вы унаследовали кадры, подобранные не программистом, вполне возможно, что среди ваших подчиненных есть такие типажи. Руководители, имеющие весьма отдаленное представление о технологии, иногда покупаются на необоснованные заверения бездарных претендентов на место.

Эклектик

Можно сказать, что эклектики просто стряпуют программные продукты. Представитель этой породы сочетает в себе какчества инженера, разгильдяя и не слишком талантливого художника, причем упомянутые ингредиенты находятся в чудовищной диспропорции. Результат их деятельности представляет собой винегрет из стилей кодирования и подключаемых модулей при невероятной путанице коде. Все это выглядет довольно привлекательно, но стоит лишь попробывать кусочек, как наступят необратимые последствия. Отправьте такого программиста на кулинарные курсы и обязательно проверьте, не скрывается ли за внешней оболочкой талантливости банальный разгильдяй. В классичсеком виде эта дворянская порода встречатеся довольно редко, а упомянул я ее по той причине, что отдельные ее черты проявляются в ситях кодирования самых разных типов программистов. Если они не считают нужным следовать корпоративным стандартам, вам придется посвящать все рабочее время напряженным попыткам выяснить, что же они все-таки имели в виду и как сопровождать их код. основным средством реабелитации эклектиков служит критика кода.

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

Благодарности автору книги "Herding Cats. A primer of programmers who lead programmers" J. Hank Rainwater'у за хороший материал и отдельное спасибо моей супруге Ирине за помощь, в переносе материала.