JavaRush /Blog Java /Random-PL /Wzorce projektowe w Javie
Viacheslav
Poziom 3

Wzorce projektowe w Javie

Opublikowano w grupie Random-PL
Wzorce lub wzorce projektowe są często pomijaną częścią pracy programisty, co utrudnia utrzymanie kodu i dostosowywanie go do nowych wymagań. Sugeruję, abyś przyjrzał się, co to jest i jak jest używane w JDK. Naturalnie wszystkie podstawowe wzorce w tej czy innej formie towarzyszą nam od dawna. Zobaczmy je w tej recenzji.
Wzorce projektowe w Javie - 1
Treść:

Szablony

Jednym z najczęstszych wymagań na stanowiskach pracy jest „Znajomość wzorców”. Przede wszystkim warto odpowiedzieć sobie na proste pytanie – „Co to jest wzorzec projektowy?” Wzór jest tłumaczony z języka angielskiego jako „szablon”. Oznacza to, że jest to pewien schemat, według którego coś robimy. Podobnie jest w programowaniu. Istnieje kilka ustalonych najlepszych praktyk i podejść do rozwiązywania typowych problemów. Każdy programista jest architektem. Nawet jeśli utworzysz tylko kilka klas lub nawet jedną, od Ciebie zależy, jak długo kod wytrzyma przy zmieniających się wymaganiach, jak wygodny będzie w użyciu dla innych. I tu pomocna będzie znajomość szablonów, bo... Dzięki temu szybko zrozumiesz, jak najlepiej napisać kod, bez konieczności jego przepisywania. Jak wiadomo programiści to leniwi ludzie i łatwiej jest od razu coś dobrze napisać, niż powtarzać to kilka razy). Wzorce też mogą wydawać się podobne do algorytmów. Ale mają różnicę. Algorytm składa się z konkretnych kroków opisujących niezbędne działania. Wzorce opisują jedynie podejście, ale nie opisują etapów wdrażania. Wzory są różne, ponieważ... rozwiązywać różne problemy. Zwykle wyróżnia się następujące kategorie:
  • Generatywny

    Wzorce te rozwiązują problem zapewnienia elastyczności tworzenia obiektów

  • Strukturalny

    Wzorce te rozwiązują problem efektywnego budowania połączeń pomiędzy obiektami

  • Behawioralne

    Wzorce te rozwiązują problem efektywnej interakcji pomiędzy obiektami

Aby rozważyć przykłady, sugeruję skorzystanie z kompilatora kodu online repl.it.
Wzorce projektowe w Javie - 2

Wzorce twórcze

