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.