Dzisiaj dla zabawy zrobiłem zadanie z wykorzystania wzorca projektowego Strategii. Lubię go i uważam, że “bawienie się we wzorce” dobrze robi na mózg. Dlatego pokusiłem się o spytanie Czata GPT o zadanie, które następnie sam mogę sobie rozwiązać, a potem podzielić się z nim przez YouTube. Na razie piszę post, a filmik przedstawię później.
Zadanie
Daj mi proszę krótkie zadanie programistyczne, które testowałoby znajomość wzorca projektowego strategy, w temacie restauracji. Nie wspominaj o tym wzorcu, tylko tak stwórz zadanie, żeby uczeń sam musiał się domyślić, że chodzi o wzorzec strategy.
GPT: Napisz program, który będzie symulował działanie restauracji. Restauracja ma różne metody płatności (gotówka, karta, aplikacja mobilna) oraz różne metody dostawy (odbiór osobisty, dostawa do domu, dostawa na wynos). Użytkownik programu powinien mieć możliwość wyboru sposobu płatności i dostawy, a następnie otrzymać odpowiedni koszt zamówienia. Należy zadbać o to, aby kod był czytelny i łatwo rozszerzalny w przypadku dodania nowych sposobów płatności lub dostawy.
Jak patrzeć na takie zadanie? Przecież nie o to w nim chodzi, aby najpierw zakodować jakąś kiepską wersję, którą potem trzeba refaktoryzować. Zadanie samo już podpowiada, że kod powinien być łatwo rozszerzalny, co jest efektem ubocznym jednej z zasad SOLID, a mianowicie Open/Closed Principle, gdzie obiekt powinien być otwarty zmiany, ale zamknięty na modyfikację.
Kiedy mamy do czynienia z taką restauracją, która ma jakieś metody płatności czy sposoby dostawy, to patrzymy na nie jako zewnętrzne wobec tejże restauracji. Że to dodatki, inna domena, coś, co nie jest sprzężone ani zależne od niej.
Żeby lepiej zrozumieć takie zadanie, trzeba też sobie wyobrazić, że każda z tych dodatkowych usług (jak strategia płatności czy dostawy) ma swoją logikę. Tutaj kłania się pierwsza zasada SOLID, czyli Single Responsibility Principle, gdzie jeśli zmieni się coś w logice np. CardPayment
, to zmiana zajdzie tylko tam, a nie w innych klasach, a już tym bardziej nie w klasie Restaurant
.
Rozwiązanie
A oto kod, który napisałem korzystając z platformy replit.com, z której nieraz korzystam z uczniami:
class Restaurant
attr_reader :payment_strategy, :delivery_strategy
def initialize(payment_strategy, delivery_strategy)
@payment_strategy = payment_strategy
@delivery_strategy = delivery_strategy
end
def calculate_order(amount)
amount +
payment_strategy.fee(amount) +
delivery_strategy.fee(amount)
end
end
class CashPayment; def fee(amount); amount > 100 ? 1 : 0.5; end; end
class CardPayment; def fee(amount); amount > 200 ? 2 : 0.15; end; end
class AppPayment; def fee(amount); amount > 300 ? 3 : 0.15; end; end
class SelfDelivery; def fee(amount); 0; end; end
class GoToDelivery; def fee(amount); amount > 50 ? 0 : 10; end; end
class HomeDelivery; def fee(amount); amount > 200 ? 0 : 20; end; end
r = Restaurant.new(CashPayment.new, HomeDelivery.new)
puts r.calculate_order(1000)
Oczywiście, można by tu jeszcze dopisać dwie abstrakcyjne klasy, jak PaymentStrategy
oraz DeliveryStrategy
, które miałyby niezaimplementowaną metodę fee
, a po której pozostałe klasy odpowiednio by dziedziczyły. W tym przykładzie jest to jednak zbędne, ze względu na jego prostotę.
Sprawdzenie w GPT
Po zabawie w Replit, wkleiłem kod do Czata i spytałem go, jak mi poszło.
GPT: Kod przedstawiony w pytaniu jest poprawnym rozwiązaniem zadania, które zaproponowałem. Jest zgodny z wzorcem projektowym Strategy. Klasa Restaurant
jest klasą klienta, która korzysta z różnych strategii płatności i dostawy, które są dostarczane przez klasy CashPayment
, CardPayment
, AppPayment
, SelfDelivery
, GoToDelivery
, HomeDelivery
. Te klasy implementują interfejs strategii, który jest reprezentowany przez metodę fee
, która jest wykorzystywana przez klasę Restaurant
do obliczenia kosztu zamówienia.
Kod jest czytelny i łatwo rozszerzalny, ponieważ jeśli koniecznym będzie dodać nowy sposób płatności lub dostawy, wystarczy dodać nową klasę i dostarczyć ją do klasy Restaurant
przy tworzeniu obiektu. W ten sposób nie będzie konieczna modyfikacja istniejącego kodu.
Testy
Na koniec kilka wygenerowanych testów. Jakkolwiek Chat GPT jest super, musiałem je po nim poprawić, bo źle wstawiał wartości.
require 'minitest/autorun'
class RestaurantTest < Minitest::Test
def test_cash_payment_order
r = Restaurant.new(CashPayment.new, SelfDelivery.new)
assert_equal 100.5, r.calculate_order(100)
end
def test_card_payment_order
r = Restaurant.new(CardPayment.new, GoToDelivery.new)
assert_equal 200.15, r.calculate_order(200)
end
def test_app_payment_order
r = Restaurant.new(AppPayment.new, HomeDelivery.new)
assert_equal 210.15, r.calculate_order(190)
end
end