Zacznijmy od początku cyklu życia obiektów – od powstania obiektów. Szablony generatywne pomagają w wygodniejszym tworzeniu obiektów i zapewniają elastyczność tego procesu. Jednym z najbardziej znanych jest „ Budowniczy ”. Ten wzór pozwala krok po kroku tworzyć złożone obiekty. W Javie najbardziej znanym przykładem jest StringBuilder:
class Main {
  public static void main(String[] args) {
    StringBuilder builder = new StringBuilder();
    builder.append("Hello");
    builder.append(',');
    builder.append("World!");
    System.out.println(builder.toString());
  }
}
Innym dobrze znanym podejściem do tworzenia obiektu jest przeniesienie tworzenia do osobnej metody. Metoda ta staje się swego rodzaju fabryką obiektów. Dlatego wzór nazywa się „ Metodą fabryczną ”. Na przykład w Javie jego efekt można zobaczyć w klasie java.util.Calendar. Sama klasa Calendarjest abstrakcyjna i do jej utworzenia używana jest metoda getInstance:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Calendar calendar = Calendar.getInstance();
    System.out.println(calendar.getTime());
    System.out.println(calendar.getClass().getCanonicalName());
  }
}
Dzieje się tak często dlatego, że logika tworzenia obiektów może być złożona. Przykładowo w powyższym przypadku uzyskujemy dostęp do klasy bazowej Calendari klasa zostaje utworzona GregorianCalendar. Jeśli spojrzymy na konstruktora, zobaczymy, że w zależności od warunków tworzone są różne implementacje Calendar. Ale czasami jedna metoda fabryczna nie wystarczy. Czasami trzeba stworzyć różne obiekty, aby do siebie pasowały. Pomoże nam w tym inny szablon - „ Fabryka abstrakcyjna ”. A potem musimy stworzyć różne fabryki w jednym miejscu. Jednocześnie zaletą jest to, że nie są dla nas istotne szczegóły wdrożenia, tj. nie ma znaczenia, jaką konkretną fabrykę trafimy. Najważniejsze, że tworzy odpowiednie wdrożenia. Super przykład:
Wzorce projektowe w Javie - 3
Oznacza to, że w zależności od środowiska (systemu operacyjnego) otrzymamy określoną fabrykę, która stworzy kompatybilne elementy. Jako alternatywę dla podejścia tworzenia za pośrednictwem kogoś innego możemy zastosować wzór „ Prototyp ”. Jej istota jest prosta – nowe obiekty powstają na obraz i podobieństwo obiektów już istniejących, tj. według ich prototypu. Każdy spotkał się z tym wzorcem w Javie - jest to użycie interfejsu java.lang.Cloneable:
class Main {
  public static void main(String[] args) {
    class CloneObject implements Cloneable {
      @Override
      protected Object clone() throws CloneNotSupportedException {
        return new CloneObject();
      }
    }
    CloneObject obj = new CloneObject();
    try {
      CloneObject pattern = (CloneObject) obj.clone();
    } catch (CloneNotSupportedException e) {
      //Do something
    }
  }
}
Jak widać, osoba wywołująca nie wie, w jaki sposób clone. Oznacza to, że za stworzenie obiektu na podstawie prototypu odpowiada sam obiekt. Jest to przydatne, ponieważ nie wiąże użytkownika z implementacją obiektu szablonu. Cóż, ostatnim na tej liście jest wzór „Singleton”. Jego cel jest prosty – zapewnić pojedynczą instancję obiektu dla całej aplikacji. Ten wzorzec jest interesujący, ponieważ często pokazuje problemy z wielowątkowością. Aby uzyskać bardziej szczegółowe informacje, zapoznaj się z tymi artykułami:
Wzorce projektowe w Javie - 4

Wzory strukturalne

