Wzorzec projektowy – Budowniczy

Wzorzec projektowy o nazwie Budowniczy (ang. Builder) pozwala na tworzenie skomplikowanych obiektów poprzez krok po kroku ustawianie ich właściwości. W tym przypadku, przedstawię Ci przykład kodu, w którym stacja benzynowa ma kilka właściwości, a następnie zastosuję wzorzec Budowniczego do refaktoryzacji tego kodu. Kod przed refaktoryzacją:

class GasStation
    attr_accessor :gas_price, :location, :is_open_24h, :has_car_wash
    
    def initialize(gas_price, location, is_open_24h, has_car_wash)
        @gas_price = gas_price
        @location = location
        @is_open_24h = is_open_24h
        @has_car_wash = has_car_wash
    end
end

station = GasStation.new(3.50, "New York", true, false)

Ten kod tworzy klasę GasStation, która ma 4 atrybuty: cena paliwa, lokalizacja, czy jest otwarta 24h oraz czy ma myjnię samochodową. Problem polega na tym, że w momencie tworzenia obiektu klasy GasStation trzeba podać wszystkie te atrybuty, co może być trudne i nieintuicyjne, a czasami niemożliwe, ponieważ możemy dowiadywać się o danych wartościach atrybutów dopiero na późniejszym etapie pracy z obiektem. Na tym etapie nie możemy też manipulować wartościami, które są w konstruktorze, bez wyraźnej zmiany w nim.

Refaktoring

Oto przykład kodu po refaktoryzacji:

class GasStation
    attr_accessor :gas_price, :location, :is_open_24h, :has_car_wash

    def initialize(builder)
        @gas_price = builder.gas_price
        @location = builder.location
        @is_open_24h = builder.is_open_24h
        @has_car_wash = builder.has_car_wash
    end

    class Builder
        attr_accessor :gas_price, :location, :is_open_24h, :has_car_wash

        def initialize
            @gas_price = 3.50
            @location = "New York"
            @is_open_24h = false
            @has_car_wash = false
        end

        def set_gas_price(price)
            @gas_price = price
            return self
        end

        def set_location(loc)
            @location = loc
            return self
        end

        def set_open_24h(is_open)
            @is_open_24h = is_open
            return self
        end

        def set_car_wash(has_car_wash)
            @has_car_wash = has_car_wash
            return self
        end

        def build
            GasStation.new(self)
        end
    end
end

station = GasStation::Builder.new
              .set_gas_price(4.00)
              .set_location("Los Angeles")
              .set_open_24h(true)
              .set_car_wash(true)
              .build

Jak widać, kod teraz tworzy klasę Builder wewnątrz klasy GasStation, która umożliwia ustawianie poszczególnych właściwości obiektu GasStation poprzez metody set_gas_price, set_location, set_open_24h, set_car_wash. Builder umożliwia również ustawienie domyślnych wartości dla tych właściwości. Dzięki temu, tworzenie obiektu klasy GasStation jest łatwiejsze i bardziej intuicyjne.

Owszem, kod po refaktoryzacji jest trochę dłuższy niż przed refaktoryzacją. Z drugiej strony, wzorzec Budowniczego daje więcej elastyczności i przejrzystości kodu, szczególnie jeśli chodzi o konstrukcję skomplikowanych obiektów. Przykładowo, w przypadku, gdyby konieczne było dodać nowe właściwości do klasy GasStation, np. czy ma restaurację, wystarczyłoby dodać jedną metodę set_restaurant do klasy Builder.

Przy takim prostym przykładzie jak stacja benzynowa, attr_accessor jest wystarczającym rozwiązaniem. Jednak w przypadku bardziej skomplikowanych klas, gdzie obiekt ma wiele różnych właściwości, różne warunki w jakich te właściwości mogą być ustawiane, a także różne działania jakie chcemy wykonać podczas ich ustawiania, wzorzec Budowniczego jest bardziej odpowiedni.

Warto też dodać, że dzięki takiemu podejściu możemy mieć różne klasy Budowniczych, dostosowane do sytuacji. Tym samym klasa GasStation nie jest zależna od danego sposobu budowania jej instancji, który w prostym rozwiązaniu przed refaktoryzacją jest z nim sztywno związany.

Testy

Na koniec zobaczmy jeszcze jak wygląda testowanie obiektu w stylu Budowniczego:

class GasStationTest < Minitest::Test
    def test_gas_station_builder
        station = GasStation::Builder.new
                      .set_gas_price(4.00)
                      .set_location("Los Angeles")
                      .set_open_24h(true)
                      .set_car_wash(true)
                      .build
        assert_equal 4.00, station.gas_price
        assert_equal "Los Angeles", station.location
        assert_equal true, station.is_open_24h
        assert_equal true, station.has_car_wash
    end

    def test_default_values
        station = GasStation::Builder.new.build
        assert_equal 3.50, station.gas_price
        assert_equal "New York", station.location
        assert_equal false, station.is_open_24h
        assert_equal false, station.has_car_wash
    end
end

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *