mirror of
https://github.com/rjNemo/design-patterns
synced 2026-06-06 02:26:40 +00:00
Observer (#20)
* doc: create package & update general TOCs * doc: add documentation * add code example * reformat using black
This commit is contained in:
parent
bf1d5dd7f0
commit
c8cc1f47b8
26 changed files with 277 additions and 58 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
47
behavioral/observer/README.md
Normal file
47
behavioral/observer/README.md
Normal 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 they’re 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, it’d upset other customers who aren't interested in new products.
|
||||
|
||||
It looks like we’ve 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 it’s also going to notify other objects about the changes to its state, we’ll call it publisher. All other objects that want to track changes to the publisher’s 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 isn’t 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.
|
||||
|
||||
That’s why it’s 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 you’re 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 there’s 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.
|
||||
4
behavioral/observer/__init__.py
Normal file
4
behavioral/observer/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
"""
|
||||
Lets you define a subscription mechanism to notify multiple objects about any
|
||||
events that happen to the object they’re observing.
|
||||
"""
|
||||
18
behavioral/observer/concrete_observers.py
Normal file
18
behavioral/observer/concrete_observers.py
Normal 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")
|
||||
61
behavioral/observer/concrete_subject.py
Normal file
61
behavioral/observer/concrete_subject.py
Normal 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()
|
||||
23
behavioral/observer/main.py
Normal file
23
behavioral/observer/main.py
Normal 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()
|
||||
16
behavioral/observer/observer.py
Normal file
16
behavioral/observer/observer.py
Normal 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
|
||||
35
behavioral/observer/subject.py
Normal file
35
behavioral/observer/subject.py
Normal 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
|
||||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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="")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in a new issue