Wzorce Projektowe

Gdy uczymy się programowania i konsekwentnie rozszerzamy swoją wiedzę w tym zakresie, to jest pewne, że spotkamy się z wzorcami projektowymi. Czym one są? Do czego służą? I czy po ich opanowaniu, korzysta się z nich w każdym przypadku?

Zacznijmy od tego, że każdy program jest tworzony w jakimś celu – ma do wykonania jakieś zadanie. Podczas jego pisania programista napotka na pewne problemy (mniejsze lub większe), które musi rozwiązać. Może on sobie nie zdawać sprawy, że na te same problemy natknęli się też inni programiści, pisząc zupełnie inne programy. Jeśli zastosowane przez niego rozwiązania sprawdzą się także u innych programistów, to właśnie te rozwiązania nazwiemy Wzorcami Projektowymi. Innymi słowy – to katalog najlepszych praktyk z gotowymi rozwiązaniami dla często pojawiających się problemów projektowych.

Trzeba pamiętać, że wzorce projektowe to nie prawa czy sztywne reguły – jedynie zalecenia. To zadaniem programisty jest ich adaptacja do potrzeb konkretnej aplikacji.

Sprawdzonych wzorców jest dużo i wciąż powstają nowe. W tym artykule bardziej szczegółowo omówię tylko 3 z nich.


1) Wzorzec Fabryka

Wzorzec Fabryka pozwala na tworzenie w uporządkowany sposób różnych obiektów bez ujawniania logiki ich powstawania, które implementują ten sam interfejs (nie musi to być koniecznie interfejs, może to być też klasa abstrakcyjna). Wymaganych obiektów nie tworzymy bezpośrednio lecz „zgłaszamy się” do specjalnej klasy (często nazywanej Fabryka), która wytworzy je za nas. To w jaki sposób fabryka je konstruuje pozostaje dla nas niewidoczne. Ten wzorzec dostarcza jeden z najlepszych sposobów na tworzenie obiektu.

Rozważmy następujący przykład:
Tworzymy winiarnię i zamawiamy u niej trzy rodzaje win (czerwone, białe i różowe). Dla uproszczenia założymy, że istnieje tylko jeden rodzaj każdego z nich:
– Czerwone Rosso Italiano 2020, półwytrawne w cenie 35 zł.
– Białe Androsela 2019, półsłodkie w cenie 39 zł.
– Różowe Espanillo Rosado 2019, wytrawne w cenie 33 zł.
Mamy trzy pola: nazwa, smak i cena.

Przykładowa implementacja może wyglądać tak:
Krok 1 – Tworzymy klasę Wino.

public class Wine {
    private String name;
    private String flavor;
    private double price;

    public Wine(String name, String flavor, double price) {
        this.name = name;
        this.flavor = flavor;
        this.price = price;
    }

    public String getName() {
        return name;
    }

    public String getFlavor() {
        return flavor;
    }

    public double getPrice() {
        return price;
    }
}

Krok 2 – Tworzymy klasę Winiarnia, w której kupujemy wina.

public class Winery {
    public static void main(String[] args) {
        Wine redWine = new Wine("Rosso Italiano 2020", "semidry", 35.0);
        System.out.println("You bought: ");
        System.out.print("Red ");
        System.out.println(redWine.getName() + ", " + redWine.getFlavor() + ", for " + redWine.getPrice() +" PLN");

        Wine whiteWine = new Wine("Androsela 2019", "semisweet", 39.0);
        System.out.print("White ");
        System.out.println(whiteWine.getName() + ", " + whiteWine.getFlavor() + ", for " + whiteWine.getPrice() +" PLN");

        Wine pinkWine = new Wine("Espanillo Rosado 2019", "dry", 33.0);
        System.out.print("Pink ");
        System.out.println(pinkWine.getName() + ", " + pinkWine.getFlavor() + ", for " + pinkWine.getPrice() +" PLN");
    }
}

Weryfikacja wyniku:

You bought: 
Red Rosso Italiano 2020, semidry, for 35.0 PLN
White Androsela 2019, semisweet, for 39.0 PLN
Pink Espanillo Rosado 2019, dry, for 33.0 PLN

Poprzez proste zastosowanie operatora new tworzymy wymagane obiekty każdego rodzaju wina. I w naszym przykładzie wszystko jest w porządku, ale gdy mamy bardziej skomplikowaną sytuację (lub aplikację), to tworzenie nowych obiektów w ten sposób nie zawsze powinno być publicznie dostępne, co więcej, może to prowadzić do problemów z powiązaniami obiektowymi.

Użyjmy teraz Wzorca Fabryka, aby poprawić nasz program.

Krok 1 – Tworzymy interfejs Wino.

public interface Wine {
    String getName();
    String getFlavor();
    double getPrice();
}

Krok 2 – Tworzymy rzeczywiste klasy implementujące ten sam interfejs.

public class RedWine implements Wine {

    @Override
    public String getName() {
        return "Rosso Italiano 2020";
    }

    @Override
    public String getFlavor() {
        return "semidry";
    }

    @Override
    public double getPrice() {
        return 35.0;
    }
}

public class WhiteWine implements Wine {

    @Override
    public String getName() {
        return "Androsela 2019";
    }

    @Override
    public String getFlavor() {
        return "semisweet";
    }

    @Override
    public double getPrice() {
        return 39.0;
    }
}

public class PinkWine implements Wine {

    @Override
    public String getName() {
        return "Espanillo Rosado 2019";
    }

    @Override
    public String getFlavor() {
        return "dry";
    }

    @Override
    public double getPrice() {
        return 33.0;
    }
}

Krok 3 – Tworzymy Winiarnię (Fabrykę) do wytworzenia obiektu konkretnej klasy (wina).

public class WineFactory {
    public Wine buyWine(String type) {
        if (type.equals("Red")) {
            return new RedWine();
        } else if (type.equals("White")) {
            return new WhiteWine();
        } else if (type.equals("Pink")) {
            return new PinkWine();
        } else {
            return null;
        }
    }
}

Krok 4 – Używamy Fabryki, aby utworzyć wino (konkretny obiekt) bazując na jego kolorze.

public class FactoryPatternDemo {
    public static void main(String[] args) {
        WineFactory wine = new WineFactory();

        Wine red = wine.buyWine("Red");
        System.out.println("You bought red wine:");
        System.out.println("Name: " + red.getName());
        System.out.println("Flavor: " + red.getFlavor());
        System.out.println("Price: " + red.getPrice() + " PLN");

        System.out.println();

        Wine white = wine.buyWine("White");
        System.out.println("You have chosen white wine:");
        System.out.println(white.getName() + ", " + white.getFlavor() + ", " + white.getPrice() + " PLN");

        System.out.println();

        Wine pink = wine.buyWine("Pink");
        System.out.println("You bought pink " + pink.getFlavor() + " wine " + pink.getName() + " for " + pink.getPrice()
                + " PLN");
    }
}

Weryfikacja wyniku:

You bought red wine:
Name: Rosso Italiano 2020
Flavor: semidry
Price: 35.0 PLN

You have chosen white wine:
Androsela 2019, semisweet, 39.0 PLN

You bought pink dry wine Espanillo Rosado 2019 for 33.0 PLN

Jak widać Wzorzec Fabryka w prosty i skuteczny sposób oddziela nam klienta od implementacji klas rzeczywistych.


2) Wzorzec Obserwator

Wzorzec Obserwator stosujemy, kiedy mamy relację jeden-do-wielu pomiędzy obiektami danego zbioru obiektów. Gdy wybrany obiekt (nazywany Podmiotem lub Obiektem Obserwowanym) zmieni swój stan, to wszystkie obiekty, które są od niego zależne (Obserwatorzy) zostają o tym powiadomione i zaktualizowane.

Rozważmy prosty przykład konwersji liczb na inny system liczbowy.
Tworzymy klasę systemy liczbowe i dokonujemy w niej konwersji korzystając z osobnych funkcji, gdzie przekazujemy do nich liczbę jako argument.