Wraz z tworzeniem obiektów stało się to jaśniejsze. Nadszedł czas, aby przyjrzeć się wzorcom strukturalnym. Ich celem jest budowanie łatwych do utrzymania hierarchii klas i ich relacji. Jednym z pierwszych i dobrze znanych wzorców jest „ Zastępca ” (Proxy). Serwer proxy ma ten sam interfejs, co obiekt rzeczywisty, więc nie ma znaczenia, czy klient będzie pracował przez serwer proxy, czy bezpośrednio. Najprostszym przykładem jest Java.lang.reflect.Proxy :
import java.util.*;
import java.lang.reflect.*;
class Main {
  public static void main(String[] arguments) {
    final Map<String, String> original = new HashMap<>();
    InvocationHandler proxy = (obj, method, args) -> {
      System.out.println("Invoked: " + method.getName());
      return method.invoke(original, args);
    };
    Map<String, String> proxyInstance = (Map) Proxy.newProxyInstance(
        original.getClass().getClassLoader(),
        original.getClass().getInterfaces(),
        proxy);
    proxyInstance.put("key", "value");
    System.out.println(proxyInstance.get("key"));
  }
}
Jak widać, w przykładzie mamy oryginał - to ten HashMap, który implementuje interfejs Map. Następnie tworzymy proxy, które zastępuje oryginalne HashMapdla części klienta, które wywołuje putmetody geti dodając naszą własną logikę podczas wywołania. Jak widzimy, interakcja we wzorcu odbywa się poprzez interfejsy. Czasem jednak zamiennik nie wystarczy. Następnie można zastosować wzór „ Dekorator ”. Dekorator nazywany jest również opakowaniem lub opakowaniem. Serwer proxy i dekorator są bardzo podobne, ale jeśli spojrzysz na przykład, zobaczysz różnicę:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    List<String> list = new ArrayList<>();
    List<String> decorated = Collections.checkedList(list, String.class);
    decorated.add("2");
    list.add("3");
    System.out.println(decorated);
  }
}
W przeciwieństwie do proxy, dekorator skupia się wokół czegoś, co jest przekazywane jako dane wejściowe. Serwer proxy może zarówno zaakceptować to, co wymaga proxy, jak i zarządzać życiem samego obiektu proxy (na przykład utworzyć obiekt proxy). Jest jeszcze jeden ciekawy wzór – „ Adapter ”. Działa podobnie do dekoratora - dekorator przyjmuje jeden obiekt jako dane wejściowe i zwraca opakowanie na ten obiekt. Różnica polega na tym, że celem nie jest zmiana funkcjonalności, ale dostosowanie jednego interfejsu do drugiego. Java ma bardzo wyraźny przykład tego:
import java.util.*;
class Main {
  public static void main(String[] arguments) {
    String[] array = {"One", "Two", "Three"};
    List<String> strings = Arrays.asList(array);
    strings.set(0, "1");
    System.out.println(Arrays.toString(array));
  }
}
Na wejściu mamy tablicę. Następnie tworzymy adapter, który przenosi tablicę do interfejsu List. Pracując z nim, tak naprawdę pracujemy z tablicą. Dlatego dodawanie elementów nie będzie działać, ponieważ... Oryginalnej tablicy nie można zmienić. I w tym przypadku otrzymamy UnsupportedOperationException. Kolejnym interesującym podejściem do tworzenia struktury klas jest wzorzec Composite . Interesujące jest to, że pewien zbiór elementów korzystających z jednego interfejsu ułożony jest w pewną drzewiastą hierarchię. Wywołując metodę na elemencie nadrzędnym, otrzymujemy wywołanie tej metody na wszystkich niezbędnych elementach potomnych. Doskonałym przykładem tego wzorca jest interfejs użytkownika (czy to Java.awt, czy JSF):
import java.awt.*;
class Main {
  public static void main(String[] arguments) {
    Container container = new Container();
    Component component = new java.awt.Component(){};
    System.out.println(component.getComponentOrientation().isLeftToRight());
    container.add(component);
    container.applyComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
    System.out.println(component.getComponentOrientation().isLeftToRight());
  }
}
Jak widzimy, dodaliśmy komponent do kontenera. Następnie poprosiliśmy kontener o zastosowanie nowej orientacji komponentów. A kontener wiedząc, z jakich komponentów się składa, delegował wykonanie tego polecenia wszystkim komponentom potomnym. Kolejnym ciekawym wzorem jest wzór „ Most ”. Nazywa się to tak, ponieważ opisuje połączenie lub pomost pomiędzy dwiema różnymi hierarchiami klas. Jedna z tych hierarchii jest uważana za abstrakcję, a druga za implementację. Jest to podkreślone, ponieważ sama abstrakcja nie wykonuje akcji, ale deleguje to wykonanie do implementacji. Ten wzorzec jest często używany, gdy istnieją klasy „kontrolne” i kilka typów klas „platformowych” (na przykład Windows, Linux itp.). Dzięki takiemu podejściu jedna z tych hierarchii (abstrakcja) otrzyma odniesienie do obiektów innej hierarchii (implementacja) i deleguje im główną pracę. Ponieważ wszystkie implementacje będą miały wspólny interfejs, można je wymieniać w ramach abstrakcji. W Javie wyraźnym tego przykładem jest java.awt:
Wzorce projektowe w Javie - 5
Więcej informacji znajdziesz w artykule „ Wzorce w Java AWT ”. Wśród wzorów strukturalnych chciałbym zwrócić uwagę również na wzór „ Fasada ”. Jego istotą jest ukrycie złożoności korzystania z bibliotek/frameworków stojących za tym API za wygodnym i zwięzłym interfejsem. Na przykład możesz użyć JSF lub EntityManager z JPA. Istnieje również inny wzór zwany „ wagą muszą ”. Jego istotą jest to, że jeśli różne obiekty mają ten sam stan, to można go uogólnić i zapisać nie w każdym obiekcie, ale w jednym miejscu. Następnie każdy obiekt będzie mógł odwoływać się do wspólnej części, co obniży koszty pamięci do przechowywania. Ten wzorzec często obejmuje wstępne buforowanie lub utrzymywanie puli obiektów. Co ciekawe, znamy też ten wzór od samego początku:
Wzorce projektowe w Javie - 6
Przez tę samą analogię można tu uwzględnić pulę ciągów. Możesz przeczytać artykuł na ten temat: „ Wzorzec projektowy wagi muszej ”.
Wzorce projektowe w Javie - 7

