Wzorce projektowe

Autor Autor:
Zespół Innokrea
Data publikacji: 2024-10-14
Kategorie: Programowanie

Hej, dzisiaj jako Innokrea opowiemy Wam o tym czym są wzorce projektowe i dlaczego każdy programista powinien mieć je w swojej wirtualnej skrzynce deweloperskich narzędzi. Jeśli jesteście zainteresowani, zapraszamy do lektury, a jeśli nie mieliście jeszcze okazji zapoznać się z naszymi wcześniejszymi artykułami dotyczącymi zasad SOLID, gorąco zachęcamy, ponieważ są one ściśle powiązane z tematem wzorców projektowych.

 

Czym są wzorce projektowe?

Wzorce projektowe to sprawdzone rozwiązania często występujących w programowaniu problemów. Dotyczą one przede wszystkim tego jakie klasy deweloper powinien stworzyć w celu rozwiązania danego problemu (inaczej, jak rozdzielić odpowiedzialność pomiędzy komponenty) oraz jak je połączyć ze sobą. Dzięki zastosowaniu wzorców sprawiamy, że nasz kod jest zgodny z zasadami SOLID i łatwo go rozszerzać o dodatkowe funkcjonalności. Wzorce zachęcają dewelopera do pisania zgodnie z zasadami SOLID. Bez wzorców pisanie dużych systemów jest trudniejsze ze względu na rosnącą złożoność zależności między komponentami. W praktyce oznacza to, że pojedyncza zmiana w kodzie może wpływać na komponenty, na które nie powinna. Rozwijanie takiego kodu stają się bardzo trudne, czasochłonne i przede wszystkim kosztowne. Zwiększamy wtedy ryzyko wprowadzania błędów i dług technologiczny.

Wzorce projektowe nie tylko upraszczają rozwój oprogramowania, ale także usprawniają współpracę między programistami. Jeżeli każdy członek zespołu zna wzorce, które można zastosować do rozwiązywania często występujących problemów, komunikacja w zespole i praca nad projektem stają się bardziej efektywne.

 

Rodzaje wzorców projektowych

Istnieją dziesiątki wzorców projektowych adresujących różne problemy programistyczne, ale podstawowo wyróżniamy trzy kategorie:

  • wzorce kreacyjne – ułatwiają one tworzenie obiektów i oddzielają proces tworzenia obiektów od ich użycia. Przykładami mogą tu być takie rozwiązania jak singleton, factory method czy builder. Można je porównać do procesu produkcji – czasem warto skorzystać z fabryki, a innym razem tworzyć produkty indywidualnie, dostosowując każdy element.
  • wzorce strukturalne – określają sposoby łączenia klas, aby możliwe było ich łatwe rozszerzane. Przykładami mogą tu być wzorce adapter czy dekorator. To jak stosowanie przejściówek do gniazdek w obcym kraju lub upraszczanie procesów hotelowych przez wyznaczoną osobę, która kontaktuje się z odpowiednimi usługami w imieniu klienta.
  • wzorce behawioralne – opisują przede wszystkim interakcje pomiędzy różnymi obiektami – to jak się komunikują i współpracują, aby zrealizować konkretne zadanie. Przykładem mogą być tutaj wzorce obserwator czy strategia. Tego rodzaju rozwiązania można porównać do subskrybcji newslettera, albo do możliwości wyboru sposobu (strategii) dostania się do biura np. rowerem czy samochodem.

Istnieją także inne rodzaje wzorców projektowych takie jak: dotyczące architektury czy programowania wielowątkowego.

 

Implementacja – Strategy

Spróbujmy zaimplementować jeden ze wzorców behawioralnych – strategię. Pozwala on na zdefiniowanie rodziny algorytmów/strategii, ich enkapsulację i wymienność w czasie działania programu. Wzorzec ten zapewnia elastyczność, umożliwiając klientowi wybór bez zmiany kodu. Przykład, który przytoczymy na potrzeby tego artykułu dotyczy mechanizmów płatności sklepu internetowego. Wyobraźmy sobie, że nasz program jest aplikacją sklepu online, która ma za zadanie między innymi przetwarzać płatności użytkowników. Musimy jednak dopuszczać możliwość skorzystania z wielu mechanizmów płatności, a nasze rozwiązanie powinno być rozszerzalne o nowych dostawców płatności bez znacznej modyfikacji kodu. Na ratunek przychodzi wzorzec projektowy strategii.

 

Wzorce projektowe - Diagram klas

