Umiejętność praktycznego korzystania z wzorców projektowych jest czymś, co spłaca się bardzo szybko. Wzorce właśnie po to są – aby rozwiązywać problemy, z którymi się spotykamy na co dzień, bez odkrywania koła na nowo. Spójrzmy zatem na dwa świetnie pracujące ze sobą wzorce – fasady i strategii – w oparciu o dosyć standardowy scenariusz biznesowy.

Weryfikacja założeń biznesowych

W biznesie bardzo często trzeba weryfikować podjęte wcześniej decyzje, próbując dostosować się do panujących wokoło realiów. Każdy mały biznes na początku lubi używać sformułowań typu „tym się nie interesujmy, raczej nigdy tego nie będziemy potrzebować”. O ile w chwili bieżącej biznes ma konkretne potrzeby, to jednak nie powinien on stawiać na słowo „nigdy”. Zwłaszcza, kontaktując się z programistami, którzy tego typu słowa często biorą bardzo poważnie, dostosowując kod do wymagań zawierających owo słowo „nigdy”.

Rośniemy, czyli nigdy nie mów „nigdy”

Przypadek biznesowy, o którym mowa, zaczerpnąłem z doświadczenia związanego z systemami e-commerce. Wyobraźmy sobie, że istnieje sklep o bardzo charakterystycznych i skomplikowanych procesach zakupowych. Jest bardzo innowacyjny i korzysta z niecodziennych rozwiązań. Na etapie projektowania systemu jednym z głównych założeń było stwierdzenie, że dostosowujemy się jedynie do wymagań jednego konkretnego rynku. Projekt przerodził się w implementację, która następnie została wdrożona.

Po pewnym czasie osoby odpowiedzialne za decyzje biznesowe stwierdziły, że fajnie by było objąć rynek znajdujący się na drugiej półkuli globu. Rynek, na którym użytkownicy mają nieco inne przyzwyczajenia, a niektóre z rozwiązań w obecnej aplikacji wydają się jedynie zaszkodzić marce. Jednakże z grubsza system wydaje się być w porządku, zatem pada decyzja, aby w ramach jednej aplikacji znaleźć sposób na to, aby zaspokoić obydwa rynki, tzn. dostosować aplikację do tego, aby część jej zachowań różniła się w zależności od tego, gdzie jest wdrożona.

Gdy decyzja dochodzi do deweloperów…

Jedni zaczynają mówić „mówiłem, by nie robić hardcode”. Inni mówią: „to zróbmy warunki i będzie git”. Inni rzucają słowa niegodne zacytowania, lub rzucają papierami. Teoretycznie prosta decyzja biznesowa przewraca projekt do góry nogami. Jak tu rozwijać X, kiedy dodatkowo będzie powstawało Y? I to w większości na podstawie tego samego kodu. Okazuje się, że jest rozwiązanie na to, aby zrobić to możliwie mało boleśnie.

Ratunkiem jest znajomość wzorców projektowych

W zasadzie całe zadanie, które stoi przed programistami możemy spłycić do sformułowania: „Tutaj zrobimy tak, ale tutaj to samo zrobimy odrobinę inaczej”. Brzmi to, jak główne założenie wzorca Strategia, którego implementacja sprowadza się do driverów, które mają wykonywać teoretycznie to samo zadanie, ale inną drogą. Przykładem strategii, z którym na co dzień się spotykamy jest korzystanie z różnego rodzaju bazy danych za pomocą jednej biblioteki – Doctrine. Gdzieś wewnątrz Doctrine dochodzi do pracy na driverach, które pełnią podobną rolę – umożliwiają komunikację z systemem bazodanowym. Wykorzystując zatem tą wiedzę, możemy spróbować przenieść ją na przypadek, który nas spotkał.

Implementacja wzorca Strategii w języku PHP

Abyśmy mogli próbować podejść do implementacji, potrzeba, abyśmy spróbowali ją zdefiniować. Zacytujmy zatem wszystkim dobrze znaną Wikipedię:

Strategia – czynnościowy wzorzec projektowy, który definiuje rodzinę wymiennych algorytmów i kapsułkuje je w postaci klas. Umożliwia wymienne stosowanie każdego z nich w trakcie działania aplikacji niezależnie od korzystających z nich użytkowników.

Wikipedia

Upewniliśmy się zatem, że dobrze kojarzymy ten wzorzec, zatem możemy przejść przez proces myślowy związany z implementacją.

Naszym zadaniem jest adaptacja istniejącego systemu do pracy na dwóch driverach. W obecnej implementacji możemy założyć więc, że w zasadzie to już korzystamy z jednego drivera. Załóżmy, że założenie biznesowe brzmi: „Promocja dla X ma naliczać się procentowo, ale na Y będzie to rabat kwotowy, do określonej kwoty minimalnej”. Obecna implementacja dla kanału X wygląda następująco:

<?php

namespace App\Promotion;

final class PromotionCalculator implements PromotionCalculatorInterface
{
    public function calculate(int $amount, int $discount): int
    {
        return $amount * $discount / 100;
    }
}

Praca, jaką mamy przed sobą, to:

  1. Zapewnienie, że obecny serwis nie zmieni swojego działania dla kanału X.
  2. Dostarczenie nowych reguł dla kanału Y, w ramach tego samego serwisu.

Utwórzmy zatem dwa drivery:

<?php

namespace App\Promotion\Driver;

final class CalculatorDriverX implements PromotionCalculatorDriverInterface
{
    public function supports(ChannelInterface $channel): bool
    {
        return $channel->is('X');
    }

    public function calculate(int $amount, int $discount): int
    {
        return $amount * $discount / 100;
    }
}

namespace App\Promotion\Driver;

final class CalculatorDriverY implements PromotionCalculatorDriverInterface
{
    private const MINIMAL_AMOUNT = 30;

    public function supports(ChannelInterface $channel): bool
    {
        return $channel->is('Y');
    }

    public function calculate(int $amount, int $discount): int
    {
        $newAmount = $amount - $discount;
        if ($newAmount < self::MINIMAL_AMOUNT) {
            return self::MINIMAL_AMOUNT;
        }

        return $newAmount;
    }
}

Czymś nowym jest dla nas metoda supports(...), która definiuje, kiedy będziemy korzystali z którego drivera.

Następną rzeczą jest zadbanie o to, aby powstałe drivery stały się zależnościami serwisu kalkulatora. Do tego celu możemy skorzystać z dokumentacji Symfony. Rozwiązania z linku jest automatyczne przygotowanie aplikacji na wypadek, gdyby pojawiły się kolejne kanały sprzedażowe. Przy okazji, jesteśmy bardzo zgodni z drugą zasadą SOLID, czyli zasadą „Otwarty-Zamknięty”.

Przyjrzyjmy się naszemu kalkulatorowi po zainstalowaniu kolekcji driverów:

<?php

namespace App\Promotion;

final class PromotionCalculator implements PromotionCalculatorInterface
{
    /** @var Collection<PromotionCalculatorDriverInterface> */
    private Collection $drivers;

    public function __construct(Collection $drivers)
    {
        $this->drivers = $drivers;
    }

    public function calculate(int $amount, int $discount): int
    {
        return $amount * $discount / 100;
    }
}

Ostatnim krokiem jest adaptacja metody calculate(...) do pracy z driverami, w zależnie od kanału sprzedaży:

<?php

namespace App\Promotion;

final class PromotionCalculator implements PromotionCalculatorInterface
{
    /** @var Collection<PromotionCalculatorDriverInterface> */
    private Collection $drivers;

    public function __construct(Collection $drivers)
    {
        $this->drivers = $drivers;
    }

    public function calculate(
        ChannelInterface $channel,
        int $amount,
        int $discount
    ): int {
        foreach ($this->drivers as $driver) {
            if ($driver->supports($channel) {
                return $driver->calculate($amount, $discount);
            }
        }

        throw new UnsupportedSalesChannel($channel->getCode());
    }
}

Tak przygotowany serwis jest gotowy do pracy z kanałami sprzedaży X oraz Y. Dodatkowo, bez ingerencji w istniejące mechanizmy, możemy wprowadzić nowe.

Gdy do gry wchodzą bardziej zaawansowane zachowania

Powyższe rozwiązanie bazuje na poziomie pojedynczego serwisu. Jest bardzo proste w implementacji, o ile mamy do czynienia z pojedynczymi klasami, które reprezentują zachowanie aplikacji.

Jeżeli mamy proces, który opiera się o wzajemną zależność dużej ilości klas, to najlepiej jest skorzystać z drugiego wspomnianego w tytule wzorca: fasady.

Idąc za Wikipedią:

Fasada – wzorzec projektowy należący do grupy wzorców strukturalnych. Służy do ujednolicenia dostępu do złożonego systemu poprzez wystawienie uproszczonego, uporządkowanego interfejsu programistycznego, który ułatwia jego użycie.

Wikipedia

Mówiąc po programistycznemu, to tworzymy jeden serwis, który będzie frontem dla naszej skomplikowanej logiki. Jeżeli gdziekolwiek w kodzie będziemy chcieli wykorzystać tą logikę, to tylko przez użycie wzorca fasady. Nikt z zewnątrz (oprócz fasady) nie powinien mieć dostępu do klas wewnętrznych.

To rozwiązanie w połączeniu z wzorcem strategii ma taką zaletę, że jako drivera implementacyjnego strategii możemy wykorzystać właśnie klasę fasady – pomijając całą zabawę z ustawianiem strategii dla klas znanych tylko tej fasadzie.

Dążąc do podsumowania…

Po przeczytaniu niniejszego wpisu możemy odnieść wrażenie, że całe rozwiązanie jest dosyć trywialne. I w zasadzie takie właśnie ono jest, pod warunkiem, że wpadniemy na nie w trakcie burzy deweloperskiej po otrzymaniu nowego biznesowego pomysłu. Dlatego właśnie zachęcam do poznawania konceptów, jakimi są wzorce projektowe, które za zadanie mają nam służyć 🙂

Profesjonalista, zajmujący się na co dzień aplikacjami biznesowymi w ekosystemie PHP. Jego pasją jest odkrywanie nowych konceptów programistycznych oraz wzorce architektoniczne. Uwielbia również pisać testy, gdyż jak sam uważa, dobry kod to przetestowany kod.

Comments are closed.