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
-
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
-
-
Kontekst/Problem
- określa, kiedy stosować dany wzorzec.
-
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
-
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
- 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
- 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
-
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
- Hierarchia
- 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
- powiązane ze sobą zachowania nie są rozsiane po wszystkich klasach definiujących strukturę obiektową, lecz są umiejscowione w klasie
- Trudne dodawanie nowych klas
ConcreteElement
python-dp
By Krystian Piękoś
python-dp
- 3