Przyjrzyjmy się wzorcowi projektowemu o nazwie Metoda Szablonowa (ang. Template Method). Poniższy kod ilustruje działanie stacji benzynowej z dwoma dystrybutorami paliwowymi o różnym rodzaju paliwa, ale działającymi na podobnej zasadzie. W tym przykładzie klasa FuelDispenser
reprezentuje ogólny proces obsługi dystrybutora, natomiast klasy DieselDispenser
i PetrolDispenser
są klasami pochodnymi, które definiują specyficzne implementacje dla poszczególnych rodzajów paliw.
class FuelDispenser
def initialize
@pump_liters = 0
end
def dispense
raise NotImplementedError
end
def pump_liters
@pump_liters
end
end
class DieselDispenser < FuelDispenser
def initialize
super
end
def dispense(liters)
@pump_liters += liters
puts "Diesel dispensed: #{liters} liters"
end
end
class PetrolDispenser < FuelDispenser
def initialize
super
end
def dispense(liters)
@pump_liters += liters
puts "Petrol dispensed: #{liters} liters"
end
end
Zastosowanie wzorca Metoda Szablonowa polega na tym, że proces obsługi dystrybutora jest opisany w klasie bazowej FuelDispenser
, a konkretne implementacje dla różnych rodzajów paliw są opisane w klasach pochodnych DieselDispenser
i PetrolDispenser
. Dzięki temu, jeśli pojawi się konieczność zmiany w procesie obsługi dystrybutora, można to zrobić w jednym miejscu (w klasie bazowej) i zmiana ta automatycznie zostanie zastosowana we wszystkich klasach pochodnych. Wzorzec Metoda Szablonowa pozwala na trzymanie podobnych kawałków kodu razem i umożliwia stosowanie DRY (Don’t Repeat Yourself) – unikanie powtarzającego się kodu.
Refaktoring
Ten wzorzec pozwala też na rozszerzanie działania klas bez modyfikowanie istniejącej kodu, daje jasność i przejrzystość w hierarchii klas. Poniżej przedstawiam przykład kodu po refaktoryzacji z użyciem wzorca Metoda Szablonowa:
class FuelDispenser
def initialize
@pump_liters = 0
end
def dispense(liters)
fuel_dispensed(liters)
@pump_liters += liters
puts "Fuel dispensed: #{liters} liters"
end
def pump_liters
@pump_liters
end
def fuel_dispensed(liters)
raise NotImplementedError
end
end
class DieselDispenser < FuelDispenser
def fuel_dispensed(liters)
puts "Diesel dispensed: #{liters} liters"
end
end
class PetrolDispenser < FuelDispenser
def fuel_dispensed(liters)
puts "Petrol dispensed: #{liters} liters"
end
end
W powyższym przykładzie, proces obsługi dystrybutora jest opisany w klasie bazowej FuelDispenser
, a konkretne implementacje dla różnych rodzajów paliw są opisane w klasach pochodnych DieselDispenser
i PetrolDispenser
poprzez przesłonięcie metody fuel_dispensed
. Dzięki temu, jeśli pojawi się konieczność zmiany w procesie obsługi dystrybutora, można to zrobić w jednym miejscu (w klasie bazowej) i zmiana ta automatycznie zostanie zastosowana we wszystkich klasach pochodnych.
Testy
Poniżej przedstawiam przykład testów jednostkowych dla kodu po refaktoryzacji z użyciem wzorca Metoda Szablonowa i korzystających z biblioteki Minitest:
require 'minitest/autorun'
class TestFuelDispenser < Minitest::Test
def setup
@diesel_dispenser = DieselDispenser.new
@petrol_dispenser = PetrolDispenser.new
end
def test_dispense_diesel
expected_output = "Diesel dispensed: 20.0 liters\nFuel dispensed: 20.0 liters\n"
assert_output(expected_output) { @diesel_dispenser.dispense(20.0) }
assert_equal 20.0, @diesel_dispenser.pump_liters
end
def test_dispense_petrol
expected_output = "Petrol dispensed: 15.0 liters\nFuel dispensed: 15.0 liters\n"
assert_output(expected_output) { @petrol_dispenser.dispense(15.0) }
assert_equal 15.0, @petrol_dispenser.pump_liters
end
end