Wzorce projektowe
w języku Python

Krystian Piękoś

Filary OOP

Abstrakcja

  • Abstrakcja umożliwia wyodrębnienie elementów charakteryzujących dany obiekt, które są istotne dla rozwiązywanego problemu
     

  • Każdy obiekt w systemie służy jako model abstrakcyjnego “wykonawcy”, który może wykonywać pracę, opisywać i zmieniać swój stan oraz komunikować się z innymi obiektami w systemie, bez ujawniania, w jaki sposób zaimplementowano dane cechy.

Abstrakcja

An abstraction denotes the essential characteristics of an object that distinguish it from all other kinds of object and thus provide crisply defined conceptual boundaries, relative to the perspective of the viewer

G. Booch, Object-Oriented Design With Applications

Enkapsulacja

  • Każdy typ obiektu prezentuje innym obiektom swój “interfejs”, który określa dopuszczalne metody współpracy
     
  • Zewnętrzne obiekty nie powinny manipulować stanem obiektu z którym współpracują
     
  • Zamiast tego powinny zlecać wykonanie odpowiedniej operacji przy pomocy metody będącej częścią interfejsu - zasada Tell, Don’t Ask

Enkapsulacja

class BankAccount:
    def __init__(self, id, balance):
        self.__id = id
        self.__balance = balance

    @property
    def id():
        return self.__id

    @property
    def balance(self):
        return self.__balance

    def deposit(self, amount):
        # validation
        #...
        self.__balance += amount

    def withdraw(self, amount):
        # validation
        #...
        self.__balance -= amount

Polimorfizm

  • Referencje i kolekcje obiektów mogą dotyczyć obiektów różnego typu, a wywołanie metody dla referencji spowoduje zachowanie odpowiednie dla pełnego typu obiektu wywoływanego

  • Jeśli dzieje się to w czasie działania programu, to nazywa się to późnym wiązaniem lub wiązaniem dynamicznym

  • Python umożliwia polimorficzne wykorzystanie obiektów poprzez tzw. duck typing

     

Duck typing

  • Umożliwia użycie przez klienta obiektu dowolnego typu, jeżeli tylko obiekt ten implementuje używane metody
     

  • Polimorfizm w Pythonie wykorzystuje protokoły

    • protokół jest nieformalnym interfejsem, który jest wymagany przez klienta

    • protokół najczęściej zdefiniowany jest jedynie w dokumentacji klasy lub metody

Kompozycja i dziedziczenie

  • Podstawowe techniki umożliwiające ponowne wykorzystanie napisanego już kodu
     
  • Kompozycja - umożliwia składanie obiektów
     
  • Dziedziczenie - umożliwia definiowanie i tworzenie wyspecjalizowanych typów (a potem obiektów) na podstawie  typów bardziej ogólnych

Obiekt - Interfejs - Klasa

Obiekt

  • Jest elementem zawierającym zarówno dane, jak i funkcje na nich działające (metody lub operacje)
     
  • Wykonuje operację po otrzymaniu żądania (lub komunikatu) od klienta (innego obiektu)
     
  • Wewnętrzny stan obiektu jest zaenkapsulowany

Interfejs

  • Określa kompletny zbiór żądań, jakie mogą być wysyłane do obiektu
     
  • Interfejs obiektu nie mówi nic o implementacji obiektu – różne obiekty mają prawo różnie implementować żądania

Klasa

  • Implementacja obiektu jest zdefiniowana przez jego klasę:

    • klasa specyfikuje wewnętrzne dane obiektu i ich reprezentację oraz definiuje operacje, które obiekt może wykonywać

    • obiekty powstają w wyniku tworzenia egzemplarzy klas

    • za pomocą dziedziczenia klas można definiować nowe klasy w kategoriach klas już istniejących

ABC

Klasy abstrakcyjne

ABC

An abstract class represents an interface.

 

Bjarne Stroustrup, creator of C++

ABC

  • Abstrakcyjne klasy bazowe (ABC) umożliwiają silniejszą kontrolę typów (interfejsu) niż sprawdzanie implementacji poszczególnych metod przy pomocy hasattr()
     
  • Służą do definicji interfejsu pełniącego rolę API, który jest wykorzystywany przez obiekty klienckie
     
  • Są szczególnie przydatne do tworzenia framework’ów lub plugin’ów.

ABC - Goose Typing

Polega na:

  • jawnej deklaracji implementacji danego interfejsu
    • dziedziczeniu po klasach ABC
    • rejestracja ABC
  • wykorzystaniu ABC do sprawdzenia typu obiektu w czasie wykonania: isinstance lub issubclass

Klasa ABC

Coord = namedtuple('Coord', 'x,y')