Przykładowa implementacja może wyglądać tak:

public class NumberSystemsDemo {
    public static void main(String[] args) {
        System.out.println("First state change: 15");
        System.out.print("Binary String: ");
        changeIntToBinary(15);
        System.out.print("Octal String: ");
        changeIntToOctal(15);
        System.out.print("Hexadecimal String: ");
        changeIntToHexadecimal(15);

        System.out.println();

        System.out.println("Second state change: 10");
        System.out.print("Binary String: ");
        changeIntToBinary(10);
        System.out.print("Octal String: ");
        changeIntToOctal(10);
        System.out.print("Hexadecimal String: ");
        changeIntToHexadecimal(10);
    }

    public static void changeIntToBinary(int number) {
        System.out.println(Integer.toBinaryString(number));
    }

    public static void changeIntToOctal(int number) {
        System.out.println(Integer.toOctalString(number));
    }

    public static void changeIntToHexadecimal(int number) {
        System.out.println(Integer.toHexString(number).toUpperCase());
    }
}

Weryfikacja wyniku:

First state change: 15
Binary String: 1111
Octal String: 17
Hexadecimal String: F

Second state change: 10
Binary String: 1010
Octal String: 12
Hexadecimal String: A

Program działa poprawnie, ale każda zmiana stanu (inna liczba) wymaga od nas ręcznego uruchomienia odpowiednich funkcji. Nie jest to wydajne rozwiązanie.

Spróbujemy teraz użyć Wzorca Obserwator do naszego programu.

Krok 1 – Tworzymy interfejsy Podmiot i Obserwator.

public interface Subject {
    public void registerObserver(Observer o);
    public void removeObserver(Observer o);
    public void notifyAllObservers();
}

public interface Observer {
    void update();
}

Krok 2 – Tworzymy konkretnych obserwatorów.

public class BinaryObserver implements Observer {
    SomeSubject subject;

    public BinaryObserver(SomeSubject subject) {
        this.subject = subject;
        this.subject.registerObserver(this);
    }

    @Override
    public void update() {
        System.out.println("Binary String: " + Integer.toBinaryString(subject.getState()));
    }
}

public class OctalObserver implements Observer {
    SomeSubject subject;

    public OctalObserver(SomeSubject subject) {
        this.subject = subject;
        this.subject.registerObserver(this);
    }

    @Override
    public void update() {
        System.out.println("Octal String: " + Integer.toOctalString(subject.getState()));
    }
}

public class HexadecimalObserver implements Observer {
    SomeSubject subject;

    public HexadecimalObserver(SomeSubject subject) {
        this.subject = subject;
        this.subject.registerObserver(this);
    }

    @Override
    public void update() {
        System.out.println("Hexadecimal String: " + Integer.toHexString(subject.getState()).toUpperCase());
    }
}

Krok 3 – Tworzymy klasę obiekt obserwowany.

import java.util.ArrayList;

public class SomeSubject implements Subject {
    private ArrayList observers;
    private int state;

    public SomeSubject() {
        observers = new ArrayList();
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
        notifyAllObservers();
    }

    @Override
    public void registerObserver(Observer o) {
        observers.add(o);
    }

    @Override
    public void removeObserver(Observer o) {
        observers.remove(o);
    }

    @Override
    public void notifyAllObservers() {
        for (int i = 0; i < observers.size(); i++) {
            Observer Obs = (Observer) observers.get(i);
            Obs.update();
        }
    }
}

Krok 4 – Używamy klasy Obiekt Obserwator do tworzenia, usuwania i powiadamiania obserwatorów, gdy zmienia się stan (liczba).

public class ObserverPatternDemo {
    public static void main(String[] args) {

        SomeSubject subject = new SomeSubject();
        Observer obs1 = new BinaryObserver(subject);
        Observer obs2 = new OctalObserver(subject);
        Observer obs3 = new HexadecimalObserver(subject);

        System.out.println("First state change: 15");
        subject.setState(15);
        System.out.println();
        System.out.println("Second state change: 10");
        subject.setState(10);
        System.out.println();

        subject.removeObserver(obs2);
        System.out.println("Third state change: 59");
        subject.setState(59);
    }
}

