Я использую собственный Union Type.
Рассмотрим пример, чтобы прояснить ситуацию.
Представьте, что у нас есть класс контактов:
public class Contact
{
public string Name { get; set; }
public string EmailAddress { get; set; }
public string PostalAdrress { get; set; }
}
Все они определены как простые строки, но на самом деле это просто строки?
Конечно, нет. Имя может состоять из имени и фамилии. Или электронная почта - это просто набор символов? Я знаю, что по крайней мере он должен содержать @, и это обязательно.
Давайте улучшим нам модель домена
public class PersonalName
{
public PersonalName(string firstName, string lastName) { ... }
public string Name() { return _fistName + " " _lastName; }
}
public class EmailAddress
{
public EmailAddress(string email) { ... }
}
public class PostalAdrress
{
public PostalAdrress(string address, string city, int zip) { ... }
}
В этих классах будут проверки при создании, и в итоге у нас будут действительные модели. Consturctor в классе PersonaName требует имен FirstName и LastName одновременно. Это означает, что после создания оно не может иметь недопустимое состояние.
И контактный класс соответственно
public class Contact
{
public PersonalName Name { get; set; }
public EmailAdress EmailAddress { get; set; }
public PostalAddress PostalAddress { get; set; }
}
В этом случае у нас та же проблема, объект класса Contact может быть в недопустимом состоянии. Я имею в виду, что у него может быть EmailAddress, но нет имени
var contact = new Contact { EmailAddress = new EmailAddress("foo@bar.com") };
Давайте исправим это и создадим класс Contact с конструктором, который требует PersonalName, EmailAddress и PostalAddress:
public class Contact
{
public Contact(
PersonalName personalName,
EmailAddress emailAddress,
PostalAddress postalAddress
)
{
...
}
}
Но здесь у нас другая проблема. Что делать, если у Person есть только EmailAdress и нет PostalAddress?
Если мы подумаем об этом, мы поймем, что существует три возможности действительного состояния объекта класса Contact:
- Контакт имеет только адрес электронной почты
- Контакт имеет только почтовый адрес
- Контакт имеет адрес электронной почты и почтовый адрес
Давайте выпишем доменные модели. Для начала создадим класс Contact Info, состояние которого будет соответствовать вышеприведенным случаям.
public class ContactInfo
{
public ContactInfo(EmailAddress emailAddress) { ... }
public ContactInfo(PostalAddress postalAddress) { ... }
public ContactInfo(Tuple<EmailAddress,PostalAddress> emailAndPostalAddress) { ... }
}
А Контактный класс:
public class Contact
{
public Contact(
PersonalName personalName,
ContactInfo contactInfo
)
{
...
}
}
Давайте попробуем использовать это:
var contact = new Contact(
new PersonalName("James", "Bond"),
new ContactInfo(
new EmailAddress("agent@007.com")
)
);
Console.WriteLine(contact.PersonalName()); // James Bond
Console.WriteLine(contact.ContactInfo().???) // here we have problem, because ContactInfo have three possible state and if we want print it we would write `if` cases
Давайте добавим метод Match в класс ContactInfo
public class ContactInfo
{
// constructor
public TResult Match<TResult>(
Func<EmailAddress,TResult> f1,
Func<PostalAddress,TResult> f2,
Func<Tuple<EmailAddress,PostalAddress>> f3
)
{
if (_emailAddress != null)
{
return f1(_emailAddress);
}
else if(_postalAddress != null)
{
...
}
...
}
}
В методе match мы можем написать этот код, потому что состояние класса контакта контролируется конструкторами и может иметь только одно из возможных состояний.
Давайте создадим вспомогательный класс, чтобы каждый раз не писать столько кода.
public abstract class Union<T1,T2,T3>
where T1 : class
where T2 : class
where T3 : class
{
private readonly T1 _t1;
private readonly T2 _t2;
private readonly T3 _t3;
public Union(T1 t1) { _t1 = t1; }
public Union(T2 t2) { _t2 = t2; }
public Union(T3 t3) { _t3 = t3; }
public TResult Match<TResult>(
Func<T1, TResult> f1,
Func<T2, TResult> f2,
Func<T3, TResult> f3
)
{
if (_t1 != null)
{
return f1(_t1);
}
else if (_t2 != null)
{
return f2(_t2);
}
else if (_t3 != null)
{
return f3(_t3);
}
throw new Exception("can't match");
}
}
Мы можем заранее иметь такой класс для нескольких типов, как это делается с делегатами Func, Action. 4-6 параметров универсального типа будут полностью для класса Union.
Перепишем ContactInfo
класс:
public sealed class ContactInfo : Union<
EmailAddress,
PostalAddress,
Tuple<EmaiAddress,PostalAddress>
>
{
public Contact(EmailAddress emailAddress) : base(emailAddress) { }
public Contact(PostalAddress postalAddress) : base(postalAddress) { }
public Contact(Tuple<EmaiAddress, PostalAddress> emailAndPostalAddress) : base(emailAndPostalAddress) { }
}
Здесь компилятор запросит переопределение хотя бы для одного конструктора. Если мы забудем переопределить остальные конструкторы, мы не сможем создать объект класса ContactInfo с другим состоянием. Это защитит нас от исключений во время выполнения.
var contact = new Contact(
new PersonalName("James", "Bond"),
new ContactInfo(
new EmailAddress("agent@007.com")
)
);
Console.WriteLine(contact.PersonalName()); // James Bond
Console
.WriteLine(
contact
.ContactInfo()
.Match(
(emailAddress) => emailAddress.Address,
(postalAddress) => postalAddress.City + " " postalAddress.Zip.ToString(),
(emailAndPostalAddress) => emailAndPostalAddress.Item1.Name + emailAndPostalAddress.Item2.City + " " emailAndPostalAddress.Item2.Zip.ToString()
)
);
Вот и все.
Я надеюсь, вам понравилось.
Пример взят с сайта F # для удовольствия и прибыли