class Shape(abc.ABC):

    def __init__(self, x, y):
        self.__coord = Coord(x, y)

    @property
    @abc.abstractmethod
    def coordinates(self):
        return self.__coord

    @abc.abstractmethod
    def move(self, dx, dy):
        self.__coord = Coord(self.__coord.x + dx, self.__coord.y + dy)

    @abc.abstractmethod
    def draw(self):
        """Abstract method - must be overriden in subclass"""

ABC - dziedziczenie

class Rectangle(Shape):

    def __init__(self, x, y, width, height):
        super().__init__(x, y)
        self.__width = width
        self.__height = height

    @property
    def width(self):
        return self.__width

    @width.setter
    def width(self, new_width):
        self.__width = new_width

    @property
    def height(self):
        return self.__height

    @height.setter
    def height(self, new_height):
        self.__height = new_height

    @property
    def coordinates(self):
        return super().coordinates

    def move(self, dx, dy):
        super().move(dx, dy)

    def draw(self):
        print("Drawing a rectangle at {} with width {} and height {}"
                .format(self.coordinates, self.width, self.height))

ABC - rejestracja

@Shape.register
class Circle:

    def __init__(self, x, y, radius):
        self.__x = x
        self.__y = y
        self.__radius = radius

    @property
    def radius(self):
        return self.__radius

    @radius.setter
    def radius(self, new_radius):
        self.__radius = new_radius

    @property
    def coordinates(self):
        return Coord(self.__x, self.__y)

    def move(self, dx, dy):
        self.__x += dx
        self.__y += dy

    def draw(self):
        print("Drawing a circle at {} with radius {}".format(self.coordinates, self.radius))

ABC - runtime checks

>>> r = Rectangle(0, 15, 10, 20)

>>> issubclass(Rectangle, Shape)
True

>>> isinstance(r, Shape)
True

ABC & structural typing

>>> class Struggle:
...     def __len__(self): return 23
...

>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True

>>> issubclass(Struggle, abc.Sized)
True

ABC & structural typing

class Sized(metaclass=ABCMeta):

    __slots__ = ()

    @abstractmethod
    def __len__(self):
        return 0

    @classmethod
    def __subclasshook__(cls, C):
        if cls is Sized:
            if any("__len__" in B.__dict__ for B in C.__mro__):  
                return True  
        return NotImplemented  

Protocol

Protokół

  • Protokół jest klasą:
    • dziedziczącą po typing.Protocol
    • definiującą interfejs, który może być sprawdzany przez type checker
       
  • Klasa implementuje protokół nie musi dziedziczyć lub deklarować jakichkolwiek związków z protokołem

Protocol

Coord = namedtuple('Coord', 'x,y')


@runtime_checkable
class Shape(Protocol):
    @property
    def coordinates(self) -> Coord: ...

    def move(self, dx, dy) -> None: ...

    def draw(self) -> None: ...

Protocol

class Circle:
    def __init__(self, x, y, radius):
        self.__x = x
        self.__y = y
        self.__radius = radius

    @property
    def radius(self):
        return self.__radius

    @radius.setter
    def radius(self, new_radius):
        self.__radius = new_radius

    @property
    def coordinates(self):
        return Coord(self.__x, self.__y)

    def move(self, dx, dy):
        self.__x += dx
        self.__y += dy

    def draw(self):
        print("Drawing a circle at {} with radius {}".format(
            self.coordinates, self.radius))

Protocol

def draw_shapes(shapes: Iterable[Shape]) -> None:
    for s in shapes:
        s.draw()
r = Rectangle(100, 200, 300, 400)

print(isinstance(r, Shape))  # works because of @runtime_checkable

shapes = [r, Circle(100, 100, 50)]
draw_shapes(shapes)

SOLID OOP

Single Responsibility Principle

A class should have only one reason to change

Uncle Bob

Open-Closed Principle

Software entities (classes, modules, functions, etc.) should be open for extension, but  closed for modification

 

Bertrand Meyer

Liskov Substitution Principle

If S is a subtype of  T, then objects of type T in a program may be replaced with objects of type S) without altering any of the desirable properties of that program (e.g. correctness)

Design by contract

  • Pre-conditions cannot be strengthened in a subtype
     
  • Post-conditions cannot be weakened in a subtype
     
  • Invariants of the supertype must be preserved in a subtype

Interface Segregation Principle

No code should be forced to depend on methods it does not use

 

Uncle Bob

Dependency Inversion Principle

  • High-level modules should not import anything from low-level modules
     
  • Both should depend on abstractions (e.g., interfaces)
  • Abstractions should not depend on details
     
  • Details (concrete implementations) should depend on abstractions

Wzorzec projektowy