Weryfikacja wyniku:

First state change: 15
Binary String: 1111
Octal String: 17
Hexadecimal String: F

Second state change: 10
Binary String: 1010
Octal String: 12
Hexadecimal String: A

Third state change: 59
Binary String: 111011
Hexadecimal String: 3B

Zauważmy, że przy trzeciej zmianie stanu (liczba 59) brakuje nam konwersji ósemkowej (octal). Dzieje się tak, gdyż wcześniej usunęliśmy jego obserwatora (obs2) z grupy obiektów obserwujących nasz podmiot (liczbę do konwersji). Natomiast widzimy, że przy każdej zmianie stanu (liczby) automatycznie powiadamiamy o tym wszystkich obserwatorów, co powoduje uruchomienie odpowiedniej konwersji. Wykorzystanie Wzorca Obserwator w naszym przykładzie jest o wiele bardziej wydajniejsze.


3) Wzorzec Flyweight

Wzorzec Flyweight stosuje się wszędzie tam, gdzie wymagane jest stworzenie wielu obiektów danej klasy, a wszystkie mogą być sterowane w identyczny sposób. Jeden egzemplarz takiej klasy może zostać później wykorzystany do stworzenia wielu „egzemplarzy wirtualnych”. Oszczędzamy w ten sposób pamięć aplikacji, ponieważ redukujemy liczbę tworzonych obiektów.

Rozważmy następujący przykład.
„Narysujmy” 10 kół w różnych miejscach (wykorzystamy losowe współrzędne x i y oraz promień). Ustawimy też 5 dostępnych kolorów dla tych kół.

Przykładowa Implementacja może wyglądać tak:
Krok 1 – Tworzymy klasę Koło.

public class Circle {
    private String color;
    private int x;
    private int y;
    private int r;

    public Circle(String color) {
        this.color = color;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setRadius(int r) {
        this.r = r;
    }

    public void draw() {
        System.out.println("Drawing circle - Color: " + color + ", x: " + x + ", y: " + y + ", r: " + r);
    }
}

Krok 2 – Tworzymy klasę główną, w której „rysujemy” koła.

public class DrawingCirclesDemo {
    private static final String colors[] = {"Red", "Blue", "Yellow", "White", "Black"};

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            String color = getRandomColor();
            Circle circle = new Circle(color);
            System.out.println("Creating circle of color: " + color);
            circle.setX(getRandomX());
            circle.setY(getRandomY());
            circle.setRadius(getRandomRadius());
            circle.draw();
        }
    }

    private static String getRandomColor() {
        return colors[(int)(Math.random() * colors.length)];
    }

    private static int getRandomX() {
        return (int)(Math.random() * 100);
    }

    private static int getRandomY() {
        return (int)(Math.random() * 10);
    }

    private static int getRandomRadius() {
        return (int)(Math.random() * 100);
    }
}

Weryfikacja wyniku:

Creating circle of color: White
Drawing circle - Color: White, x: 23, y: 5, r: 11
Creating circle of color: White
Drawing circle - Color: White, x: 51, y: 2, r: 48
Creating circle of color: White
Drawing circle - Color: White, x: 59, y: 1, r: 97
Creating circle of color: White
Drawing circle - Color: White, x: 1, y: 0, r: 22
Creating circle of color: White
Drawing circle - Color: White, x: 18, y: 6, r: 16
Creating circle of color: Blue
Drawing circle - Color: Blue, x: 30, y: 6, r: 9
Creating circle of color: Black
Drawing circle - Color: Black, x: 49, y: 7, r: 8
Creating circle of color: Black
Drawing circle - Color: Black, x: 21, y: 9, r: 90
Creating circle of color: Yellow
Drawing circle - Color: Yellow, x: 16, y: 5, r: 30
Creating circle of color: Blue
Drawing circle - Color: Blue, x: 44, y: 9, r: 24

