Przyjrzyjmy się dzisiaj trochę dłuższemu przykładowi związanemu ze wzorcem Stan (ang. State). Poniżej znajduje się przykład kodu Ruby, który reprezentuje stację benzynową, a który wymaga refaktoryzacji za pomocą wzorca projektowego Stan:
class GasStation
attr_reader :state
def initialize
@state = :waiting_for_fuel
end
def open
if @state == :waiting_for_fuel
puts "Cannot open, waiting for fuel."
elsif @state == :closed
@state = :open
puts "Gas station is now open."
else
puts "Gas station is already open."
end
end
def close
if @state == :waiting_for_fuel
puts "Cannot close, waiting for fuel."
elsif @state == :open
@state = :closed
puts "Gas station is now closed."
else
puts "Gas station is already closed."
end
end
def fuel_delivered
@state = :closed
puts "Fuel has been delivered, gas station is now closed."
end
end
Ten kod jest trudny do utrzymania, ponieważ logika dotycząca stanu stacji benzynowej jest rozproszona w różnych metodach. W przypadku dodania nowego stanu lub zmiany logiki dla istniejącego stanu, trzeba będzie dokonać zmian w kilku miejscach w kodzie.
Aby uprościć kod i ułatwić jego rozszerzanie, można użyć wzorca projektowego Stan. Wzorzec ten polega na tym, że każdy stan jest reprezentowany przez osobny obiekt, który zawiera logikę dotyczącą tego stanu. W tym przypadku, logika dotycząca każdego stanu stacji benzynowej zostanie wydzielona do osobnej klasy.
Refaktoring
Oto przykład kodu po refaktoryzacji z użyciem wzorca projektowego Stan. Najpierw stacja benzynowa:
class GasStation
attr_reader :state
def initialize
@state = WaitingForFuelState.new(self)
end
def open
@state.open
end
def close
@state.close
end
def fuel_delivered
@state.fuel_delivered
end
def set_state(new_state)
@state = new_state
end
end
Potem jej przykładowe stany (zwykle trzymane w osobnych plikach):
class WaitingForFuelState
def initialize(gas_station)
@gas_station = gas_station
end
def open
puts "Cannot open, waiting for fuel."
end
def close
puts "Cannot close, waiting for fuel."
end
def fuel_delivered
@gas_station.set_state(ClosedState.new(@gas_station))
puts "Fuel has been delivered, gas station is now closed."
end
end
class ClosedState
def initialize(gas_station)
@gas_station = gas_station
end
def open
@gas_station.set_state(OpenState.new(@gas_station))
puts "Gas station is now open."
end
def close
puts "Gas station is already closed."
end
def fuel_delivered
puts "Fuel has already been delivered."
end
end
class OpenState
def initialize(gas_station)
@gas_station = gas_station
end
def open
puts "Gas station is already open."
end
def close
@gas_station.set_state(ClosedState.new(@gas_station))
puts "Gas station is now closed."
end
def fuel_delivered
puts "Fuel cannot be delivered, gas station is open."
end
end
Wzorzec Stan pozwala na lepsze oddzielenie logiki dotyczącej poszczególnych stanów stacji benzynowej, dzięki czemu jest to łatwiejsze do rozszerzania i utrzymania. W przypadku dodania nowego stanu lub zmiany logiki dla istniejącego stanu, trzeba będzie tylko utworzyć nową klasę i zmienić logikę w tej klasie, zamiast robić zmiany w kilku różnych metodach.
Przykładowo dodanie nowego stanu i nowej klasy związanej z tym stanem może wymagać zmiany w pozostałych klasach, ale zmiana ta byłaby ograniczona tylko do klas, które odpowiadają za przejście między różnymi stanami. Dodanie nowego stanu wymagałoby utworzenia nowej klasy, która będzie reprezentować ten stan, oraz zmiany w klasach, które odpowiadają za przejście do tego stanu, oraz z jego powrotem do innego stanu.
Dzięki wzorcowi Stan, logika związana z działaniem poszczególnych stanów jest rozdzielona, co pozwala na łatwiejsze rozszerzanie i utrzymanie aplikacji.
Testy
Poniższe testy sprawdzają, że stacja benzynowa jest tworzona z odpowiednim stanem, nie może być otwarta, kiedy oczekuje na paliwo, może otrzymać paliwo i po tym nie może być otwarta, może być otwarta i zamknięta po otrzymaniu paliwa.
require "minitest/autorun"
class GasStationTest < Minitest::Test
def test_gas_station_is_waiting_for_fuel_when_created
gas_station = GasStation.new
assert_equal WaitingForFuelState, gas_station.state.class
end
def test_gas_station_cannot_be_opened_when_waiting_for_fuel
gas_station = GasStation.new
gas_station.open
assert_equal WaitingForFuelState, gas_station.state.class
end
def test_gas_station_can_receive_fuel_when_waiting_for_fuel
gas_station = GasStation.new
gas_station.fuel_delivered
assert_equal ClosedState, gas_station.state.class
end
def test_gas_station_can_be_opened_when_closed
gas_station = GasStation.new
gas_station.fuel_delivered
gas_station.open
assert_equal OpenState, gas_station.state.class
end
def test_gas_station_can_be_closed_when_open
gas_station = GasStation.new
gas_station.fuel_delivered
gas_station.open
gas_station.close
assert_equal ClosedState, gas_station.state.class
end
end