Krystian Piękoś
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.
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
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
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
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
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
An abstract class represents an interface.
Bjarne Stroustrup, creator of C++
hasattr()
Polega na:
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"""
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))
@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))
>>> r = Rectangle(0, 15, 10, 20)
>>> issubclass(Rectangle, Shape)
True
>>> isinstance(r, Shape)
True
>>> class Struggle:
... def __len__(self): return 23
...
>>> from collections import abc
>>> isinstance(Struggle(), abc.Sized)
True
>>> issubclass(Struggle, abc.Sized)
True
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
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: ...
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))
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)
A class should have only one reason to change
Uncle Bob
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification
Bertrand Meyer
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)
No code should be forced to depend on methods it does not use
Uncle Bob
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
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
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
numbers = OddNumbers(7)
for n in numbers:
print(n)
it = iter(OddNumbers(5))
print(next(it))
print(next(it))
Preferuj luźne powiązania między klasami
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
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")
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
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
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ę
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
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
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
import copy
original = [ 1, 2, ["one", "two"], { 1: "one", 2: "two" }, ("a", 1) ]
deep_clone = copy.deepcopy(original)
shallow_clone = copy.copy(original)
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()
Interfejs wymagany przez klienta i interfejs klasy dostarczającej implementację nie są ze sobą zgodne
class Adaptee:
def specific_request(self):
print("Adaptee.specific_request()")
class Target:
def request(self):
pass
class Client:
def use(self, target):
target.request()
class ClassAdapter(Adaptee, Target):
def request(self):
Adaptee.specific_request(self)
def main():
client = Client()
client.use(ClassAdapter())
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)
class AdapterMixin:
def request(self):
self.specific_request()
class Adapter2(AdapterMixin, Adaptee):
pass
def main():
client = Client()
client.use(Adapter2())
W projekcie istnieje olbrzymia liczba obiektów
Koszt związany z przechowywaniem tych obiektów w pamięci jest znaczący
Chcemy ograniczyć koszty, związane z przechowywaniem obiektów w pamięci współdzieląc obiekty w postaci pyłków
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
Istnieje algorytm wymagający zmiany implementacji poszczególnych kroków
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
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
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)
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
Chcemy dokonać hermetyzacji algorytmu w klasie i stosując kompozycję umożliwić wymianę implementacji algorytmu
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
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
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
Chcemy umożliwić obiektowi zmianę zachowania w momencie zmiany wewnętrznego stanu obiektu hermetyzując stan w postaci klasy
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
)
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ć
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
Chcemy wysłać żądanie do jednego z kilku obiektów, nie określając jawnie odbiorcy
Chcemy odseparować nadawcę żądania od jego odbiorców
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
Zmiana stanu jednego obiektu wymaga zmiany innych i nie wiadomo, ile obiektów trzeba zmienić
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
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
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.
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
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
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.
Colleague
, bez konieczności definiowania klas pochodnych dla nich.Visitor
ułatwia dodawanie operacji - nową operację na strukturze obiektowej definiuje się po prostu przez dodanie nowej klasy w tej hierarchiiVisitor
ConcreteElement