Jak widać za każdym razem tworzymy nowy obiekt tego samego typu (koloru), co nie jest dobrym rozwiązaniem.

Użyjmy teraz Wzorca Flyweight do naszego przykładu.

Krok 1 – Tworzymy interfejs Kształt.

public interface Shape {
    void draw();
}

Krok 2 – Tworzymy konkretną klasę Koło, implementującą stworzony interfejs.

public class Circle implements Shape {
    private String color;
    private int x;
    private int y;
    private int r;

    public Circle(String color) {
        this.color = color;
    }

    public void setX(int x) {
        this.x = x;
    }

    public void setY(int y) {
        this.y = y;
    }

    public void setRadius(int r) {
        this.r = r;
    }

    @Override
    public void draw() {
        System.out.println("Drawing circle - Color: " + color + ", x: " + x + ", y: " + y + ", r: " + r);
    }
}

Krok 3 – Tworzymy klasę Fabryka Kształtów do stworzenia obiektu klasy Koło.

import java.util.HashMap;

public class ShapeFactory {
    private static final HashMap circleMap = new HashMap();

    public static Shape getCircle(String color) {
        Circle circle = (Circle) circleMap.get(color);

        if (circle == null) {
            circle = new Circle(color);
            circleMap.put(color, circle);
            System.out.println("Creating circle of color: " + color);
        }
        return circle;
    }
}

Krok 4 – Używamy fabryki do otrzymania obiektu danej klasy, przekazując określony argument (w naszym przykładzie będzie to kolor).

public class FlyweightPatternDemo {
    private static final String colors[] = {"Red", "Blue", "Yellow", "White", "Black"};
    public static void main(String[] args) {

        for (int i = 0; i < 10; ++i) {
            Circle circle = (Circle) ShapeFactory.getCircle(getRandomColor());
            circle.setX(getRandomX());
            circle.setY(getRandomY());
            circle.setRadius(getRandomRadius());
            circle.draw();
            System.out.println();
        }

    }

    private static String getRandomColor() {
        return colors[(int)(Math.random() * colors.length)];
    }

    private static int getRandomX() {
        return (int)(Math.random() * 100);
    }

    private static int getRandomY() {
        return (int)(Math.random() * 10);
    }

    private static int getRandomRadius() {
        return (int)(Math.random() * 100);
    }
}

Weryfikacja wyniku:

Creating circle of color: White
Drawing circle - Color: White, x: 72, y: 5, r: 5

Creating circle of color: Black
Drawing circle - Color: Black, x: 25, y: 3, r: 68

Drawing circle - Color: White, x: 80, y: 2, r: 1 

Drawing circle - Color: White, x: 31, y: 8, r: 86

Creating circle of color: Blue 
Drawing circle - Color: Blue, x: 16, y: 9, r: 31  

Drawing circle - Color: Blue, x: 78, y: 9, r: 24

Drawing circle - Color: Blue, x: 68, y: 8, r: 34

Drawing circle - Color: Blue, x: 72, y: 0, r: 46

Drawing circle - Color: White, x: 24, y: 4, r: 38

Creating circle of color: Yellow
Drawing circle - Color: Yellow, x: 10, y: 0, r: 60

Zobaczmy, że najpierw stworzyliśmy nowy obiekt (białe koło). Następnie je narysowaliśmy. Podobnie z czarnym kolorem. Gdy potem wylosowaliśmy ponownie biały kolor, to już nie tworzymy nowego obiektu koła białego, tylko wykorzystujemy już istniejący jego obiekt (ale z innymi wartościami współrzędnych i promienia). Potem tworzymy nowy obiekt niebieskiego koła, ponieważ wcześniej nie mieliśmy koła o tej barwie. I podobnie jak przy białym kole, kolejne niebieskie są rysowane, używając już istniejącego obiektu niebieskie koło. Innymi słowy narysowaliśmy 10 okręgów, tworząc tylko 4 obiekty.

Podobne wpisy

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *