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