Wzorzec opisuje problem występujący wielokrotnie w danym środowisku, pokazując podstawowe rozwiązanie tego problemu dane w taki sposób, aby można wielokrotnie użyć tego rozwiązania do wszystkich wystąpień danego problemu, bez konieczności ponownego wykonywania tych samych czynności projektowych

 

        Christopher Alexander, A Pattern Language, 1977

Opis komunikujących się obiektów i klas, które przerabia się w celu rozwiązania  ogólnego problemu projektowego przy dokładnie określonym kontekście

 

        GangOfFour, 1994

Design Patterns help you to learn from others successes instead of your own failure

 

Mark Johnson

Elementy wzorca projektowego

  1. Nazwa wzorca

    • skrót, którego można użyć do zwięzłego określenia problemu projektowego, jego rozwiązania i konsekwencji

    • umożliwia projektowanie na wyższym poziomie abstrakcji

  2. Kontekst/Problem
    • określa, kiedy stosować dany wzorzec.
  3. Rozwiązanie 
    • opis elementów składających się na rozwiązanie zdefiniowanego problemu, ich związki, zobowiązania i współpraca
    • nie opisuje konkretnego projektu lub implementacji
  4. Konsekwencje
    • zalety  oraz wady zastosowania wzorca.

Klasyfikacja wzorców

  • Wzorce kreacyjne
    • Factory Method
    • Abstract Factory
    • Prototype
    • Builder
    • Singleton
  • Wzorce strukturalne
    • Adapter
    • Decorator
    • Composite
    • Proxy
    • Facade
    • Bridge
    • Flyweight
  • Wzorce behawioralne
    • Template Method
    • Strategy
    • State
    • Chain of Responsibility
    • Observer
    • Command
    • Memento
    • Mediator
    • Interpreter
    • Visitor

Iterator

Kontekst

  • Istnieje agregat (kolekcja), który powinien być przeglądany w sposób sekwencyjny

Problem

  • Chcemy zapewnić sekwencyjny dostęp do elementów agregatu (kolekcji) bez ujawniania jego struktury wewnętrznej

Iterator w Pythonie

class OddNumbers(abc.Iterable):
    "An iterable object."

    def __init__(self, maximum):
        self.maximum = maximum

    def __iter__(self):
        return OddIterator(self)
class OddIterator(abc.Iterator):
    "An iterator."

    def __init__(self, container):
        self.container = container
        self.n = -1

    def __next__(self):
        self.n += 2
        if self.n > self.container.maximum:
            raise StopIteration
        return self.n

    def __iter__(self):
        return self

Iterator w Pythonie

numbers = OddNumbers(7)

for n in numbers:
    print(n)
it = iter(OddNumbers(5))
print(next(it))
print(next(it))
  • Użycie w pętli for:
  • Użycie bezpośrednie:

Fabryki

Preferuj luźne powiązania między klasami

Fabryki - 1

  • Umożliwiają separację procesu tworzenia obiektu, od jego późniejszego użycia
     
  • Umożliwiają elastyczny wybór typu obiektu, który jest tworzony

Fabryki - 2

  • Chcąc utworzyć obiekt, musimy dokładnie wiedzieć jaki jest jego typ
     
  • Jednak czasami:
    • chcemy tę wiedzę oddelegować do kogoś innego
    • dysponujemy informacją o typie obiektu w postaci np. string'a
    • o typie tworzonego obiektu decyduje typ innego obiektu

Factory Method

class AMusicService:
    def __init__(self, user_name, user_secret):
        self._user_name = user_name
        self._user_secret = user_secret
    
    def load_track(self, title):
        return MP3Track(title)
        
class Client:
    def play_track(self, title):
        srv = AMusicService("username", "SECRET_KEY")
        track = srv.load_track(title)
        self.player_queue.play_now(track)
        

wybór klasy AMusicService narusza SRP

Problem z tworzeniem obiektów

class MusicClient:
    def __init__(self, music_service_factory, config):
        self._music_service_factory = music_service_factory
        self._config = config

    def play_track(self, title):
        srv = self._music_service_factory(**self._config)
        track = srv.load_track(title)
        track.play()


def main():
    config_A = {'user_name': 'superuser', 'user_secret': 'JDKHKJASHF4354'}
    client = MusicClient(AMusicService, config_A)
    client.play_track("Kill'em All")

    config_B = {'user_name': 'superuser',
                'user_secret': 'JDKHKJASHF4354', 'timeout': 30}
    client = MusicClient(BMusicService, config_B)
    client.play_track("Schism")

Poprawione rozwiązanie

Problem

  • Klient nie może przewidzieć, jakich klas obiekty musi tworzyć
     
  • Informacja o typie tworzonego obiektu (produktu) znana jest dopiero w czasie wykonywania programu
     
  • Chcemy tworzyć instancje konkretnych klas w warunkach zależności tylko od abstrakcyjnych interfejsów

