Inspiring people. Excellent results.

Get There ICT professionals in Leek, Groningen. Gedreven door ICT willen wij samen meer bereiken, als betrokken en professionele ICT partner.

  • Deel:

Een C# implementatie van het Builder Pattern met lambdas

Richard de Zwart, 1-11-2017

Er is natuurlijk al het nodige geschreven over het hoe en waarom van het Builder Pattern. Dus ik ga er vanuit dat je hier gekomen bent omdat je zeker weet dat je een Builder nodig hebt, en dat je zoekt naar een manier om het anders te doen.

De implementaties die ik tot zover vond (zoals die van Cameron McKay of Nat Pryce) volgen uiteraard altijd hetzelfde patroon: een builder class met een methode voor elk stukje van het object wat er gebouwd moet worden (aantal deuren, kleur, type brandstof). Die code ziet eruit als heel veel meer-van-hetzelfde, terwijl je juist een pattern zoekt om je code te vereenvoudigen. Je Director daarentegen is heel netjes. Stel dat je een Fluent Interface wilt (nee, niet het Fluent Design waar Microsoft het nu veel over heeft) dan ziet het er ongeveer als volgt uit:

Car car = new CarBuilder()
  .WithDoors(4)
  .WithColor("red")
  .WithABS(true)
  .Build();

(met dank aan Tobiask).
Maar stel nou dat ik het bakken van de objecten generiek en herbruikbaar zou kunnen maken, met een stuk configuratie dat de bakker vertelt wat ie moet maken?
Gelukkig had ik enige tijd geleden een briljante collega (@JelleHissink) die de code die ik zocht in 3 minuten uit zijn hoofd intikte. En vervolgens nog 5 minuten doorratelde over verbeteringen die ik totaal niet kon volgen…

Een generieke Builder

Code first.

public class Builder<T> where T : new()
{
  IList<Action<T>> actions = new List<Action<T>>();
  public T Build ()
  {
    var built = new T ();
    foreach (var action in actions)
    {
      action (built);
    }

    return built;
  }

  public Builder<T> With(Action<T> with)
  {
    actions.Add (with);
    return this;
  }
}

Ik zal eerst uitleggen hoe je ‘m gebruikt, dan hoe het werkt.
De eenvoudigste manier om een Builder te configureren is:

var pet = new Builder<Pet> ()
  .With (p => p.Name = "Dolly")
  .With (p => p.Age = 3)
  .With (p => p.Type = "Cat")
  .Build();

Waarbij de Pet class in het voorbeeld gewoon een class is met 3 publieke velden.
Wat win je daar nou mee? Nou, je hoeft niet een aparte Builder class te maken, dat scheelt. Je moet wel alle namen van de properties die je wilt zetten aangeven, en dat is nou eigenlijk wat we net niet wilden. Maar ik vind het wel beter leesbaar dan een rijtje constructor overloads; en je kunt zelf de volgorde bepalen, velden weglaten en het is duidelijk wat een parameter betekent.

Een voorbeeld van dat laatste. Als je defaults wilt gebruiken kun je het volgende doen:

public class DogBuilder : Builder<Pet>
{
  public DogBuilder ()
  {
    With(a => a.Type = "dog");
  }
}

Een eenvoudige afgeleide class waar je de defaults zet in de constructor. Je kunt dan bij het bakken van je hond zulke code gebruiken:

var dog = new DogBuilder()
  .With(d => d.Name = "Tarzan")
  .With(d => d.Age = 1)
  .Build ();

En omdat je een lambda gebruikt kun je de 2 aanroepen ook nog samenvoegen.

var dog = new DogBuilder()
  .With(d => { d.Name = "Tarzan"; d.Age = 1; })
  .Build ();

Hoe werkt het?

De Builder heeft een Build() methode die het gewenste type T teruggeeft. De methode is verantwoordelijk voor het feitelijke creëren van het object. In regel 6-10 gebeurt dat door het aanroepen van de default constructor, gevolgd door het doorlopen van alle Actions die je hebt gespecificeerd door de With() methode aan te roepen.

Dat is de essentie van het verhaal: je neemt als het ware de acties die je uitgevoerd wilt hebben op, en die worden afgespeeld zodra je Build() aanroept. Niet eerder dan dat je Build() aanroept krijg je echt een object.
Het is een beetje als een verlangslijstje maken: eerst schrijf je op wat je wilt en dan stuur je het naar de Sint om te zorgen dat je het ook allemaal daadwerkelijk krijgt.

Nadelen

Je hebt een default constructor nodig en publieke velden of properties. Dat zou in een Domain Driven Design natuurlijk uit den boze zijn. In DDD vaak een private constructor gemaakt en een static Create() functie. Dat heeft wel iets van een Builder behalve dat je weer een functie krijgt met soms een enorme bak aan parameters.

Voordelen

Separation of concerns. Het mooie van deze oplossing is dat hoe je een object bouwt de verantwoordelijkheid is van de Builder; en dat wat je bouwt de verantwoordelijkheid is van de Director of de afgeleide class. Of zoals Zoran Horvat het zou zeggen: de infrastructuur is losgetrokken van de business logica.

Op dit moment ben ik Builders aan gebruiken om test data te genereren voor een integratie test. Ik bouw een entiteit op met zijn/haar sub-entiteiten en voeg in dat proces van alles toe aan een Entity Framework context. Pas als de Build() wordt aangeroepen doe ik een context.SaveChanges(). Daarmee functioneert mijn Builder in feite als een Unit of Work.