Ako písať čitateľný kód s immutable objektami
Predstavme si, že chceme vytvoriť Immutable triedu / Entitu. Prikladom može byť Value object, message alebo Data Transfer Object (DTO). (Message ktorú mám na mysli je v podstate špeciálnym typom DTO)
Jediná možnosť, ako dostať do triedy parametre, je posunúť parametre konštruktoru triedy.
V rámci konštruktora možeme elegatne vykonať validácie, takže by sa nám nemalo podariť vytvoriť objekt v nekonzistentnom stave. Dajme si (silno neoreginálny) príklad - Immutable Objednávka a Immutable Riadok objednávky s reláciou 1 : n.
public class Order
{
private readonly string _firstName, _lastName;
private readonly int _orderNumber;
private readonly DateTime _created;
private readonly ReadOnlyCollection<OrderLine> _orderLines;
public string LastName
{
get { return _lastName; }
}
public string FirstName
{
get { return _firstName; }
}
public int OrderNumber
{
get { return _orderNumber; }
}
public DateTime Created
{
get { return _created; }
}
public ReadOnlyCollection<OrderLine> OrderLines
{
get { return _orderLines; }
}
public Order(string firstName, string lastName, int orderNumber, DateTime created, IList<OrderLine> orderLines)
{
if (lastName == null)
throw new ArgumentNullException("lastName");
_firstName = firstName;
_lastName = lastName;
_orderNumber = orderNumber;
_created = created;
_orderLines = new ReadOnlyCollection<OrderLine>(orderLines ?? new OrderLine[0]);
}
}
public class OrderLine
{
private readonly string _product;
private readonly int _quantity;
private readonly decimal _price;
public string Product
{
get { return _product; }
}
public int Quantity
{
get { return _quantity; }
}
public decimal Price
{
get { return _price; }
}
public OrderLine(string product, int quantity, decimal price)
{
if (quantity <= 0)
throw new ArgumentOutOfRangeException(
"quantity",
"Value must be higher than 0.");
_product = product;
_quantity = quantity;
_price = price;
}
}
Čím viacej vlastností bude entita mať, tým viac parametrov bude mať konštruktor a tým menej čitatelný kód bude pred nami.
Order order = new Order(
"Mickey",
"Mouse",
12345,
DateTime.Now,
new OrderLine[]{
new OrderLine("Product 1", 10, 20),
new OrderLine("Product 2", 2, 4)
}
);
Mne sa zdá čitatelnejší rozhodne takýto kód.
Order order2 = new OrderBuilder()
.WithNumber(12345)
.CreatedOn(DateTime.Now)
.ForCustomerNamed("Mickey", "Mouse")
.WithOrderLine("Product 1", 10, 20)
.WithOrderLine("Product 2", 2, 4);
Kto nesúhlasí, nech nečíta ďalej :-)
Riešenie je jednoduché. Immutable object + builder = fluent interface. Čo to znamená? Nič ako to, že potrebujeme vytvoriť ku našej triede ešte builder, ktorý umožní postupné nastavovanie parametrov cez metódy, ktoré bude možné chainovať.
public class OrderBuilder
{
private string _firstName, _lastName;
private int _orderNumber;
private DateTime _created;
private List<OrderLine> _orderLines;
public OrderBuilder ForCustomerNamed(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
return this;
}
public OrderBuilder CreatedOn(DateTime date)
{
_created = date;
return this;
}
public OrderBuilder WithNumber(int number)
{
_orderNumber = number;
return this;
}
public OrderBuilder WithOrderLine(string product, int quantity, decimal price)
{
if (_orderLines == null)
_orderLines = new List<OrderLine>();
_orderLines.Add(new OrderLine(product, quantity, price));
return this;
}
public Order BuildOrder()
{
return new Order(_firstName, _lastName, _orderNumber, _created, _orderLines);
}
public static implicit operator Order(OrderBuilder builder)
{
return builder.BuildOrder();
}
}
Veľa zbytočného kódu naviac? Povedzme, že nás ako slušných programátorov čakajú Test Cases, povedzme ze objekt vytvárame v kóde na viacerých miestach. Investícia sa nám rýchlo vráti.
Ak sa nechceme pri debugovaní príliš zahrabať v chainovanom builder kóde, možeme si pomôct označením jednotlivých metód buildera pracujúcich s fieldami(pretože nič zaujímavé sa tam nedeje), attribútom [DebuggerStepThrough] (System.Diagnostics) a F11(step into) skočíme rovno do konštuktora buildovanej triedy.
public class OrderBuilder
{
private string _firstName, _lastName;
private int _orderNumber;
private DateTime _created;
private List<OrderLine> _orderLines;
[DebuggerStepThrough]
public OrderBuilder ForCustomerNamed(string firstName, string lastName)
{
_firstName = firstName;
_lastName = lastName;
return this;
}
[DebuggerStepThrough]
public OrderBuilder CreatedOn(DateTime date)
{
_created = date;
return this;
}
[DebuggerStepThrough]
public OrderBuilder WithNumber(int number)
{
_orderNumber = number;
return this;
}
[DebuggerStepThrough]
public OrderBuilder WithOrderLine(string product, int quantity, decimal price)
{
if (_orderLines == null)
_orderLines = new List<OrderLine>();
_orderLines.Add(new OrderLine(product, quantity, price));
return this;
}
[DebuggerStepThrough]
public Order BuildOrder()
{
return new Order(_firstName, _lastName, _orderNumber, _created, _orderLines);
}
public static implicit operator Order(OrderBuilder builder)
{
return builder.BuildOrder();
}
}
Alternatívne riešenia samozrejme existujú. Builder s gettermi a settermi, ktorý ale neumožnuje logické zoskupovanie relevatných properties a zabstraktraktnenie interface buildera. Dalšou možnosťou je muttable param object vystavujúci všetky properties entity posunutý do konštruktora miesto jednotlivých hodnôt a ich následné prekopírovanie v konštruktore do memberov. Vnášame do našeho immutable objektu zbytočný kód, vytvárame takto zbytočnú obojsmernú závislosť. Rovnaký problém má aj posunutie naplneného builder objektu hoci fluent prístupom do konštruktora. Tieto veci proste smrdia :-) Ďalšie nie moc štastné riešenie fluent buildera, ktoré som videl, bolo cez extension metódy.
Z kategoricky (ne)alternatívnych riešení možem vybať ešte dopisovanie komentárov ku každému parametru všade, kde budeme vytvárať objekt - pracné, neprehľadné a zbytočné. Ani Mouseover vo Visual Studiu pre mňa tiež nie je uspokojivá odpoveď.
Na niektorých detailoch zaleží. Konštruktor parametre ponechané len tak nám budú hovoriť niečo najviac tak mesiac, potom začneme bezpečne zabúdať na ich kontext a skomplikuje to čitatelnosť kódu aj nám, keď už ostatní jej čelia automaticky.