Factory Method

Konsekwencje

  • Eliminuje potrzebę wstawiania specyficznych dla danej aplikacji klas w kod
     
  • Tworzenie obiektów wewnątrz klasy za pomocą metody wytwórczej jest bardziej elastyczne niż tworzenie ich bezpośrednio
    • wzorzec Factory Method daje podklasom punkt zaczepienia do dostarczenia rozszerzonej wersji obiektu
       
  • Promuje luźne powiązania między obiektami, ponieważ redukuje zależność kodu aplikacji od konkretnych klas

Konsekwencje

  • Umożliwia łączenie równoległych hierarchii klas:
    • równoległe hierarchie klas powstają wtedy, gdy klasa przekazuje niektóre ze swych zobowiązań odrębnej klasie,
    • Factory Method pozwala zdefiniować związek między dwiema hierarchiami.

Factory Method
równoległe hierarchie klas

Abstract Factory

Kontekst

  • W systemie istnieją rodziny powiązanych ze sobą obiektów, zaprojektowane tak, by obiekty były używane razem i ograniczenie to powinno być zachowane
     

  • System powinien być niezależny od tego, jak jego produkty są tworzone

Problem

  • Chcemy umożliwić konfigurację systemu przy użyciu jednej z wielu rodzin obiektów (produktów)
     

  • Kod powinien być zależny od interfejsów lub klas abstrakcyjnych

Abstract Factory

Konsekwencje

  • Odseparowanie klas konkretnych

    • pomaga zapanować nad tym, jakie klasy obiektów tworzy dana aplikacja

    • fabryka hermetyzuje odpowiedzialność i proces tworzenia obiektów-produktów
       

  • Łatwiejsza wymiana rodzin produktów

    • klasa fabryki konkretnej pojawia się w aplikacji zwykle tylko raz

    • umożliwia to łatwą zmianę instancji fabryki używanej przez aplikację

Konsekwencje

  • Spójność produktów

    • współpraca produktów wymaga, by aplikacja używała obiektów tylko z danej rodziny implementacji
       

  • Utrudnione dołączenie nowych produktów

    • interfejs AbstractFactory ustala zbiór obiektów, które mogą być utworzone

    • obsługa nowych rodzajów produktów wymaga rozszerzenia interfejsu fabryki, co z kolei pociąga konieczność reimplementacji wszystkich podklas

Prototype

Scenariusz

Kontekst

  • Rzeczywiste typy obiektów, które chcemy utworzyć nie są znane
     

  • Klasy, których instancje chcemy tworzyć, są ładowane dynamicznie
     

  • Stan obiektów klasy może przyjmować tylko jedną z kilku dozwolonych wartości

Problem

  • Chcemy tworzyć nowe egzemplarze obiektów bez wiedzy o tym, jaka jest ich konkretna klasa
     

  • Chcemy uniknąć budowania hierarchii klas fabryk, która jest porównywalna z hierarchią klas produktów

Prototype (UML)

Shallow vs deep copy

import copy

original = [ 1, 2, ["one", "two"], { 1: "one", 2: "two" }, ("a", 1) ]

deep_clone = copy.deepcopy(original)

shallow_clone = copy.copy(original)

Klasa z operacją clone()

class Point:
    def __init__(self, x, y, z):
        self.x = x
        self.y = y
        self.z = z
        
    def __repr__(self):
        return '<Point ({}, {}, {})>'.format(self.x, self.y, self.z)
        
    def clone(self):
        return self.__class__(self.x, self.y, self.z)
p1 = Point(3, 4, 5)

p2 = p1.clone()

Builder

Kontekst

  • Algorytm konstrukcji obiektu składa się z wielu kroków
  • Proces konstrukcji złożonego obiektu prowadzi do utworzenia różnych reprezentacji obiektu

Problem

  • Chcemy:
    • zdefiniować operacje niezbędne do utworzenia złożonego obiektu
    • ukryć wewnętrzną reprezentację obiektu przed klientem
    • mieć możliwość modyfikacji poszczególnych kroków algorytmu służącego do budowy obiektu

Adapter

Kontekst

  • Interfejs wymagany przez klienta i interfejs klasy dostarczającej implementację nie są ze sobą zgodne

Problem

  • Chcemy wykorzystać istniejącą klasę, a jej interfejs nie odpowiada temu, którego potrzebujemy

Adapter klas

Adapter obiektów

Implementacja

class Adaptee:
    def specific_request(self):
        print("Adaptee.specific_request()")


class Target:
    def request(self):
        pass


class Client:
    def use(self, target):
        target.request()

Implementacja - adapter klas

class ClassAdapter(Adaptee, Target):

    def request(self):
        Adaptee.specific_request(self)


