W trakcie tworzenia oprogramowania niezwykle ważne jest takie zapewnienie elastyczności w projekcie, aby był łatwy w utrzymaniu, wydajny oraz mógł działać wraz ze zmieniającymi się warunkami technologicznymi lub biznesowymi. Dlatego aby kod naszej aplikacji spełniał takie warunki, musimy wykorzystywać sprawdzone praktyki, którymi są między innymi wzorce projektowe.
Java, będąca jednym z najpopularniejszych języków programowania, posiada własny zestaw wzorców projektowych, które każdy programista Java powinien znać. W tym artykule poznamy najpopularniejsze z nich i przeanalizujemy ich zastosowanie.
Spis treści
Czym są wzorce projektowe?
Wzorzec projektowy to rozwiązanie wielokrotnego użytku dla typowego problemu występującego w projektowaniu oprogramowania, który można zastosować w różnych sytuacjach. Nie są to gotowe projekty czy kod, lecz sprawdzone praktyki wielu programistów z gotowymi rozwiązaniami dla wybranych problemów napotykanych w trakcie projektowania rozwiązań zorientowanych obiektowo.
Kluczową rolę w procesie popularyzacji koncepcji wzorców projektowych odegrali autorzy bestsellera „Design Patterns: Elements of Reusable Object-Oriented Software„: Erich Gamma, Richard Helm, Ralph Johnson i John Vlissides, wspólnie znani jako „Gang of Four” (GoF).
Od 1994 roku powstało wiele frameworków, które również wykorzystują te wzorce projektowe, więc programiści korzystający z bibliotek w swoich projektach używają tych wzorców, czasem nawet nieświadomie. Z drugiej strony, warto używać ich celowo, aby rozmawiać z programistami używającymi tego samego języka i posiadającymi ten sam kod źródłowy.
Dlaczego wzorce projektowe Java są ważne?
Wzorce projektowe Java są niezbędne z kilku powodów. Po pierwsze, promują one ponowne wykorzystanie kodu, umożliwiając programistom pisanie modułowego i rozszerzalnego kodu. Ułatwia to utrzymanie i aktualizację systemów oprogramowania w dłuższej perspektywie.
Na przykład wzorzec Factory, to wzorzec projektowy, który zapewnia interfejs do tworzenia obiektów, ale pozwala podklasom decydować, która klasa ma utworzyć instancję. Promuje to ponowne wykorzystanie kodu, umożliwiając programistom tworzenie obiektów bez określania dokładnej klasy obiektu, który zostanie utworzony.
Po drugie, wzorce projektowe poprawiają czytelność i łatwość utrzymania kodu. Podążając za standardową strukturą, wzorce sprawiają, że kod jest łatwiejszy do zrozumienia, co jest szczególnie pomocne podczas pracy w zespołach lub podczas powrotu do bazy kodu po długim czasie.
Trzy powszechnie używane kategorie wzorców projektowych
Zgodnie z wcześniej wspomnianym podręcznikiem wzorców projektowych, 23 wzorce projektowe można podzielić na trzy kategorie w zależności od realizowanej odpowiedzialności: wzorce kreacyjne, strukturalne i behawioralne. Przyjrzyjmy się teraz bliżej niektórym z ważnych wzorców projektowych wykorzystywanych w Javie.
Wzorce kreacyjne
Kreacyjne wzorce projektowe dotyczą sposobu tworzenia klas, metod oraz typów danych, koncentrując się na procesie tworzenia obiektów, starając się tworzyć obiekty w sposób odpowiedni do sytuacji. Wzorce te mają na celu uczynienie tworzenia obiektów bardziej elastycznym i oddzielonym od kodu klienta, promując możliwość ponownego użycia kodu i łatwość konserwacji.
- Factory
Wzorzec Factory zapewnia interfejs do tworzenia obiektów w superklasie, ale pozwala podklasom na zmianę typów obiektów, które zostaną utworzone. Jest on stosowany, gdy system musi być niezależny od tworzenia, kompozycji i reprezentacji obiektów. Dzięki temu klasa może delegować odpowiedzialność za tworzenie instancji na swoje podklasy.
Wzorzec fabryki jest powszechnie stosowany w scenariuszach, w których istnieje wiele klas wdrażających wspólny interfejs lub rozszerzających wspólną klasę bazową. Istnieją różne odmiany wzorca fabryki, takie jak Simple Factory, Factory Method i Abstract Factory. Każda odmiana ma swoje zalety i przypadki użycia, co pozwala wybrać najbardziej odpowiednią dla konkretnych wymagań.
- Builder
Builder to kreacyjny wzorzec projektowy, który oddziela konstrukcję złożonego obiektu od jego reprezentacji, umożliwiając temu samemu procesowi konstrukcyjnemu tworzenie różnych reprezentacji. Obejmuje on klasę dyrektora, która koordynuje proces budowy, oraz interfejs buildera z konkretnymi klasami implementującymi etapy budowy.
Builder jest często używany przez programistów przy użyciu biblioteki Lombok, która generuje dla nas kod Java i, między innymi, zapewnia również tworzenie metod, które są używane w Builderze. Czyli tworząc klasy i tworząc nowe obiekty, możemy z tego korzystać bez wysiłku, bez pisania dużej ilości kodu. Stosuje się go, gdy obiekt musi zostać skonstruowany z wieloma możliwymi konfiguracjami lub gdy proces budowy obejmuje wiele kroków.
- Singleton
Singleton to kreacyjny wzorzec projektowy, który daje pewność, że klasa ma tylko jedną instancję, która jest jednocześnie globalnym punktem dostępu do tej instancji. Jest on niezwykle przydatny, kiedy mamy do czynienia z implementacjami usług, które wykorzystujemy w całym programie tylko raz, niezależnie od miejsca utworzenia. rozwiązanie takie jest szczególnie wykorzystywane przy: połączeniach z bazą danych czy z urządzeniami wykorzystujących komunikację portów szeregowych. Posiadając tylko jedną instancję klasy, można zapewnić efektywne wykorzystanie zasobów i uniknąć niepotrzebnego obciążenia.
- Prototyp
Wzorzec Prototyp jest zwykle używany, gdy mamy instancję klasy (prototyp) i chcemy utworzyć nowe obiekty, po prostu kopiując prototyp. Wzorzec ten jest szczególnie przydatny, gdy koszt stworzenia nowego obiektu jest droższy lub bardziej skomplikowany niż skopiowanie istniejącego. Jest to szczególnie przydatne w scenariuszach, w których tworzenie obiektów musi być dynamiczne i elastyczne.
Wzorce strukturalne
Strukturalne wzorce projektowe dotyczą zależności powiązanych ze sobą obiektów w celu utworzenia większych struktur. Wzorce te pomagają definiować relacje między jednostkami i upraszczają projektowanie złożonych systemów.
- Dekorator
Wzorzec Dekorator umożliwia dodanie zachowania do pojedynczego obiektu, statycznie lub dynamicznie, bez wpływu na zachowanie innych obiektów z tej samej klasy. Jest to wzorzec strukturalny obejmujący zestaw klas dekoratorów, które są używane do opakowywania konkretnych komponentów. Najczęściej jest używany, gdy chcemy rozszerzyć zachowanie poszczególnych obiektów, bez modyfikowania ich kodu.
- Fasada
Wzorzec Fasada zapewnia uproszczony (ale ograniczony) interfejs do złożonego systemu klas, biblioteki lub frameworka, zmniejsza ogólną złożoność aplikacji oraz pomaga przenieść niechciane zależności w jedno miejsce. Głównym celem wzorca Fasada jest uproszczenie i ujednolicenie zestawu interfejsów, dzięki czemu podsystem staje się bardziej dostępny i łatwiejszy w użyciu dla klientów. Wzorce Fasada stosuje się najczęściej w aplikacjach napisanych w Javie, podczas pracy ze złożonymi bibliotekami i interfejsami API.
- Proxy (pełnomocnik)
Proxy to niejako reprezentant lub pełnomocnik innego obiektu w celu uzyskania nadzorowanego dostępu do obiektu, który przedstawia. Używając tego wzorca, możemy zminimalizować obciążenie i zwiększyć wydajność. Jeśli musimy użyć jakiejś biblioteki, o której wiemy, że spowoduje duże obciążenie serwera CPU, możemy niejako odłożyć tę operację do ostatecznego momentu, w którym będziemy musieli jej użyć. W przypadku aplikacji oszczędzamy pamięć i zasoby sprzętowe służące do realizacji ciężkich obliczeniowo operacji, a skomplikowane elementy wczytują się tylko wtedy, kiedy są one rzeczywiście potrzebne.
Wzorce behawioralne
Dotyczą zachowania współpracujących obiektów oraz sposobów, w jaki klasy i obiekty wchodzą w interakcje, komunikują się i współpracują. Wzorce behawioralne dotyczą przede wszystkim delegowania odpowiedzialności między obiektami i wzorców komunikacji między nimi. Bardzo często się je stosuje w projektach typu enterprise.
Wynika to z faktu, że korzystając z zewnętrznych dostawców bibliotek, możemy mieć do czynienia z wyciekami pamięci, co może przysporzyć nam problemów, o których dowiemy się dopiero za kilka tygodni lub miesięcy. W takich sytuacjach warto skorzystać z różnych wzorców, aby na przykład monitorować działanie aplikacji.
- Strategy
Strategy definiuje rodzinę algorytmów, hermetyzuje każdy z nich i umożliwia ich wymianę. Strategia pozwala algorytmowi zmieniać się niezależnie od klientów, którzy go używają. Pozwala klientowi wybierać z rodziny algorytmów w momencie ich wykonywania. Kluczową ideą wzorca Strategy jest oddzielenie algorytmów od klientów, którzy ich używają, promując elastyczność i możliwość rozbudowy.
- Visitor
Wzorzec Visitor odpowiada za wykonanie jakiejś konkretną operację na złożonej strukturze danych. Pozwala zdefiniować nową operację bez zmiany klas elementów, na których operuje. Czyli na przykład będzie nam logował czasy operacji albo informował kto i kiedy oraz w jaki sposób uruchomił jakieś funkcje.
- Observer
Observer to wzorzec, który definiuje zależność jeden-do-wielu między obiektami, dzięki czemu, gdy jeden obiekt zmienia stan, wszystkie jego obiekty zależne są automatycznie powiadamiane i aktualizowane. Z jednej strony daje pewność, że mamy moduł, który publikuje nam zdarzenia, na przykład użytkownik zażądał określonych danych i wtedy tworzymy zdarzenie, które z kolei wywołuje reakcję w drugiej części systemu, generując odpowiednie dane.
3 przykłady użycia wzorców projektowych w Java
Przykład #1
W pierwszym przypadku zastosujemy cztery rodzaje wzorców do rozwiązania problemu tworzenia różnych obiektów typu Vehicle które posiadają specyficzne dla siebie zachowania.
- Builder uzyskany za pomocą adnotacji @SuperBuilder służy do tworzenia metod Buildera podczas dziedziczenia. Zwykły @Builder lomboka nie ma tu zastosowania.
- Factory zastosowany w klasie VehicleFactory do wytwarzania typów obiektów w zależności od nazwy.
- Factory Metod (metoda wytwórcza) zdefiniowane za pomocą klas VehicleCreator zawierają szczegóły wytwarzania poszczególnych pojazdów.
- Strategy użyty w formie EconomicDriving oraz AggressiveDriving definiuje style jazdy poszczególnych pojazdów.
Poniżej przykład użycia czterech wzorców do rozwiązania problemu tworzenia różnych obiektów typu Vehicle, które posiadają specyficzne dla siebie zachowanie.
Po uruchomieniu programu na ekranie otrzymamy:
- 24.0L V12 diesel truck economic driving
- 6.0L V6 diesel bus economic driving
- 2.0L R4 petrol car aggressive driving
Przykład #2
W drugim przykładzie użyjemy wzorca memento w programie typu Paint aby uzyskać możliwość 10 poziomowej funkcji 'Undo’ czyli wstecz.
Na płótnie (Canvas) możemy rysować kształty oraz wypełniać je kolorami. Za każdym razem, kiedy naciśniemy Save, Caretaker utworzy i zapisze bieżący stan płótna jako obiekt CanvasMemento. W przypadku naciśnięcia przycisku Undo, wracamy do poprzedniego stanu.
Po uruchomieniu programu dwukrotnie zapisujemy zmiany, modyfikujemy jeszcze raz a następnie dwa razy używamy undo aby przywrócić poprzednie wartości płótna.
Na ekranie otrzymamy:
- Canvas after all operations: draw:Circle fill:Red draw:Triangle
- Canvas after undo draw triangle: draw:Circle fill:Red
- Canvas after undo fill red: draw:Circle
Przykład #3
Trzeci przykład to zastosowanie wzorca typu Visitor w przypadku inspektora budowlanego który ma za zadanie wykonać tylko jemu znane czynności podczas odwiedzania kolejnych pomieszczeń domu.
W momencie, kiedy zaakceptujemy inspektora i wpuścimy go do domu (house.accept(inspector)), BuildingInspector rozpoczyna inspekcję pomieszczeń zdefiniowanych na etapie budowy domu. Wykonuje on czynności specyficzne dla każdego z pomieszczeń a na końcu sprawdza strukturę całego domu kiedy w metodzie accept klasy House wołamy 'visitor.visit(this)’
Na ekranie po zakończeniu programu zgodnie z kolejnością pomieszczeń podczas tworzenia domu:
- Inspecting the quality of kitchen appliances and safety.
- Checking living room space and ventilation.
- Inspecting plumbing and hygiene conditions in the bathroom.
- Performing overall structural integrity check of the house.
Co wziąć pod uwagę przy wyborze wzorców projektowych?
W trakcie wdrażania wzorców projektowych ważne jest, aby nie komplikować zbytnio naszego projektu. Lepiej zacząć od prostych rozwiązań, natomiast wzorce projektowe, wprowadzić po to, by rozwiązać problem złożoności.
Kolejną ważną rzeczą jest trzymanie się standardów kodowania i najlepszych praktyk. Spójne style kodowania ułatwiają zespołowi zrozumienie i utrzymanie kodu, nawet przy użyciu wzorców projektowych.
Aby kierować członkami zespołu i innymi przyszłymi recenzentami kodu, konieczne jest jasne udokumentowanie decyzji projektowych. Chociaż wzorce projektowe mogą ułatwić konserwację, niewłaściwe ich wdrożenie może wprowadzać błędy. Dlatego też powinniśmy zawsze korzystać z kompleksowych testów, aby zapewnić poprawność implementacji naszych wzorców projektowych.
Wzorce projektowe w refaktoryzacji kodu – jak używać ich mądrze?
Jeśli wybrany wzorzec projektowy nie zapewnia oczekiwanych korzyści lub jeśli zmienią się wymagania trzeba być gotowym do refaktoryzacji kodu, ponieważ wzorce projektowe powinny dostosowywać się do zmieniających się potrzeb projektu.
Podchodząc do refaktoryzacji, z pewnością warto zrozumieć cały system holistycznie, aby świadomie zdecydować, czy chcemy i w jakiej skali chcemy przepisać ten system, np. czy są tam jakieś problemy z wydajnością? Czy problemem jest po prostu to, że np. chcemy użyć nowej wersji biblioteki, która ma zupełnie inny interfejs?
Jeśli zapadnie decyzja, że np. przepisujemy moduł, uwzględniając obecną strukturę, a wzorce są użyte w przemyślany sposób, warto z nich skorzystać. Dają one możliwość rozszerzania i rozbudowy projektu bez zbyt dużych nakładów czasowych.
Z drugiej strony, wprowadzanie wzorców od zera do projektu, który jest napisany w sposób chaotyczny i niezrozumiały, może skutkować tym, że w zasadzie zaczynamy nowy projekt od zera. To znaczy, najpierw spędzamy kilka miesięcy na zrozumieniu starszej wersji projektu, a potem określamy, że niekiedy 3 lub 4 wzorce mogłyby rozwiązać podstawowe problemy. Oznacza to, że od samego początku, kiedy tworzymy pierwszy etap projektu, MVP z najważniejszymi funkcjami dla klienta, to korzystamy z kilku wzorców, a następne moduły według nich implementujemy.
Podsumowanie
W tym artykule omówiliśmy tylko niektóre z popularnych wzorców projektowych Java, takich jak kreacyjne, strukturalne czy behawioralne i wyjaśniliśmy, w jaki sposób można je zastosować w programowaniu w Javie. Im lepiej zrozumiesz i wykorzystasz te wzorce projektowe w codziennej pracy, możesz poprawić jakość swojego kodu Java, ułatwić jego konserwację i stać się bardziej wykwalifikowanym i wydajnym programistą.
Jeśli chciałbyś porozmawiać z doświadczonymi programistami o ich praktykach dotyczących kodowania, migracji, czy refaktoryzacji, skontaktuj się z nami. Chętnie opowiemy o swoich doświadczeniach i doradzimy skuteczne rozwiązania w Twoim projekcie.