Logo MockIT na ciemnym tle.
Umów rozmowęBlog
Zaloguj się
Logo MockIT na ciemnym tle.
Logo MockIT.

Masz wrażenie, że nikt nie czyta Twojego CV? Nie dostajesz zaproszeń na rozmowy rekrutacyjne?

Sprawdź swoje umiejętności techniczne a MockIT pomoże Ci znaleźć pracę w IT!

Zapisz się na rozmowę
Powrót

Wzorce projektowe Java podstawy + przykładowe pytania rekrutacyjne

90% rozmów rekrutacyjnych z Javy zawiera pytania o wzorce projektowe. W tym artykule przybliżam najpopularniejsze z nich i wskazuje pytania rekrutacyjne na jakie możesz natrafić podczas rozmowy rekrutacyjnej.

Adrian Miś

Senior Java Developer - RightHub


17 lipca 2024

Intro

Wzorce projektowe (z ang. design patterns) stanowią fundament dobrze zaprojektowanego oprogramowania. Są gotowymi rozwiązaniami do powtarzających się problemów projektowych, które można zastosować w różnych kontekstach podczas tworzenia oprogramowania. Bez wątpienia, każdy programista Java powinien używać ich do tworzenia efektywnych rozwiązań programistycznych.

W niniejszym artykule przyjrzymy się podstawowym, a jednocześnie najpopularniejszym wzorcom projektowym w Javie. Omówimy ich kategorie oraz przedstawimy praktyczne przykłady ich zastosowania. Informacje, które starałem się tu zawrzeć osobiście wykorzystywałem w wielu rozmowach rekrutacyjnych, które dały mi pracę, więc Tobie też mogą pomóc :) Na końcu przedstawię kilka przykładowych pytań rekrutacyjnych, które mogą pojawić się na rozmowie kwalifikacyjnej.

Czym są wzorce projektowe?

To opisy lub szablony rozwiązania problemu, który występuje w określonym kontekście projektowym. Zostały one zebrane, sklasyfikowane i udokumentowane przez ekspertów w swoich dziedzinach, aby pomóc programistom w utrzymaniu kodu wysokiej jakości, w tym poprawić jego czytelność i elastyczność. Wdrażanie wzorców projektowych powinno ułatwiać i optymalizować kod, a nie niepotrzebnie kompikować projekt.

Cele wzorców projektowych

Ułatwienie komunikacji: dostarczają wspólnego języka dla programistów, co ułatwia komunikację i zrozumienie między członkami zespołu.

Usprawnienie procesu projektowania: dzięki wzorcom można szybciej znaleźć odpowiednie rozwiązanie problemu, co przyspiesza proces projektowania oprogramowania.

Zwiększenie jakości kodu: zastosowanie sprawdzonych wzorców projektowych prowadzi do bardziej zrozumiałego i utrzymywalnego kodu.

Kategorie wzorców projektowych

Wzorce kreacyjne

Wzorce kreacyjne koncentrują się na procesie tworzenia obiektów. Ich celem jest abstrakcja procesu instancjonowania, co pozwala na tworzenie obiektów w sposób dynamiczny i elastyczny, a co za tym idzie ułatwiają ponowne użycie kodu

Singleton

Zapewnia, że klasa ma tylko jedną instancję i globalny punkt dostępu do niej.

Factory Method

Definiuje interfejs do tworzenia obiektów, ale pozwala podklasom decydować, która klasa będzie instancjonowana.

Abstract Factory

Udostępnia interfejs do tworzenia rodzin powiązanych lub zależnych obiektów bez określania ich konkretnych klas.

Wzorce strukturalne

Wzorce strukturalne dotyczą kompozycji klas i obiektów, koncentrując się na uproszczeniu projektów przez rozwiązywanie problemów dotyczących strukturyzacji.

Adapter

Przekształca interfejs na inny interfejs oczekiwany przez klienta, umożliwiając współpracę klas, które w przeciwnym razie nie mogłyby ze sobą współpracować.

Bridge

Oddziela abstrakcję od jej implementacji, co pozwala na niezależny rozwój obu tych elementów.

Composite

Składa obiekty w strukturę drzewiastą, aby reprezentować hierarchie części i całości, umożliwiając klientom traktowanie pojedynczych obiektów i ich kompozycji w jednolity sposób.

Wzorce behawioralne

Wzorce behawioralne koncentrują się na komunikacji między obiektami i algorytmach, definiując sposób, w jaki obiekty współpracują i komunikują się ze sobą.

Observer

Definiuje zależność jeden-do-wielu między obiektami, tak aby zmiana stanu jednego obiektu skutkowała powiadomieniem i aktualizacją wszystkich zależnych obiektów.

Strategy

Definiuje rodzinę algorytmów, enkapsuluje je i sprawia, że są wymienne, pozwalając na dynamiczną zmianę algorytmu używanego przez klienta.

Command

Przekształca żądanie w obiekt, co pozwala na parametryzowanie obiektów innymi żądaniami, kolejkowanie lub logowanie żądań oraz obsługę operacji undo.

Podstawowe wzorce projektowe Java

Przedstawię Ci tutaj kilka wzorców projektowych, które są naprawdę proste do nauczenia i zrozumienia, a dzięki nim na pewno zabłyśniesz na rozmowie rekrutacyjnej ;)

Singleton

Należy do kategorii wzorców kreacyjnych i jego celem jest zapewnienie, że dana klasa posiada tylko jedną instancję oraz udostępnienie globalnego punktu dostępu do tej instancji. Jest używany w sytuacjach, gdzie konieczne jest jedno, wspólne źródło danych lub jeden punkt koordynacji, np. w systemach logowania, zarządzania konfiguracją, połączeń z bazą danych czy w zarządzaniu zasobami zewnętrznymi.

Kluczowe cechy

Jedna instancja: gwarantuje, że klasa posiada tylko jedną instancję.

Globalny dostęp: umożliwia globalny dostęp do tej instancji.

Kontrola nad instancją: zapewnia kontrolę nad procesem tworzenia instancji.

Implementacja w Javie

public class SingleInstance { private static SingleInstance instance; // Private constructor prevents the creation of external objects. private SingleInstance() { } // Public method that returns a single instance of Singleton public static synchronized SingleInstance getInstance() { if (instance == null) { instance = new SingleInstance(); } return instance; } }

W powyższym przykładzie:

Prywatny konstruktor: zapobiega tworzeniu instancji z zewnątrz.

Statyczna metoda getInstance: zapewnia globalny punkt dostępu do jedynej instancji. Napisanie słowa kluczowego Synchronized zapewnia,że metoda jest bezpieczna w środowiskach wielowątkowych. 

Zarządzanie konfiguracją

Singleton może być używany do przechowywania i udostępniania ustawień konfiguracyjnych aplikacji, zapewniając, że wszystkie części aplikacji korzystają z tych samych ustawień.

public class ConfigurationManager { private static ConfigurationManager instance; private Properties config; private ConfigurationManager() { config = new Properties(); // Load properties from a file // config.load(new FileInputStream("config.properties")); } public static synchronized ConfigurationManager getInstance() { if (instance == null) { instance = new ConfigurationManager(); } return instance; } public String getProperty(String key) { return config.getProperty(key); } }

Singleton w Spring Framework

Spring Framework automatycznie zarządza beanami w kontekście aplikacji jako singletonami. Domyślnie, każdy bean w Springu jest singletonem, co oznacza, że Spring zarządza pojedynczą instancją beana w całym kontekście aplikacji.

Przykład definicji beana jako singleton w konfiguracji Springa:

@Configuration public class AppConfig { @Bean public MyService myService() { return new MyService(); } }

W Springu zawsze można stworzyć Singleton w bardzo prosty sposób, wystarczy dodać adnotacje @Repository, @Service, @Controller, @Component̨, a kontener Springa zajmie się resztą.

Zalety Singletona

Globalny dostęp: umożliwia dostęp do tej samej instancji z dowolnego miejsca w aplikacji.

Kontrola zasobów: zapewnia centralizację zarządzania zasobami, takimi jak konfiguracje czy połączenia z bazą danych.

Łatwość implementacji: prosta i intuicyjna konstrukcja.

Wady Singletona

Problemy z testowaniem: trudność w testowaniu jednostkowym z powodu globalnego stanu, który może być trudny do resetowania między testami.

Skłonność do nadużywania: może prowadzić do nadmiernego używania globalnego stanu, co komplikuje utrzymanie i zrozumienie kodu.

Wielowątkowość: może być problematyczny w środowiskach wielowątkowych, jeśli nie jest odpowiednio zaimplementowany (konieczne są mechanizmy synchronizacji).

Podsumowanie

Singleton jest kluczowym narzędziem w arsenale programisty oferując prosty sposób zarządzania unikalnymi instancjami w aplikacji. Choć ma swoje ograniczenia i potencjalne wady, jego odpowiednie użycie może znacząco uprościć zarządzanie zasobami i konfiguracjami, zwłaszcza w złożonych systemach.W Spring Framework, zarządzanie singletonami jest jeszcze prostsze dzięki automatycznemu zarządzaniu beanami, co dodatkowo zwiększa efektywność i przejrzystość kodu.

Wzorzec Wrapper

Wrapper jest wzorcem strukturalnym, który służy do opakowywania jednego interfejsu w inny, kompatybilny interfejs. Jest używany w sytuacjach, gdzie istnieją różnice w interfejsach lub oczekiwaniach między różnymi częściami systemu lub modułami.

Użycie w Javie: Integer vs int

W języku Java Integer jest przykładem Wrappera. Integer jest klasą opakowującą typ prymitywny int. Oto różnice między nimi:

int: typ prymitywny reprezentujący liczby całkowite (np. 1, 2, -3).

Integer: klasa opakowująca dla int, która zapewnia dodatkowe funkcjonalności, takie jak metody pomocnicze, możliwość używania w kontekście generycznym i automatyczne konwertowanie do/z typu prymitywnego. Także możliwe jest tutaj przechowywanie null’a oraz wykonywanie takich funkcji jak equals() czy toString().

int primitiveInt = 10; Integer wrappedInt = new Integer(10); // Can be used in a generic context List<Integer> integerList = new ArrayList<>(); integerList.add(1); integerList.add(2); // Automatic conversion between int and Integer int intValue = wrappedInt.intValue();

Tworzenie prostego Wrappera w biznesowym kontekście

Załóżmy, że mamy dwa niepowiązane obiekty w naszej aplikacji, które biznesowo muszą być traktowane jako jeden, na przykład customer i order. Możemy użyć Wrappera, aby opakować te dwa obiekty w jeden.
public class CustomerOrderWrapper { private Customer customer; private Order order; public CustomerOrderWrapper(Customer customer, Order order) { this.customer = customer; this.order = order; } public Customer getCustomer() { return customer; } public Order getOrder() { return order; } }

W powyższym przykładzie CustomerOrderWrapper opakowuje obiekty customer i order w jeden wrapper, umożliwiając operowanie na obu obiektach jednocześnie w aplikacji.

Zalety Wrapperów

Adaptacja interfejsów: umożliwia adaptację istniejących interfejsów lub klas do nowych wymagań.

Łatwość używania różnych typów danych: Wrappery pozwalają na manipulowanie danymi w bardziej elastyczny sposób, w tym konwersję między różnymi typami.

Wady Wrapperów

Dodatkowy poziom abstrakcji: może wprowadzać dodatkowy poziom abstrakcji i złożoności w kodzie.

Overhead związany z opakowywaniem: może wprowadzać nieznaczny narzut wydajnościowy związany z tworzeniem i zarządzaniem obiektami wrapperów.

Podsumowanie

Wrapper jest używany do adaptacji i opakowywania istniejących interfejsów lub obiektów w inny sposób, aby spełniały nowe wymagania aplikacji. W kontekście języka java Integer jest przykładem wrappera dla typu prymitywnego int, a przykładowe użycie może obejmować opakowywanie niepowiązanych obiektów biznesowych w jedną strukturę danych.

Wzorzec Iterator

Iterator to wzorzec behawioralny, który umożliwia sekwencyjny dostęp do elementów kolekcji bez ujawniania jej wewnętrznej reprezentacji. Iterator pozwala na iterację po elementach kolekcji i operowanie nimi bez znajomości szczegółów implementacyjnych kolekcji.

Użycie w Javie (kolekcje)

W Javie Iterator jest powszechnie stosowany w interfejsach kolekcji, takich jak List, Set czy Map. Oto jak korzystać z Iteratora w praktyce:
List<String> list = new ArrayList<>(); list.add("Element 1"); list.add("Element 2"); list.add("Element 3"); // Obtaining an iterator Iterator<String> iterator = list.iterator(); // Iteration and display of elements while (iterator.hasNext()) { String element = iterator.next(); System.out.println(element); }

W powyższym przykładzie iterator() zwraca Iterator dla listy ArrayList, który może być używany do sekwencyjnego przechodzenia przez elementy listy za pomocą metod hasNext() i next()

Implementacja własnego Iteratora

Możemy również stworzyć własny Iterator dla niestandardowej kolekcji. Oto przykład prostego Iteratora dla niestandardowej klasy CustomCollection:
public class CustomCollection { private String[] elements = {"A", "B", "C"}; public Iterator<String> iterator() { return new CustomIterator(); } private class CustomIterator implements Iterator<String> { private int index = 0; @Override public boolean hasNext() { return index < elements.length; } @Override public String next() { if (!hasNext()) { throw new NoSuchElementException(); } return elements[index++]; } } }

W tym przykładzie CustomCollection zawiera niestandardową tablicę elements, a CustomIterator jest zagnieżdżoną klasą implementującą interfejs Iterator. Dzięki temu możemy iterować po elementach CustomCollection tak samo jak po standardowych kolekcjach.

Zalety Iteratora

Oddzielenie interfejsu od implementacji: Iterator umożliwia dostęp do elementów kolekcji bez ujawniania szczegółów jej struktury.

Uniwersalność: umożliwia zastosowanie tych samych operacji iteracyjnych dla różnych rodzajów kolekcji.

Zastosowanie Iteratora

Przeglądanie i operowanie na elementach kolekcji: Iterator jest używany wszędzie tam, gdzie potrzebujemy sekwencyjnego dostępu do elementów kolekcji bez względu na jej konkretną implementację.

Wielokrotne przetwarzanie danych: Iterator pozwala wielokrotnie przechodzić przez te same dane bez konieczności kopiowania ich lub modyfikowania struktury kolekcji.

Podsumowanie

Wzorzec Iterator jest niezwykle użyteczny w programowaniu obiektowym, umożliwiając bezpieczny i uporządkowany dostęp do elementów kolekcji. Jego implementacja w Javie jest powszechna w interfejsach kolekcji, co ułatwia programistom pracę z różnymi typami danych.Dzięki Iteratorowi możemy tworzyć bardziej elastyczne i modułowe aplikacje, które łatwiej się utrzymuje i rozwija. Jest to jeden z tych wzorców, których używasz, a nie masz świadomości, że tak naprawdę to robisz.

Przykładowe pytania rekrutacyjne