def main():
    client = Client()
    client.use(ClassAdapter())

Implementacja - adapter obiektowy

class ObjectAdapter:
    def __init__(self, adaptee):
        self.adaptee = adaptee

    def request(self):
        self.adaptee.specific_request()


def main():
    client = Client()
    adaptee = Adaptee()
    
    adapter = ObjectAdapter(adaptee)
    client.use(adapter)

Implementacja - mixin

class AdapterMixin:
    def request(self):
        self.specific_request()


class Adapter2(AdapterMixin, Adaptee):
    pass


def main():
    client = Client()
    client.use(Adapter2())

Decorator

Scenariusz

Image Viewer

Image Viewer

Wersja 1.0

Wersja 2.0

Wersja 2.0

Kontekst

  • Część aplikacji wymaga dynamicznej zmiany funkcjonalności

Problem

  • Chcemy dynamicznie i w przezroczysty sposób (tzn. nie wpływający na inne obiekty) dodać nową funkcjonalność (zachowanie lub stan) dla określonych obiektów
  • Dodane zobowiązania mogą być cofnięte (usunięte) w trakcie działania programu

Decorator (UML)

Konsekwencje

  • Większa elastyczność niż przy stosowaniu statycznego dziedziczenia.
    • wykorzystując dekoratory można dodawać i usuwać zobowiązania w czasie wykonywania programu.
    • dekoratory ułatwiają także dwukrotne dołączanie właściwości (np. fotografia z podwójną ramką).
  • Unikanie przeładowania właściwościami klas na szczycie hierarchii
    • możliwe jest zdefiniowanie prostej klasy i przyrostowe rozszerzanie jej funkcjonalności za pomocą obiektów dekoratora. Nowe rodzaje dekoratorów są łatwe do zdefiniowania.
  • Wiele małych obiektów
    • projekty wykorzystujące dekoratory prowadzą często do powstawanie aplikacji z dużą liczbą małych, podobnych do siebie obiektów

Composite

Scenariusz

Kontekst

  • Chcemy przedstawić grupę obiektów jako obiekt z określonym interfejsem
  • Chcemy zbudować hierarchiczną strukturę obiektów

Problem

  • Chcemy, aby klienci mogli ignorować różnicę między złożeniami obiektów (grupą obiektów) a pojedynczymi obiektami
    • klienci będą wtedy jednakowo traktować wszystkie obiekty występujące w strukturze

Composite (UML)

Composite (UML)

Konsekwencje

  • Uproszczenie budowy klienta
    • klienci mogą jednakowo traktować struktury złożone i pojedyncze obiekty
  • Ułatwienie dodawania nowych rodzajów komponentów
  • Może sprawić, że projekt będzie zbyt ogólny
    • umieszczenie operacji dodawania nowych komponentów w klasie bazowej Component komplikuje wprowadzanie ograniczeń dotyczących złożeń komponentów.

Wzorce pokrewne

  • Agregacja komponentów jest używana się przy implementacji wzorca Chain of Responsibility.
  • Composite jest często używany ze wzorcem Decorator
    • ​gdy dekorator i kompozyt są stosowane razem, mają na ogół wspólną klasę rodzica
  • Flyweight umożliwia współdzielenie komponentów
  • Iterator może być używany do przechodzenia kompozytów
  • Visitor umożliwia wykonanie nowej operacji na grupie obiektów

Proxy

Scenariusz

  • Chcemy napisać edytor dokumentów, który umożliwia osadzanie obiektów graficznych
    • otwieranie dokumentów powinno być szybkie
    • optymalizacja nie powinna mieć wpływu na części związane z formatowaniem czy wyświetlaniem obiektów graficznych

Scenariusz

Scenariusz

Kontekst

  • Tworzenie obiektów i ich inicjalizacja w trakcie działania programu jest kosztowne
  • Potrzebna jest kontrola dostępu do obiektu

Problem

  • Optymalizacja kosztownych procesów lub kontrola dostępu powinna być przezroczysta dla klienta

Proxy (UML)

Rodzaje proxy

  • Remote proxy – jest lokalnym reprezentantem obiektu znajdującego się w innej przestrzeni adresowej (RPC).
  • Virtual proxy – tworzy kosztowne obiekty na żądanie.
  • Protection proxy – kontroluje dostęp do oryginalnego obiektu.
  • Smart proxy – modyfikuje żądanie przed przesłaniem go do oryginalnego obiektu.

Konsekwencje

  • Zapewnia obiekt pośrednika, dzięki któremu możemy optymalizować wywołanie kosztownych operacji lub kontrolować dostęp do oryginału.
  • Interfejs obiektu Proxy jest taki sam jak interfejs oryginału.

Facade

Kontekst

  • Klient zmuszony jest do bezpośredniego stosowania złożonego podsystemu (biblioteki)

Problem

  • Chcemy odseparować klienta od bezpośredniego stosowania złożonych podsystemów

Konsekwencje

  • Oddziela klientów od komponentów podsystemu
    • zmniejsza się liczba obiektów, z którymi klienci mają do czynienia
    • podsystem staje się łatwiejszy do użycia.
  • Sprzyja słabemu powiązaniu podsystemu z jego klientami
  • Umożliwia zmianę biblioteki (podsystemu) w sposób niewidoczny dla jego klientów.
  • Ułatwia ułożenie warstwami systemu i zależności między obiektami.
  • Nie uniemożliwia aplikacjom bezpośredniego dostępu do podsystemu, jeśli tego potrzebują

Bridge

Kontekst

  • Istnieje wiele implementacji, które muszą być uwzględnione w projekcie
  • Klient korzysta z abstrakcyjnych klas w celu ujednolicenia interfejsu

Problem

  • Chcemy uniknąć stałego powiązania abstrakcji z jej implementacją
    • implementacja może być wybierana lub zmieniana w czasie wykonywania programu
  • Oczekujemy zmian zarówno po stronie abstrakcji jak i w implementacjach
  • Chcemy całkowicie ukryć implementację abstrakcji przed klientami

Scenariusz

Scenariusz

Bridge (UML)

Flyweight

Kontekst

  • W projekcie istnieje olbrzymia liczba obiektów

  • Koszt związany z przechowywaniem tych obiektów w pamięci jest znaczący

Problem

  • Chcemy ograniczyć koszty, związane z przechowywaniem obiektów w pamięci współdzieląc obiekty w postaci pyłków

Scenariusz

Scenariusz

Scenariusz

Scenariusz

Scenariusz

Scenariusz

Scenariusz

Obiekt Flyweight

  • Flyweight jest współdzielonym obiektem, który może być używany jednocześnie w wielu kontekstach
     
  • Działa jako obiekt niezależny w każdym kontekście dzięki rozróżnieniu stanu na wewnętrzny i zewnętrzny
    • stan wewnętrzny – jest przechowywany w pyłku; składa się z informacji, które są niezależne od kontekstu
    • stan zewnętrzny – zależy od kontekstu i zmienia się w zależności od niego; nie może być współdzielony

Obiekt Flyweight

  • Po usunięciu stanu zewnętrznego wiele grup obiektów można zastąpić stosunkowo niewielką liczbą współdzielonych obiektów
     
  • Pyłki modelują pojęcia lub byty, które zwykle są zbyt liczne, żeby przedstawiać je za pomocą obiektów

Flyweight (UML)

Konsekwencje

  • Zmniejszenie zużycia pamięci kosztem zwiększenia czasu wykonywania
     

  • Oszczędności pamięci zależą od kilku czynników:

    • zmniejszenia łącznej liczby egzemplarzy, wynikającego ze współdzielenia

    • wielkości stanu wewnętrznego przypadającego na obiekt

    • tego, czy stan zewnętrzny jest wyliczany, czy przechowywany

       

Template Method

Kontekst

  • Istnieje algorytm wymagający zmiany implementacji poszczególnych kroków

Problem

  • Chcemy jednorazowo zaimplementować stałą część algorytmu i pozostawić klasom pochodnym zaimplementowanie zachowania, które może się zmieniać
     

  • Chcemy zdefiniować metodę, która w wybranych miejscach wywołuje operacje (tzw. punkty zaczepienia), umożliwiając tym samym rozszerzanie klas tylko w tych miejscach

Template Method

Konsekwencje

  • Użycie metod szablonowych jest podstawową techniką stosowaną w celu zagwarantowania możliwości ponownego wykorzystania kodu
     

  • Template Method prowadzi do odwróconej struktury sterowania

    • klasa bazowa wywołuje operacje klasy pochodnej, a nie odwrotnie

Konsekwencje

  • Metody szablonowe wywołują:

    • operacje konkretne – z ConcreteClass lub z klas klienta

    • operacje konkretne z AbstractClass – te, które są na ogół przydatne dla podklas

    • operacje abstrakcyjne

    • metody wytwórcze

    • operacje tzw. punkty-zaczepienia, zapewniające zachowanie domyślne, które może być rozszerzane przez klasy pochodne (domyślna implementacja operacji punkt zaczepienia często nic nie robi)

Strategy

Kontekst

  • Wiele powiązanych ze sobą klas różni się tylko zachowaniem
     

  • Potrzebne są różne warianty jakiegoś algorytmu
     

  • Klasa definiuje wiele zachowań, które w operacjach są uwzględnione w postaci wielokrotnych instrukcji warunkowych
     

  • W algorytmie są używane dane, o których klient nie powinien wiedzieć

    • Strategy pozwala uniknąć ujawniania złożonych i specyficznych dla algorytmu struktur danych

Problem

  • Chcemy dokonać hermetyzacji algorytmu w klasie i stosując kompozycję umożliwić wymianę implementacji algorytmu

Strategy (UML)

Konsekwencje

  • Hierarchia klas Strategy definiuje rodzinę algorytmów do wielokrotnego użycia przez konteksty
     

  • Enkapsulacja algorytmu w oddzielnych klasach umożliwia modyfikowanie go niezależnie od jego kontekstu

    • ułatwia to zmienianie go, zrozumienie go i rozszerzanie
       

  • Strategie eliminują instrukcje warunkowe

    • alternatywa dla stosowania instrukcji warunkowych w celu wybrania pożądanego zachowania

Konsekwencje

  • Wybór implementacji

    • zapewnienie różnych implementacji tego samego zachowania

    • klienci muszą być świadomi istnienia różnych strategii i różnic pomiędzy nimi – potencjalna wada
       

  • Wyższe koszty związane z komunikacją między strategią a kontekstem
     

  • Zwiększona liczba obiektów

State

Kontekst

  • Zachowanie obiektu zależy od jego stanu, a obiekt ten musi zmieniać swoje zachowanie w czasie wykonywania programu w zależności od stanu
     

  • Operacje zawierają duże, wieloczęściowe instrukcje warunkowe, które zależą od stanu obiektu

    • wzorzec State przenosi każde rozgałęzienie warunkowe do oddzielnej klasy

Problem

  • Chcemy umożliwić obiektowi zmianę zachowania w momencie zmiany wewnętrznego stanu obiektu hermetyzując stan w postaci klasy

State (UML)

Konsekwencje

  • Umiejscowienie zachowania specyficznego dla stanu i rozdzielenie zachowania w wypadku różnych stanów

    • kod dla każdego stanu znajduje się w osobnej klasie

    • ułatwia to dodawanie nowych stanów (nie wymaga daleko idących modyfikacji istniejącego kodu)

    • eliminuje konieczność dzielenia kodu metod na bloki właściwe dla stanów (bloki if-else)

Konsekwencje

  • Jawność przejść między stanami

    • z perspektywy klientów przejścia między stanami są atomowe

    • dochodzi do nich poprzez wymianę obiektu reprezentującego bieżący stan
       

  • Możliwość współdzielenia obiektów typu State

    • jeśli obiekty typu State nie mają swoich zmiennych egzemplarzowych, to konteksty mogą je współdzielić

Chain of responsibility

Kontekst

  • Zbiór obiektów, które mogą obsłużyć żądanie, może być określony dynamicznie

  • Więcej niż jeden obiekt może obsłużyć żądanie, a obiekt obsługujący nie jest znany a priori

  • Wykonanie żądania nie jest gwarantowane

Problem

  • Chcemy:
    • zdefiniować operacje niezbędne do utworzenia złożonego obiektu
    • ukryć wewnętrzną reprezentację obiektu przed klientem
    • mieć możliwość modyfikacji poszczególnych kroków algorytmu służącego do budowy obiektu

Kontekst

  • Chcemy wysłać żądanie do jednego z kilku obiektów, nie określając jawnie odbiorcy
     

  • Chcemy odseparować nadawcę żądania od jego odbiorców

Chain of Responsibility

Konsekwencje

  • Zredukowanie powiązań

    • dany obiekt nie musi wiedzieć, który inny obiekt obsłuży żądanie
       

  • Dodatkowa elastyczność w przydzielaniu obiektom zobowiązań

    • Chain of Responsibility zwiększa elastyczność rozdzielania zobowiązań między obiekty
       

  • Brak gwarancji odebrania żądania

    • ponieważ odbiorca żądania nie jest jawnie znany, nie ma gwarancji, że zostanie ono obsłużone – żądanie może wypaść z łańcucha, nie zostawszy w ogóle obsłużone

Observer

Kontekst

  • Zmiana stanu jednego obiektu wymaga zmiany innych i nie wiadomo, ile obiektów trzeba zmienić

Problem

  • Obiekt powinien być w stanie powiadamiać inne obiekty, nie przyjmując żadnych założeń co do tego, co te obiekty reprezentują
     

  • Chcemy mieć luźne powiązania między obiektami

Observer (UML)

Konsekwencje

  • Abstrakcyjne powiązanie między obiektem obserwowanym a obserwatorem

    • obserwowany wie, że ma listę obserwatorów, z których każdy dostosowuje się do interfejsu klasy Observer

    • obserwowany i obserwator mogą należeć do różnych warstw abstrakcji w systemie

