Wzorzec projektowy – Stan

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

Leave a Reply

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