Rysunek 1 – Diagram klas dla aplikacji sklepu internetowego

 

Nasza aplikacja ma kilka klas odpowiadających za symulowanie działania sklepu. Nie jest to oczywiście działanie kompletne i realne, ale mające za zadanie pokazać pewien kontekst aplikacji wzorca strategii. Widzimy klasę Store, która przechowuje obiekt IPaymentStrategy, będący interfejsem z metodą pay. Dzięki zastosowaniu interfejsu, klasy PaypalPayment i StripePayment mogą być użyte w miejscu IPaymentStrategy. Klient chcący dokonać płatności wybiera metodę płatności, a kod w klasie Store dzięki metodom setPaymentStrategy oraz executePayment jest w stanie zapłacić z użyciem dowolnego zaimplementowanego sposobu. Dzięki temu możemy łatwo zmieniać sposób płatności poprzez zastosowanie abstrakcji. Klasa Store nie zależy bezpośrednio od żadnej z implementacji, a jedynie od interfejsu.

 

Wzorce projektowe - Diagram sekwencji

Rysunek 2 – Diagram sekwencji dla zaimplementowanego przypadku użycia wzorca strategii

 

Kod w Javie implementujący powyższy przypadek przedstawiamy poniżej.

public interface IPaymentStrategy {
   void pay(Double amount, String toAccountId);
}

public class PaypalPayment implements  IPaymentStrategy{
   @Override
   public void pay(Double amount, String toAccountId) {
       // Payment logic implementation
      System.out.println("Paypal payment: "+amount+" paid to "+toAccountId);
   }
}

public class StripePayment implements  IPaymentStrategy{
   @Override
   public void pay(Double amount, String toAccountId) {
      // Payment logic implementation
      System.out.println("Stripe payment: "+amount+" paid to "+toAccountId);
   }
}

public class Store {
   // Online store class
   Double cartValueAmount = 30.0;
   String shopAccountId = "123321";
   IPaymentStrategy paymentStrategy;
   
   public Store(IPaymentStrategy paymentStrategy) {
      this.paymentStrategy = paymentStrategy;
   }

   public void setPaymentStrategy(IPaymentStrategy paymentStrategy){
      this.paymentStrategy = paymentStrategy;
   }

   public void executePayment() {
      System.out.println("Executing payment strategy with class: " + paymentStrategy.getClass().getName());   this.paymentStrategy.pay(this.cartValueAmount,this.shopAccountId);
      System.out.println("Payment successful!\n");
   }
}

public class Client {
   public static void main(String[] args) {
      System.out.print("Welcome to the shop! \n \n");
      IPaymentStrategy paymentStrategy1 = new PaypalPayment();
      IPaymentStrategy paymentStrategy2 = new StripePayment();
      Store onlineStore = new Store(paymentStrategy1);
      onlineStore.executePayment();
      onlineStore.setPaymentStrategy(paymentStrategy2);
      onlineStore.executePayment();
   }
}

 

Implementacja – Template method

Drugim wzorcem behawioralnym, który zaimplementujemy, jest metoda szablonowa (Template Method), służąca do definiowania szkieletu algorytmu w klasie bazowej, przy jednoczesnym umożliwieniu podklasom nadpisywania poszczególnych kroków bez modyfikowania struktury algorytmu. Kontynuujmy przykład sklepu internetowego – tym razem w kontekście realizacji zamówienia, gdzie proces może wyglądać następująco: wybierz produkt, sprawdź, czy to prezent, zapakuj, jeśli tak, i dostarcz.

 

wzorzec template method

Rysunek 3 – wzorzec template method w kontekście sklepu

 

Wyobraźmy sobie sytuację w której sklep ma za zadanie przetwarzać zamówienia w określony sposób, który może wyglądać następująco: wybierz produkt, sprawdź czy jest oznaczony jako prezent, zawiń w papier prezentowy jeśli jest i dostarcz. Należy zauważyć, że taki proces jest mocno abstrakcyjny i jego konkretna implementacja może być nieco różna w zależności od kontekstu. W tym celu zastosujemy wzorzec template method, który pozwoli na zdefiniowanie abstrakcyjnego algorytmu, a następnie zastosowanie jego bardzo konkretnej implementacji. Zrobimy to przy użyciu klasy abstrakcyjnej OrderProcessTemplate i jej dwóch implementacji OnlineOrderProcess i StoreOrderProcess, które będą określały sposoby przetwarzania zamówienia online oraz do odbioru stacjonarnego.

 