Wzorce zachowań

Rozpracowaliśmy więc, w jaki sposób można tworzyć obiekty i jak organizować połączenia między klasami. Najciekawsze pozostało zapewnienie elastyczności w zmianie zachowania obiektów. Pomogą nam w tym wzorce zachowań. Jednym z najczęściej wymienianych wzorców jest wzór „ Strategia ”. W tym miejscu rozpoczyna się nauka wzorców z książki „ Najpierw głowa. Wzorce projektowe ”. Korzystając ze wzorca „Strategia” możemy zapisać w obiekcie sposób, w jaki wykonamy akcję, tj. obiekt wewnątrz przechowuje strategię, którą można zmienić podczas wykonywania kodu. Jest to wzór, którego często używamy podczas korzystania z komparatora:
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Comparator<String> comparator = Comparator.comparingInt(String::length);
    Set dataSet = new TreeSet(comparator);
    dataSet.addAll(data);
    System.out.println("Dataset : " + dataSet);
  }
}
Przed nami - TreeSet. Ma zachowanie polegające na TreeSetzachowaniu kolejności elementów, tj. sortuje je (ponieważ jest to SortedSet). To zachowanie ma domyślną strategię, którą widzimy w JavaDoc: sortowanie w „porządku naturalnym” (w przypadku ciągów jest to porządek leksykograficzny). Dzieje się tak, jeśli używasz konstruktora bez parametrów. Ale jeśli chcemy zmienić strategię, możemy przejść Comparator. W tym przykładzie możemy stworzyć nasz zbiór jako new TreeSet(comparator), a wówczas kolejność przechowywania elementów (strategia przechowywania) zmieni się na określoną w komparatorze. Co ciekawe, istnieje prawie taki sam wzorzec zwany „ Stan ”. Wzorzec „Stan” mówi, że jeśli w obiekcie głównym mamy jakieś zachowanie zależne od stanu tego obiektu, to możemy opisać sam stan jako obiekt i zmienić obiekt stanu. I deleguj wywołania z obiektu głównego do stanu. Kolejnym wzorcem znanym nam z studiowania samych podstaw języka Java jest wzorzec „ Polecenie ”. Ten wzorzec projektowy sugeruje, że różne polecenia mogą być reprezentowane jako różne klasy. Ten wzór jest bardzo podobny do wzorca Strategia. Jednak we wzorcu Strategia na nowo zdefiniowaliśmy sposób wykonania określonej akcji (na przykład sortowanie w TreeSet). We wzorcu „Polecenie” na nowo definiujemy, jaka akcja zostanie wykonana. Polecenie wzór towarzyszy nam na co dzień, gdy używamy wątków:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Runnable command = () -> {
      System.out.println("Command action");
    };
    Thread th = new Thread(command);
    th.start();
  }
}
Jak widać polecenie definiuje akcję lub polecenie, które zostanie wykonane w nowym wątku. Warto rozważyć także wzór „ Łańcucha odpowiedzialności ”. Ten wzór jest również bardzo prosty. Ten wzorzec mówi, że jeśli coś wymaga przetworzenia, można zebrać procedury obsługi w łańcuchu. Na przykład ten wzorzec jest często używany na serwerach internetowych. Na wejściu serwer ma jakieś żądanie od użytkownika. To żądanie przechodzi następnie przez łańcuch przetwarzania. Ten łańcuch procedur obsługi obejmuje filtry (na przykład nie akceptuj żądań z czarnej listy adresów IP), procedury obsługi uwierzytelniania (zezwalaj tylko autoryzowanym użytkownikom), procedurę obsługi nagłówka żądania, procedurę obsługi buforowania itp. Ale jest prostszy i bardziej zrozumiały przykład w Javie java.util.logging:
import java.util.logging.*;
class Main {
  public static void main(String[] args) {
    Logger logger = Logger.getLogger(Main.class.getName());
    ConsoleHandler consoleHandler = new ConsoleHandler(){
		@Override
            public void publish(LogRecord record) {
                System.out.println("LogRecord обработан");
            }
        };
    logger.addHandler(consoleHandler);
    logger.info("test");
  }
}
Jak widać, procedury obsługi są dodawane do listy procedur obsługi rejestratora. Kiedy program rejestrujący otrzymuje komunikat do przetworzenia, każdy taki komunikat przechodzi przez łańcuch procedur obsługi (od logger.getHandlers) dla tego rejestratora. Kolejnym wzorcem, który widzimy na co dzień, jest „ Iterator ”. Jej istotą jest wydzielenie zbioru obiektów (czyli klasy reprezentującej strukturę danych np. List) i przechodzenie przez ten zbiór.
import java.util.*;
class Main {
  public static void main(String[] args) {
    List<String> data = Arrays.asList("Moscow", "Paris", "NYC");
    Iterator<String> iterator = data.iterator();
    while (iterator.hasNext()) {
      System.out.println(iterator.next());
    }
  }
}
Jak widać, iterator nie jest częścią kolekcji, ale jest reprezentowany przez oddzielną klasę, która przechodzi przez kolekcję. Użytkownik iteratora może nawet nie wiedzieć, po jakiej kolekcji iterator wykonuje iterację, tj. jaką kolekcję odwiedza? Warto rozważyć wzór „ Gość ”. Wzorzec gościa jest bardzo podobny do wzorca iteratora. Ten wzorzec pomaga ominąć strukturę obiektów i wykonywać na nich akcje. Różnią się raczej koncepcją. Iterator przemierza kolekcję tak, że klienta korzystającego z iteratora nie interesuje, co znajduje się w środku kolekcji, ważne są tylko elementy w sekwencji. Gość oznacza, że ​​istnieje pewna hierarchia lub struktura obiektów, które odwiedzamy. Na przykład możemy zastosować oddzielne przetwarzanie katalogów i oddzielne przetwarzanie plików. Java ma gotową implementację tego wzorca w postaci java.nio.file.FileVisitor:
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.*;
class Main {
  public static void main(String[] args) {
    SimpleFileVisitor visitor = new SimpleFileVisitor() {
      @Override
      public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        System.out.println("File:" + file.toString());
        return FileVisitResult.CONTINUE;
      }
    };
    Path pathSource = Paths.get(System.getProperty("java.io.tmpdir"));
    try {
      Files.walkFileTree(pathSource, visitor);
    } catch (AccessDeniedException e) {
      // skip
    } catch (IOException e) {
      // Do something
    }
  }
}
Czasami zachodzi potrzeba, aby niektóre obiekty zareagowały na zmiany w innych obiektach i wtedy z pomocą przychodzi nam wzór „Obserwator” . Najwygodniejszym sposobem jest udostępnienie mechanizmu subskrypcji, który pozwala niektórym obiektom monitorować i reagować na zdarzenia zachodzące w innych obiektach. Ten wzorzec jest często używany u różnych Słuchaczy i Obserwatorów, którzy reagują na różne zdarzenia. Jako prosty przykład możemy przypomnieć implementację tego wzorca z pierwszej wersji JDK:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Observer observer = (obj, arg) -> {
      System.out.println("Arg: " + arg);
    };
    Observable target = new Observable(){
      @Override
      public void notifyObservers(Object arg) {
        setChanged();
        super.notifyObservers(arg);
      }
    };
    target.addObserver(observer);
    target.notifyObservers("Hello, World!");
  }
}
Istnieje jeszcze jeden przydatny wzorzec zachowania – „ Mediator ”. Jest to przydatne, ponieważ w złożonych systemach pomaga usunąć połączenie między różnymi obiektami i delegować wszelkie interakcje między obiektami do jakiegoś obiektu, który jest pośrednikiem. Jednym z najbardziej uderzających zastosowań tego wzorca jest Spring MVC, który wykorzystuje ten wzorzec. Więcej na ten temat przeczytasz tutaj: „ Wiosna: Wzór Mediatora ”. Często można to zobaczyć na przykładach java.util.Timer:
import java.util.*;
class Main {
  public static void main(String[] args) {
    Timer mediator = new Timer("Mediator");
    TimerTask command = new TimerTask() {
      @Override
      public void run() {
        System.out.println("Command pattern");
        mediator.cancel();
      }
    };
    mediator.schedule(command, 1000);
  }
}
Przykład wygląda bardziej jak wzorzec poleceń. A istota wzorca „Mediator” ukryta jest w implementacji Timer„a. Wewnątrz timera znajduje się kolejka zadań TaskQueuei wątek TimerThread. My, jako klienci tej klasy, nie wchodzimy z nimi w interakcję, lecz wchodzimy w interakcję z Timerobiektem, który w odpowiedzi na nasze wywołanie swoich metod uzyskuje dostęp do metod innych obiektów, których jest pośrednikiem. Zewnętrznie może wydawać się bardzo podobny do „Fasady”. Różnica polega jednak na tym, że gdy używana jest fasada, komponenty nie wiedzą, że fasada istnieje i komunikują się ze sobą. A gdy używany jest „Mediator”, komponenty znają i korzystają z pośrednika, ale nie kontaktują się ze sobą bezpośrednio. Warto zastanowić się nad wzorcem „ Metoda szablonowa ”, który wynika już z nazwy. Najważniejsze jest to, że kod jest napisany w taki sposób, że użytkownicy kodu (programiści) otrzymują pewien szablon algorytmu, którego kroki można redefiniować. Dzięki temu użytkownicy kodu nie mogą pisać całego algorytmu, a jedynie myśleć o tym, jak poprawnie wykonać jeden lub drugi krok tego algorytmu. Na przykład Java ma klasę abstrakcyjną AbstractList, która definiuje zachowanie iteratora poprzez List. Jednak sam iterator wykorzystuje metody liściowe, takie jak: get, set, remove. Zachowanie tych metod jest określane przez twórcę elementów potomnych AbstractList. Zatem iterator w AbstractList- jest szablonem algorytmu iteracji po arkuszu. Twórcy konkretnych implementacji AbstractListzmieniają zachowanie tej iteracji, definiując zachowanie określonych kroków. Ostatnim z analizowanych przez nas wzorców jest wzór „ Migawka ” (Momento). Jej istotą jest zachowanie określonego stanu obiektu z możliwością przywrócenia tego stanu. Najbardziej rozpoznawalnym przykładem z JDK jest serializacja obiektów, czyli tzw. java.io.Serializable. Spójrzmy na przykład:
import java.io.*;
import java.util.*;
class Main {
  public static void main(String[] args) throws IOException {
    ArrayList<String> list = new ArrayList<>();
    list.add("test");
    // Save State
    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    try (ObjectOutputStream out = new ObjectOutputStream(stream)) {
      out.writeObject(list);
    }
    // Load state
    byte[] bytes = stream.toByteArray();
    InputStream inputStream = new ByteArrayInputStream(bytes);
    try (ObjectInputStream in = new ObjectInputStream(inputStream)) {
      List<String> listNew = (List<String>) in.readObject();
      System.out.println(listNew.get(0));
    } catch (ClassNotFoundException e) {
      // Do something. Can't find class fpr saved state
    }
  }
}
Wzorce projektowe w Javie - 8

Wniosek

Jak widzieliśmy z recenzji, istnieje ogromna różnorodność wzorów. Każdy z nich rozwiązuje swój własny problem. Znajomość tych wzorców może pomóc w zrozumieniu na czas, jak napisać system tak, aby był elastyczny, łatwy w utrzymaniu i odporny na zmiany. I na koniec kilka linków do głębszego nurkowania: #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION