Przejdź do głównej zawartości

[OOP] Na czym polega wzorzec projektowy Budowniczy (Fluent API)?

Wzorzec Budowniczy w wersji "Fluent API" (ang. Fluent Builder) pozwala nam tworzyć instancje klas w sposób bardziej precyzyjny i czytelny niż za pomocą konstruktora. Rozważmy klasę reprezentującą drużynę piłkarską: 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
namespace Builder
{
    internal class Team
    {
        public Team(string name, string league, string shirtColor, string nickname)
        {
            Name = name;
            League = league;
            ShirtColor = shirtColor;
            Nickname = nickname;
        }

        public string Name { get; private set; }
        public string League { get; private set; }
        public string ShirtColor { get; private set; }
        public string Nickname { get; private set; }

        public override string ToString()
        {
            return $"{Name} z {League}, kolor koszulek: {ShirtColor}, pseudonim: {Nickname}";
        }
    }
}
 
Przyjrzyjmy się konstruktorowi. Przyjmuje on cztery parametry - wszytskie typu string. Tworząc obiekty tej klasy nietrudno będzie o pomyłkę:

var realMadrid = new Team("Real Madryt", "Primera División", "Biały", "Los Blancos");
var fcBarcelona = new Team("FC Barcelona", "Primera División", "Blaugrana", "Bordowo-granatowy");

W tym przypadku nieuważny programista sprawił, że FC Barcelona na pseudonim "Bordowo-granatowy", zaś kolor jej koszulek to "Blaugrana". Wszytsko się zgadza na poziomie konwersji z polskiego na hiszpański i vice versa, jest to jednak nieco mylące. Pomyłka ta jest łatwa do wykonania, lecz o wiele trudniejsza do wypatrzenia. Widząc długi ciąg stringów bez żadnego opisu mówiącego który string mapuje się na który property, ciężko dojrzeć błąd. Wygląda na to, że przydała by się nam alternatywna metoda budowania obiektów klasy Team. Taka, w której czarno na białym będzie widać, który parametr dotyczy danej cechy zespołu. Spróbujmy zaimplementować wzorzec Fluent Builder. Zanim to jednak zrobimy, wyjaśnijmy sobie, co właściwie oznacza określenie "Fluent API". W skrócie oznacza to taki kod, który możemy pisać w formie łańcucha wywołań:

new FluentClass().Working("łatwo i przyjemnie").Because("tak jest wygodniej").MakingCode("bardziej czytelnym");

Fluent API używamy np. korzystając z LINQ: 

var animals = new List<string> { "Zebra", "Aligator", "Lew" };
var orderedAnimalsWithLongNames = animals.Where(a => a.Length > 3).OrderBy(a => a);

Wróćmy do Fluent Buildera. Isnieją dwa podstawowe sposoby implementacji tego wzorca.

Sposób pierwszy

Sposób ten polega na stworzeniu klasy Buildera, zawierającej pola analogiczne do pól budowanego obiektu:
 
namespace Builder
{
    internal class TeamBuilder
    {
        private string _name;
        private string _league;
        private string _shirtColor;
        private string _nickname;

        public TeamBuilder()
        {
        }

        internal TeamBuilder Named(string name)
        {
            _name = name;
            return this;
        }

        internal TeamBuilder FromLeague(string league)
        {
            _league = league;
            return this;
        }

        internal TeamBuilder WithNickname(string nickname)
        {
            _nickname = nickname;
            return this;
        }

        internal TeamBuilder WithShirtColor(string color)
        {
            _shirtColor = color;
            return this;
        }

        internal Team Build()
        {
            return new Team(_name, _league, _shirtColor, _nickname);
        }
    }
}
 
Użycie takiego Buildera jest znacznie bardziej czytelne niż użycie konstruktora:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using System;

namespace Builder
{
    class Program
    {
        static void Main(string[] args)
        {
            var teamBuilder = new TeamBuilder();

            var realMadrid = teamBuilder.Named("Real Madryt").WithNickname("Los Blancos").FromLeague("Primera División").WithShirtColor("Biały").Build();
            var juventus = teamBuilder.Named("Juventus Turyn").WithNickname("Stara Dama").FromLeague("Serie A").WithShirtColor("Czarno-biały").Build();

            Console.WriteLine(realMadrid);
            Console.WriteLine(juventus);
            Console.ReadKey();
        }
    }
}

Zalety tej implementacji:
  • Separujemy logikę budowania obiektu od samego obiektu
Wady tej implementacji:
  • Builder posiada te same pola co klasa budowana - jest to złamanie zasady DRY (Don't Repeat Yourself - "nie powtarzaj się")
  • Aby zatwierdzić budowanie obiektu musimy zawołać metodę "Build". Jest to kolejna rzecz, o której łatwo zapomnieć

Sposób drugi

W implementacji tej dodajemy metody ustawiające pola bezpośrednio w klasę, którą chcemy budować.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
namespace Builder
{
    internal class Team
    {
        public string Name { get; private set; }
        public string League { get; private set; }
        public string ShirtColor { get; private set; }
        public string Nickname { get; private set; }

        public override string ToString()
        {
            return $"{Name} z {League}, kolor koszulek: {ShirtColor}, pseudonim: {Nickname}";
        }

        internal Team Named(string name)
        {
            Name = name;
            return this;
        }

        internal Team FromLeague(string league)
        {
            League = league;
            return this;
        }

        internal Team WithNickname(string nickname)
        {
            Nickname = nickname;
            return this;
        }

        internal Team WithShirtColor(string color)
        {
            ShirtColor = color;
            return this;
        }
    }
}

Zalety tej implementacji:
  • samo budowanie obiektu jest prostsze - nie korzystamy z pośrednika jakim był TeamBuilder, nie musimy pamiętać o zawołaniu na koniec metody Build
Wady tej implementacji:
  • funkcje budujące nie są wiele bardziej czytelne, niż po prostu publiczne properties, a zajmują więcej miejsca i zaśmiecają klasę
Istnieją jeszcze oczywiście inne implementacje, jednak w większości bazują na tych dwóch schematach.

Podsumowanie

Na koniec zastanówmy się jakie są wady i zalety tego wzorca. Istnieją zarówno argumenty za, jak i przeciwko używaniu go - wszytsko zależy od konkretnej sytuacji. Zalety:
  • dzięki fluent API mamy bardziej czytelne i opisowe budowanie obiektów, oraz mniejszą szansę na pomyłkę argumentów
  • gdybyśmy chcieli ręcznie ustawiać properties i jakoś przeformatować argument przed przypisaniem (np. użyć funkcji Trim dla stringa) musielibyśmy to robić za każdym razem, gdy budujemy nowy obiekt. Używając Buildera wystarczy tak przerobić metodę ustawiającą wartość, by zawsze odpowiednio formatowała argumenty
Wady tego wzorca:
  • Konstruktor klasy budowanej wciąż jest publiczny - inny programista może nawet nie wiedzieć, że nasz Builder istnieje, i go nie używać
  • Nikt nas nie zmusi do zawołania każdej metody ustawiającej property. Konstruktor faktycznie wymuszał przekazanie mu danej ilości parametrów
Kod z tym wzorcem, jak i z innymi, można pobrać tutaj [under construction]. W poście tym użyłam:
  • http://www.stefanoricciardi.com/2010/04/14/a-fluent-builder-in-c/
  • https://stackoverflow.com/questions/41047524/how-to-implement-fluent-builder-with-inheritance-in-java

Komentarze

  1. Dobre opracowanie. Dodałbym, że fajną i wbudowaną w język alternatywą jest po prostu ustawianie wartości za pomocą właściwości przy wywołaniu konstruktora. Niestety wymaga to, by settery właściwości nie były prywatne, no i (tak jak w przypadku FluentBuildera) nie pozwala nam to wymuszać podania niektórych argumentów.

    OdpowiedzUsuń

Prześlij komentarz