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



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