  • Czym jest Singleton? Jak go zaimplementować? Jakie są jego wady i zalety?

  • Co to jest wzorzec Factory Method? Jakie są jego zastosowania?

  • Różnica między wzorcami metody wytwórczej (Factory Method) a  fabryką abstrakcyjną (Abstract Factory)?

  • Czym są wzorce strukturalne? Podaj przykłady.

  • Opisz wzorzec MVC (Model-View-Controller). Jakie są jego zalety?

  • Jakie są wzorce behawioralne? Podaj przykłady i opisz ich zastosowania.

  • Jakie są zastosowania wzorca Builder? Jak go zaimplementować w praktyce?

  • Przy użyciu jakiej biblioteki możemy szybko i łatwo skorzystać z wzorca Builder?

  • Czym jest wzorzec Proxy? Jakie są jego zastosowania?

  • Jakie są główne różnice między wzorcami Immutable i Mutable?

  • Jakie są najczęściej występujące wzorce w Springu? Podaj przykłady i wytłumacz działanie.

Podsumowanie

Mam nadzieję, że wraz z przeczytaniem artykułu przybędzie zrozumienie tego ważnego, lecz wcale nie tak trudnego tematu.

Jeśli wiążesz swoją karierę z programowaniem to wzorce projektowe będą towarzyszyć Ci, aż do jej końca, więc warto poświęcić trochę czasu na ich naukę, ponieważ będziesz ich używał w swojej codziennej pracy podczas kodowania.

Na rozmowach technicznych na stanowiska Deweloperskie, a w szczególności Javowe pytania o wzorce projektowe zdarzają się w 90% przypadków, także uczyć się warto ;)


Mogą Cię też zainteresować

© 2024 MockIT