Observer (#20)

* doc: create package & update general TOCs

* doc: add documentation

* add code example

* reformat using black
This commit is contained in:
Ruidy 2020-10-06 08:25:18 +02:00 committed by GitHub
parent bf1d5dd7f0
commit c8cc1f47b8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 277 additions and 58 deletions

View file

@ -44,6 +44,7 @@ and divided into three groups.
- [Iterator](behavioral/iterator/README.md)
- [Mediator](behavioral/mediator/README.md)
- [Memento](behavioral/memento/README.md)
- [Observer](behavioral/observer/README.md)
## Resources

View file

@ -9,3 +9,4 @@ Behavioral design patterns are concerned with algorithms and the assignment of r
- [Iterator](iterator/README.md)
- [Mediator](mediator/README.md)
- [Memento](memento/README.md)
- [Observer](observer/README.md)

View file

@ -9,20 +9,20 @@ from behavioral.chain_responsibility.abstract_handler import AbstractHandler
class MonkeyHandler(AbstractHandler):
def handle(self, request: Any) -> Optional[str]:
if request == 'Banana':
if request == "Banana":
return f"Monkey: I'll eat the {request}"
return super().handle(request)
class SquirrelHandler(AbstractHandler):
def handle(self, request: Any) -> Optional[str]:
if request == 'Nut':
if request == "Nut":
return f"Squirrel: I'll eat the {request}"
return super().handle(request)
class DogHandler(AbstractHandler):
def handle(self, request: Any) -> Optional[str]:
if request == 'MeatBall':
if request == "MeatBall":
return f"Dog: I'll eat the {request}"
return super().handle(request)

View file

@ -1,4 +1,8 @@
from behavioral.chain_responsibility.concrete_handlers import MonkeyHandler, SquirrelHandler, DogHandler
from behavioral.chain_responsibility.concrete_handlers import (
MonkeyHandler,
SquirrelHandler,
DogHandler,
)
from behavioral.chain_responsibility.handler import Handler
@ -18,7 +22,7 @@ def client_code(handler: Handler):
print(f" {food} was left untouched.", end="")
if __name__ == '__main__':
if __name__ == "__main__":
monkey = MonkeyHandler()
squirrel = SquirrelHandler()
dog = DogHandler()

View file

@ -12,7 +12,8 @@ class SimpleCommand(Command):
def execute(self) -> None:
print(
f"SimpleCommand: See, I can do simple things like printing ({self._payload})")
f"SimpleCommand: See, I can do simple things like printing ({self._payload})"
)
class ComplexCommand(Command):
@ -36,6 +37,8 @@ class ComplexCommand(Command):
Commands can delegate to any methods of a receiver.
"""
print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
print(
"ComplexCommand: Complex stuff should be done by a receiver object", end=""
)
self._receiver.do_something(self._a)
self._receiver.do_something_else(self._b)

View file

@ -5,12 +5,11 @@ from behavioral.command.commands import SimpleCommand, ComplexCommand
from behavioral.command.invoker import Invoker
from behavioral.command.receiver import Receiver
if __name__ == '__main__':
if __name__ == "__main__":
invoker = Invoker()
invoker.set_on_start(SimpleCommand("Say hi"))
receiver = Receiver()
invoker.set_on_finish(ComplexCommand(
receiver, "Send email", "Save report"))
invoker.set_on_finish(ComplexCommand(receiver, "Send email", "Save report"))
invoker.do_something_important()

View file

@ -4,7 +4,6 @@ from behavioral.memento.memento import Memento
class ConcreteMemento(Memento):
def __init__(self, state: str) -> None:
self._state = state
self._date = str(datetime.now())[:19]

View file

@ -0,0 +1,47 @@
# Observer
Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object theyre observing.
## Problem
Imagine that you have two types of objects: a `Customer` and a `Store`. The customer is very interested in a particular brand of product which should become available in the store very soon.
The customer could visit the store every day and check product availability. But while the product is still en route, most of these trips would be pointless.
On the other hand, the store could send tons of emails (which might be considered spam) to all customers each time a new product becomes available. This would save some customers from endless trips to the store. At the same time, itd upset other customers who aren't interested in new products.
It looks like weve got a conflict. Either the customer wastes time checking product availability or the store wastes resources notifying the wrong customers.
## Solution
The object that has some interesting state is often called subject, but since its also going to notify other objects about the changes to its state, well call it publisher. All other objects that want to track changes to the publishers state are called subscribers.
The Observer pattern suggests that you add a subscription mechanism to the publisher class so individual objects can subscribe to or unsubscribe from a stream of events coming from that publisher. Fear not! Everything isnt as complicated as it sounds. In reality, this mechanism consists of 1) an array field for storing a list of references to subscriber objects and 2) several public methods which allow adding subscribers to and removing them from that list.
Now, whenever an important event happens to the publisher, it goes over its subscribers and calls the specific notification method on their objects.
Real apps might have dozens of different subscriber classes that are interested in tracking events of the same publisher class. You wouldn't want to couple the publisher to all of those classes. Besides, you might not even know about some of them beforehand if your publisher class is supposed to be used by other people.
Thats why its crucial that all subscribers implement the same interface and that the publisher communicates with them only via that interface. This interface should declare the notification method along with a set of parameters that the publisher can use to pass some contextual data along with the notification.
If your app has several different types of publishers and you want to make your subscribers compatible with all of them, you can go even further and make all publishers follow the same interface. This interface would only need to describe a few subscription methods. The interface would allow subscribers to observe publishers states without coupling to their concrete classes.
## How to Implement
1. Look over your business logic and try to break it down into two parts: the core functionality, independent from other code, will act as the publisher; the rest will turn into a set of subscriber classes.
1. Declare the subscriber interface. At a bare minimum, it should declare a single update method.
1. Declare the publisher interface and describe a pair of methods for adding a subscriber object to and removing it from the list. Remember that publishers must work with subscribers only via the subscriber interface.
1. Decide where to put the actual subscription list and the implementation of subscription methods. Usually, this code looks the same for all types of publishers, so the obvious place to put it is in an abstract class derived directly from the publisher interface. Concrete publishers extend that class, inheriting the subscription behavior.
However, if youre applying the pattern to an existing class hierarchy, consider an approach based on composition: put the subscription logic into a separate object, and make all real publishers use it.
1. Create concrete publisher classes. Each time something important happens inside a publisher, it must notify all its subscribers.
1. Implement the update notification methods in concrete subscriber classes. Most subscribers would need some context data about the event. It can be passed as an argument of the notification method.
But theres another option. Upon receiving a notification, the subscriber can fetch any data directly from the notification. In this case, the publisher must pass itself via the update method. The less flexible option is to link a publisher to the subscriber permanently via the constructor.
1. The client must create all necessary subscribers and register them with proper publishers.

View file

@ -0,0 +1,4 @@
"""
Lets you define a subscription mechanism to notify multiple objects about any
events that happen to the object theyre observing.
"""

View file

@ -0,0 +1,18 @@
"""
Concrete Observers react to the updates issued by the Subject they had been
attached to.
"""
from behavioral.observer.observer import Observer
from behavioral.observer.subject import Subject
class ConcreteObserverA(Observer):
def update(self, subject: Subject) -> None:
if subject.state < 3:
print("ConcreteObserverA: Reacted to the event")
class ConcreteObserverB(Observer):
def update(self, subject: Subject) -> None:
if subject.state == 0 or subject.state >= 2:
print("ConcreteObserverB: Reacted to the event")

View file

@ -0,0 +1,61 @@
from random import randrange
from typing import List, Optional
from behavioral.observer.observer import Observer
from behavioral.observer.subject import Subject
class ConcreteSubject(Subject):
"""
The Subject owns some important state and notifies observers when the state
changes.
"""
"""
For the sake of simplicity, the Subject's state, essential to all
subscribers, is stored in this variable.
"""
_state: Optional[int] = None
"""
List of subscribers. In real life, the list of subscribers can be stored
more comprehensively (categorized by event type, etc.).
"""
_observers: List[Observer] = []
@property
def state(self) -> int:
return self._state
def attach(self, observer: Observer) -> None:
print("Subject: Attached an observer.")
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
self._observers.remove(observer)
"""
The subscription management methods.
"""
def notify(self) -> None:
"""
Trigger an update in each subscriber.
"""
print("Subject: Notifying observers...")
for observer in self._observers:
observer.update(self)
def some_business_logic(self) -> None:
"""
Usually, the subscription logic is only a fraction of what a Subject can
really do. Subjects commonly hold some important business logic, that
triggers a notification method whenever something important is about to
happen (or after it).
"""
print("\nSubject: I'm doing something important.")
self._state = randrange(0, 10)
print(f"Subject: My state has just changed to: {self._state}")
self.notify()

View file

@ -0,0 +1,23 @@
"""The client code"""
from behavioral.observer.concrete_observers import ConcreteObserverA, ConcreteObserverB
from behavioral.observer.concrete_subject import ConcreteSubject
subject = ConcreteSubject()
observer_a = ConcreteObserverA()
subject.attach(observer_a)
observer_b = ConcreteObserverB()
subject.attach(observer_b)
subject.some_business_logic()
subject.some_business_logic()
subject.detach(observer_a)
subject.some_business_logic()

View file

@ -0,0 +1,16 @@
from abc import ABC, abstractmethod
from structural.proxy.subject import Subject
class Observer(ABC):
"""
The Observer interface declares the update method, used by subjects.
"""
@abstractmethod
def update(self, subject: Subject) -> None:
"""
Receive update from subject.
"""
pass

View file

@ -0,0 +1,35 @@
from abc import ABC, abstractmethod
from behavioral.observer.observer import Observer
class Subject(ABC):
"""
The Subject interface declares a set of methods for managing subscribers.
"""
@property
@abstractmethod
def state(self) -> int:
pass
@abstractmethod
def attach(self, observer: Observer) -> None:
"""
Attach an observer to the subject.
"""
pass
@abstractmethod
def detach(self, observer: Observer) -> None:
"""
Detach an observer to the subject.
"""
pass
@abstractmethod
def notify(self) -> None:
"""
Notify all observers about an event.
"""
pass

View file

@ -1,6 +1,10 @@
from creational.abstract_factory.AbstractFactory import AbstractFactory
from creational.abstract_factory.products import ConcreteProductA1, \
ConcreteProductB1, ConcreteProductA2, ConcreteProductB2
from creational.abstract_factory.products import (
ConcreteProductA1,
ConcreteProductB1,
ConcreteProductA2,
ConcreteProductB2,
)
class ConcreteFactory1(AbstractFactory):

View file

@ -1,8 +1,7 @@
"""The client code can work with any concrete factory class."""
from creational.abstract_factory.AbstractFactory import AbstractFactory
from creational.abstract_factory.factories import ConcreteFactory1, \
ConcreteFactory2
from creational.abstract_factory.factories import ConcreteFactory1, ConcreteFactory2
def client_code(factory: AbstractFactory) -> None:

View file

@ -4,8 +4,7 @@ product's type.
"""
from creational.factory_method.ICreator import ICreator
from creational.factory_method.products import ConcreteProduct1, \
ConcreteProduct2
from creational.factory_method.products import ConcreteProduct1, ConcreteProduct2
class ConcreteCreator1(ICreator):

View file

@ -1,6 +1,5 @@
from creational.factory_method.ICreator import ICreator
from creational.factory_method.creators import ConcreteCreator1, \
ConcreteCreator2
from creational.factory_method.creators import ConcreteCreator1, ConcreteCreator2
def client_code(creator: ICreator) -> None:

View file

@ -39,8 +39,7 @@ class SomeComponent:
# Then, let's clone the object itself, using the prepared clones of the
# nested objects.
new = self.__class__(
self.some_int, some_list_of_objects, some_circular_ref)
new = self.__class__(self.some_int, some_list_of_objects, some_circular_ref)
new.__dict__.update(self.__dict__)
return new
@ -63,8 +62,7 @@ class SomeComponent:
some_circular_ref = copy.deepcopy(self.some_circular_ref, memo)
# Then, let's clone the object itself, using the prepared clones of the
# nested objects.
new = self.__class__(
self.some_int, some_list_of_objects, some_circular_ref)
new = self.__class__(self.some_int, some_list_of_objects, some_circular_ref)
new.__dict__ = copy.deepcopy(self.__dict__, memo)

View file

@ -3,8 +3,10 @@ The client code should be able to work with any pre-configured abstraction-
implementation combination.
"""
from structural.bridge.abstractions import Abstraction
from structural.bridge.implementations import ConcreteImplementationA, \
ConcreteImplementationB
from structural.bridge.implementations import (
ConcreteImplementationA,
ConcreteImplementationB,
)
def client_code(abstraction: Abstraction) -> None:

View file

@ -1,7 +1,9 @@
from structural.decorator.component import Component
from structural.decorator.concrete_component import ConcreteComponent
from structural.decorator.concrete_decorators import ConcreteDecoratorA, \
ConcreteDecoratorB
from structural.decorator.concrete_decorators import (
ConcreteDecoratorA,
ConcreteDecoratorB,
)
def client_code(component: Component):
@ -11,10 +13,10 @@ def client_code(component: Component):
with.
"""
print(f"RESULT: {component.operation()}", end='')
print(f"RESULT: {component.operation()}", end="")
if __name__ == '__main__':
if __name__ == "__main__":
# This way the client code can support both simple components...
simple = ConcreteComponent()
print("Client: I've got a simple component:")

View file

@ -25,12 +25,13 @@ class Facade:
functionality of the subsystems. However, clients get only to a fraction
of a subsystem's capabilities.
"""
results = []
results.append("Facade initializes subsystems:")
results.append(self._subsystem1.operation1())
results.append(self._subsystem2.operation1())
results.append("Facade orders subsystems to perform the action:")
results.append(self._subsystem1.operation_n())
results.append(self._subsystem2.operation_z())
results = [
"Facade initializes subsystems:",
self._subsystem1.operation1(),
self._subsystem2.operation1(),
"Facade orders subsystems to perform the action:",
self._subsystem1.operation_n(),
self._subsystem2.operation_z(),
]
return '\n'.join(results)
return "\n".join(results)

View file

@ -10,10 +10,10 @@ def client_code(facade: Facade) -> None:
subsystem. This approach lets you keep the complexity under control.
"""
print(facade.operation(), end='')
print(facade.operation(), end="")
if __name__ == '__main__':
if __name__ == "__main__":
# The client code may have some of the subsystem's objects already created.
# In this case, it might be worthwhile to initialize the Facade with these
# objects instead of letting the Facade create new instances.

View file

@ -16,5 +16,4 @@ class Flyweight:
def operation(self, unique_state: List[str]) -> None:
s = json.dumps(self._shared_state)
u = json.dumps(unique_state)
print(f"Flyweight: Displaying shared ({s}) and unique ({u}) state.",
end="")
print(f"Flyweight: Displaying shared ({s}) and unique ({u}) state.", end="")

View file

@ -1,9 +1,14 @@
from structural.flyweight.flyweight_factory import FlyweightFactory
def add_car_to_police_database(factory: FlyweightFactory, plates: str,
owner: str, brand: str, model: str,
color: str) -> None:
def add_car_to_police_database(
factory: FlyweightFactory,
plates: str,
owner: str,
brand: str,
model: str,
color: str,
) -> None:
print("\n\nClient: Adding a car to database.")
flyweight = factory.get_flyweight([brand, model, color])
@ -12,25 +17,25 @@ def add_car_to_police_database(factory: FlyweightFactory, plates: str,
flyweight.operation([plates, owner])
if __name__ == '__main__':
if __name__ == "__main__":
"""
The client code usually creates a bunch of pre-populated flyweights in the
initialization stage of the application.
"""
factory = FlyweightFactory([
["Chevrolet", "Camaro2018", "pink"],
["Mercedes Benz", "C300", "black"],
["Mercedes Benz", "C500", "red"],
["BMW", "M5", "red"],
["BMW", "X6", "white"],
])
factory = FlyweightFactory(
[
["Chevrolet", "Camaro2018", "pink"],
["Mercedes Benz", "C300", "black"],
["Mercedes Benz", "C500", "red"],
["BMW", "M5", "red"],
["BMW", "X6", "white"],
]
)
factory.list_flyweights()
add_car_to_police_database(
factory, "CL234IR", "James Doe", "BMW", "M5", "red")
add_car_to_police_database(
factory, "CL234IR", "James Doe", "BMW", "X1", "red")
add_car_to_police_database(factory, "CL234IR", "James Doe", "BMW", "M5", "red")
add_car_to_police_database(factory, "CL234IR", "James Doe", "BMW", "X1", "red")
print("\n")

View file

@ -15,11 +15,11 @@ def client_code(subject: Subject) -> None:
subject.request()
if __name__ == '__main__':
if __name__ == "__main__":
print("Client: Executing the client code with a real subject:")
real_subject = RealSubject()
client_code(real_subject)
print('')
print("")
print("Client: Executing the same client code with a proxy:")
proxy = Proxy(real_subject)
client_code(proxy)