diagram sekwencji dla implementacji wzorca template method

Rysunek 4 – diagram sekwencji dla implementacji wzorca template method w powyższym przykładzie

 

Widzimy jak wywołanie metody processOrder powoduje wywołanie konkretnych implementacji danych metod w podklasach dziedziczących tj. OnlineOrderProcess i StoreOrderProcess. Dzięki temu można tworzyć kolejne ‘processory’ dla innych przypadków bez modyfikacji innych klas. Spójrzmy na kod implementujący te koncepcje w języku Java.

public class StoreOrderProcess extends OrderProcessTemplate {
  public StoreOrderProcess() {
    super();
  }

  @Override
  protected void selectProduct() {
     System.out.println("Selecting product from the inventory.");
  }

  @Override
  protected void deliver() {
    System.out.println("Customer will pick up the product from the store.");
  }
}

public abstract class OrderProcessTemplate {
  public OrderProcessTemplate() {
  }

  public final void processOrder() {
    selectProduct();
    if (isGift()) {
      wrapGift();
    }
    deliver();
  }

  protected abstract void selectProduct();
  protected abstract void deliver();

  protected boolean isGift() {
    return false;
  }

  public class OnlineOrderProcess extends OrderProcessTemplate {
    public OnlineOrderProcess() {
      super();
    }

    @Override
    protected void selectProduct() {
      System.out.println("Selecting product from online catalog.");
    }

    @Override
    protected void deliver() {
      System.out.println("Delivering product to the customer's address.");
    }

    @Override
    protected boolean isGift() {
      return true;
    }
  }

  private void wrapGift() {
    System.out.println("Gift wrapping completed.");
  }
}

public class StoreOrderProcess extends OrderProcessTemplate {

  public StoreOrderProcess() {
    super();
  }

  @Override
  protected void selectProduct() {
    System.out.println("Selecting product from the inventory.");
  }

  @Override
  protected void deliver() {
    System.out.println("Customer will pick up the product from the store.");
  }
}

 

Zwróćmy uwagę na rolę klasy abstrakcyjnej, która posiada implementacje metod processOrder oraz isGift, a z drugiej strony dwie metody abstrakcyjne selectProduct i deliver, które muszą zostać nadpisane w klasie dziedziczącej.

 

Połączenie Strategy i Template method

Gdyby połączyć razem dwa poprzednie przypadki, tak aby pokazać zastosowanie obu wzorców jednocześnie mogłoby to wyglądać jak na poniższym diagramie.

 

wzorce projektowe - Diagram klas 2

Rysunek 5 – Diagram klas dla połączonych wzorców Strategy i Template Method

 

 

wzorce projektowe - diagram sekwencji 3

Rysunek 6 – diagram sekwencji dla przypadku łączonego

 

Jeśli jesteście ciekawi jak wygląda kod ostatniego z rozwiązań to dołączamy całe repozytorium w załączniku.

 

Podsumowanie

Dzisiaj udało nam się opowiedzieć Wam czym są wzorce projektowe, jaką rolę spełniają i dlaczego warto je stosować. Przedstawiliśmy również kilka implementacji dzięki którym sami możecie zgłębić wiedzę w tym temacie. Jeśli jesteście ciekawi, to zajrzyjcie do kodu lub do linków w źródłach zawierających więcej informacji. Do usłyszenia w następnym wpisie!

Wszystkie powyższe przypadki są mocno dydaktyczne i nie przedstawiają w pełni aplikacji takich wzorców, ale pozwalają na zrozumienie tego jak działają i w jaki sposób powinny być stosowane.

 

Kod do pobrania na naszym gitlabie!

 

Źródła:

https://mermaid.js.org/intro/

https://refactoring.guru/

Zobacz więcej na naszym blogu:

Wzorce projektowe

Wzorce projektowe

Programisto, to artykuł dla Ciebie! Łap garść przydatnych informacji na temat wzorców projektowych.

Programowanie

Blockchain – płatności w świecie kryptowalut

Blockchain – płatności w świecie kryptowalut

Blockchainie - poznaj świat transakcji, kryptowalut i elektronicznych płatności.

BezpieczeństwoFinanse

FastAPI – czyli jak napisać proste REST API w Pythonie? – część 3

FastAPI – czyli jak napisać proste REST API w Pythonie? – część 3

REST API z użyciem frameworka FastAPI. Ostatniej części artykułów o API w Pythonie. Zacznij z nami już dziś swoją przygodę z FastAPI!

Programowanie