Wzorzec projektowy – Builder
Przeznaczenie
Oddziela tworzenie złożonego obiektu od jego reprezentacji, dzięki czemu ten sam proces konstrukcji może prowadzić do różnych reprezentacji. Dodatkowo enkapsulujemy tworzenie obiektu i możemy wielokrotnie użyć go w wielu miejscach.
Motywacja
- jeżeli algorytm tworzenia obiektu powinien być niezależny od składników tego obiektu
- kiedy proces konstrukcji musi umożliwiać tworzenie różnych reprezentacji generowanego obiektu
- kiedy tworzenie obiektu jest bardzo skomplikowane
Konsekwencje:
- możliwość modyfikowania wewnętrznej reprezentacji obiektu
- oddzielenie kodu prezentacji obiektu od kodu odpowiedzialnego za tworzenie obiektu
- kontrola nad procesem tworzenia
Przykład – podstawowy
Mamy takie klasy Person i Address:
public class Person
{
public string Name;
public int Age;
}
Na początku stwórzmy klasę z polem prywatnym:
public class PersonBuilder
{
private readonly Person _person = new Person();
}
Teraz musimy jakoś umożliwić ustawianie obiektu Person:
public class PersonBuilder
{
private readonly Person _person = new Person();
// ustawiamy imię
public void Called(string name)
{
_person.Name = name;
}
// ustawiamy wiek
public void AtAge(int age)
{
_person.Age = age;
}
}
Finalnie potrzebujemy metody, która zwróci nam zbudowany obiekt:
public class PersonBuilder
{
private readonly Person _person = new Person();
public void Called(string name)
{
_person.Name = name;
}
public void AtAge(int age)
{
_person.Age = age;
}
// zwracamy zbudowany obiekt
// dodatkowo, możemy sprawdzić tu czy został zbudowany poprawnie
public Person Build()
{
return _person;
}
}
Użycie:
var personBuilder = new PersonBuilder();
personBuilder.Called("Adam");
personBuilder.AtAge(10);
var person1 = personBuilder.Build();
Fluent Builder
Niewielkim nakładem sił możemy ułatwić, użycie naszego budowniczego. Wystarczy, że zmienimy typ zwracany z void na typ buildera:
public class FluentPersonBuilder
{
private readonly Person _person = new Person();
public FluentPersonBuilder Called(string name)
{
_person.Name = name;
return this;
}
public FluentPersonBuilder AtAge(int age)
{
_person.Age = age;
return this;
}
public Person Build()
{
return _person;
}
}
I używamy jego w taki sposób:
var fluentPersonBuilder = new FluentPersonBuilder();
var person2 = fluentPersonBuilder
.Called("Adam")
.AtAge(10)
.Build();
Jeżeli potrzebujemy zbudować obiekt w odpowiedniej kolejności?
Wystarczy zastosować segregację interfejsów. Załóżmy dla przykładu, że potrzebujemy zbudować obiekt Car, który ma ma ograniczenie że nie można stworzyć takiego obiektu z hamulcami typu Eco kiedy moc silnika przekracza 100 koni mechanicznych. Tak prezentuje się klasa Car i enum Brakes:
public enum Brakes
{
Eco,
Standard,
Sport
}
public class Car
{
public int HorsePower;
public Brakes Brakes;
}
Najpierw musimy zadbać o segregację interfejsów, stwórzmy je więc:
// najpierw będziemy ustawiać silnik
// (bez tego nie będziemy mogli sprawdzić jego mocy)
public interface ISpecifyEngine
{
ISpecifyBrakes WithEngine(int hp);
}
// następnie sprawdzamy hamulce
public interface ISpecifyBrakes
{
ICarBuilder WithBrakes(Brakes brakes);
}
// na końcu zwracamy gotowy obiekt Car
public interface ICarBuilder
{
Car Build();
}
Zaimplementujmy wszystkie interfejsy:
public class CarBuilder : ISpecifyEngine, ISpecifyBrakes, ICarBuilder
{
private readonly Car _car = new Car();
// prywatny konstruktor zabezpiecza nas przed tym, że możemy użyć tylko metody wytwórczej Create
// dzięki temu ograniczamy wybór metod do tych dostępnych w interfejsie ISpecifyEngine
private CarBuilder()
{
}
public static ISpecifyEngine Create()
{
return new CarBuilder();
}
public ISpecifyBrakes WithEngine(int hp)
{
_car.HorsePower = hp;
return this;
}
// logika ograniczenia wyboru hamulców
public ICarBuilder WithBrakes(Brakes brakes)
{
var horsePowers = _car.HorsePower;
switch (brakes)
{
case Brakes.Eco when horsePowers > 100:
case Brakes.Standard when horsePowers > 150:
throw new ArgumentException($"Wrong brakes for {horsePowers}HP engine");
}
_car.Brakes = brakes;
return this;
}
// zwracamy zbudowany obiekt
public Car Build()
{
return _car;
}
}
// tak jak w przypadku fluent buildera możemy dodać metodę pomocniczą New()
public class Car
{
public int HorsePower;
public Brakes Brakes;
public static ISpecifyEngine New()
{
return CarBuilder.Create();
}
}
Użycie:
var car = Car.New() // ISpecifyEngine
.WithEngine(100) // ISpecifyBrakes
.WithBrakes(Brakes.Sport) // ICarBuilder
.Build(); // Car
Dodatkowo w repozytoirum jest przykład jak zaimplementować Builder skladający się z wielu builderów w oparciu o wzorzec fasady.