Konsekwencje

  • Wsparcie dla rozsyłania komunikatów

    • powiadomienie jest automatycznie nadawane do wszystkich zainteresowanych obiektów, które je zaprenumerowały
       

  • Nieoczekiwane uaktualnienia

    • pozornie nieszkodliwa operacja dotycząca obiektu obserwowanego może spowodować kaskadę uaktualnień w obserwatorach i obiektach od nich zależnych.

Implementacja

  • Model push

    • obserwowany wysyła szczegółową informację o zmianie (bez względu, czy obserwatorzy tego chcą, czy nie)
       

  • Model pull

    • obserwowany nie wysyła niczego poza powiadomieniem, a obserwatorzy jawnie pytają potem o szczegóły

Command

Problem

  • Chcemy w aplikacji
    • Sparametryzować obiekty wykonywaną akcją – Command jest obiektowym zastępcą wywołania funkcji zwrotnych (callbacks)

    • Tworzyć polecenia typu makro

    • Uwzględnić możliwość anulowania wprowadzonych zmian

    • Umożliwić wpisywanie zmian do dziennika transakcji, tak by można je było ponownie wykonać, gdy dojdzie do awarii systemu

Command (UML)

Konsekwencje

  • Wzorzec Command oddziela obiekt, który wywołał operację, od tego, który wie, jak ją wykonać

    • separacja interfejsów wywołującego od odbiorcy
       

  • Polecenia są obiektami

    • mogą być przetwarzane, kolejkowane i przechowywane tak jak inne obiekty
       

  • Z poszczególnych poleceń można tworzyć polecenia złożone

    • polecenia złożone wykorzystują wzorzec Composite
       

  • Można łatwo dodawać nowe polecenia, gdyż nie wymaga to modyfikowania istniejących klas.

Memento

Kontekst

  • Aplikacja wymaga zapamiętania migawki stanu obiektu w celu jego późniejszego przywrócenia (np. w operacji Undo)

Problem

  • Chcemy przechować migawkę stanu bez naruszania hermetyzacji obiektu

Memento

Konsekwencje

  • Zachowanie granic hermetyzacji
    • Memento umożliwia uniknięcie ujawniania informacji, którymi jedynie źródło powinno zarządzać, ale które mimo tego muszą być przechowywane poza nim
    • wzorzec ten izoluje inne obiekty od potencjalnie złożonego wnętrza obiektu źródła, zachowując w ten sposób granice hermetyzacji
       
  • Uproszczenie implementacji obiektu źródła

Konsekwencje

  • Użycie pamiątek może być kosztowne
    • jeśli hermetyzacja i odtwarzanie stanu źródła jest kosztowne, wzorzec ten może okazać się nieodpowiedni
       
  • Ukryte koszty związane z przechowaniem pamiątek

Mediator

Kontekst

  • Zbiór obiektów porozumiewa się w dobrze zdefiniowany, lecz skomplikowany sposób
     
  • Wynikające stąd zależności są nieuporządkowane i trudne do zrozumienia

Problem

  • Chcemy wprowadzić obiekt pośredniczący w komunikacji między obiektami

Mediator (UML)

Konsekwencje

  • Hermetyzuje proces komunikacji między obiektami
     
  • Definiuje luźne powiązania między obiektami
     
  • Umożliwia łatwą reimplementację sposobu współpracy obiektów typu Colleague, bez konieczności definiowania klas pochodnych dla nich.

Visitor

Kontekst

  • Wiele różnych i niepowiązanych ze sobą operacji musi być wykonanych na obiektach określonej struktury obiektowej, a chcemy uniknąć „zanieczyszczenia” klas tych obiektów tymi operacjami
    • Visitor umożliwia trzymanie powiązanych ze sobą operacji razem przez zdefiniowanie ich w jednej klasie
       
  • Klasy definiujące strukturę obiektową rzadko się zmieniają, ale chcemy często definiować nowe operacje w tej strukturze

Problem

  • Chcemy umożliwić łatwe dodawanie nowej operacji do struktury obiektowej bez konieczności otwierania klas tej struktury

Visitor (UML)

Konsekwencje

  • Łatwe dodawanie nowych operacji
    • Hierarchia Visitor ułatwia dodawanie operacji - nową operację na strukturze obiektowej definiuje się po prostu przez dodanie nowej klasy w tej hierarchii
       
  • Zebranie razem powiązanych ze sobą operacji, a rozdzielenie tych niepowiązanych
    • powiązane ze sobą zachowania nie są rozsiane po wszystkich klasach definiujących strukturę obiektową, lecz są umiejscowione w klasie Visitor
       
  • Trudne dodawanie nowych klas ConcreteElement

python-dp

By Krystian Piękoś