Wzorzec projektowy – Metoda Szablonowa

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

Leave a Reply

Your email address will not be published. Required fields are marked *