Structural patterns explain how to assemble objects and classes into larger structures, while keeping these structures flexible and efficient.
Taken fully from refactoring.guru, head there for a more detailed explanation. No code was written by me, I am just able to execute it here.
Bridge
Bridge is useful when we need to extend a class in several orthogonal (independent) dimensions. It separates the abstraction from the implementation using composition, so that they can be modified independently.
from __future__ import annotationsfrom abc import ABC, abstractmethodclass Abstraction:""" The Abstraction defines the interface for the "control" part of the two class hierarchies. It maintains a reference to an object of the Implementation hierarchy and delegates all of the real work to this object. """def__init__(self, implementation: Implementation) ->None:self.implementation = implementationdef operation(self) ->str:return (f"Abstraction: Base operation with:\n"f"{self.implementation.operation_implementation()}")class ExtendedAbstraction(Abstraction):""" You can extend the Abstraction without changing the Implementation classes. """def operation(self) ->str:return (f"ExtendedAbstraction: Extended operation with:\n"f"{self.implementation.operation_implementation()}")class Implementation(ABC):""" The Implementation defines the interface for all implementation classes. It doesn't have to match the Abstraction's interface. In fact, the two interfaces can be entirely different. Typically the Implementation interface provides only primitive operations, while the Abstraction defines higher- level operations based on those primitives. """@abstractmethoddef operation_implementation(self) ->str:pass"""Each Concrete Implementation corresponds to a specific platform and implementsthe Implementation interface using that platform's API."""class ConcreteImplementationA(Implementation):def operation_implementation(self) ->str:return"ConcreteImplementationA: Here's the result on the platform A."class ConcreteImplementationB(Implementation):def operation_implementation(self) ->str:return"ConcreteImplementationB: Here's the result on the platform B."def client_code(abstraction: Abstraction) ->None:""" Except for the initialization phase, where an Abstraction object gets linked with a specific Implementation object, the client code should only depend on the Abstraction class. This way the client code can support any abstraction- implementation combination. """# ...print(abstraction.operation(), end="")# ...if__name__=="__main__":""" The client code should be able to work with any pre-configured abstraction- implementation combination. """ implementation = ConcreteImplementationA() # KEY POINT abstraction = Abstraction(implementation) client_code(abstraction)print("\n") implementation = ConcreteImplementationB() abstraction = ExtendedAbstraction(implementation) client_code(abstraction)
Abstraction: Base operation with:
ConcreteImplementationA: Here's the result on the platform A.
ExtendedAbstraction: Extended operation with:
ConcreteImplementationB: Here's the result on the platform B.
Adapter
Adapter is useful as an afterthought solution (unlike Bridge).
class Target:""" The Target defines the domain-specific interface used by the client code. """def request(self) ->str:return"Target: The default target's behavior."class Adaptee:""" The Adaptee contains some useful behavior, but its interface is incompatible with the existing client code. The Adaptee needs some adaptation before the client code can use it. """def specific_request(self) ->str:return".eetpadA eht fo roivaheb laicepS"class Adapter(Target, Adaptee): # KEY POINT""" The Adapter makes the Adaptee's interface compatible with the Target's interface via multiple inheritance. """def request(self) ->str:returnf"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"def client_code(target: "Target") ->None:""" The client code supports all classes that follow the Target interface. """print(target.request(), end="")if__name__=="__main__":print("Client: I can work just fine with the Target objects:") target = Target() client_code(target)print("\n") adaptee = Adaptee()print("Client: The Adaptee class has a weird interface. ""See, I don't understand it:")print(f"Adaptee: {adaptee.specific_request()}", end="\n\n")print("Client: But I can work with it via the Adapter:") adapter = Adapter() client_code(adapter)
Client: I can work just fine with the Target objects:
Target: The default target's behavior.
Client: The Adaptee class has a weird interface. See, I don't understand it:
Adaptee: .eetpadA eht fo roivaheb laicepS
Client: But I can work with it via the Adapter:
Adapter: (TRANSLATED) Special behavior of the Adaptee.
Composite
Composite is all about tree structures. Typically, only leaf nodes do the actual work, while non-leaf nodes (i.e. composites) delegate to their children and sum up the results.
from __future__ import annotationsfrom abc import ABC, abstractmethodfrom typing import Listclass Component(ABC):""" The base Component class declares common operations for both simple and complex objects of a composition. """@propertydef parent(self) -> Component:returnself._parent@parent.setterdef parent(self, parent: Component):""" Optionally, the base Component can declare an interface for setting and accessing a parent of the component in a tree structure. It can also provide some default implementation for these methods. """self._parent = parent""" In some cases, it would be beneficial to define the child-management operations right in the base Component class. This way, you won't need to expose any concrete component classes to the client code, even during the object tree assembly. The downside is that these methods will be empty for the leaf-level components. """def add(self, component: Component) ->None:passdef remove(self, component: Component) ->None:passdef is_composite(self) ->bool:""" You can provide a method that lets the client code figure out whether a component can bear children. """returnFalse@abstractmethoddef operation(self) ->str:""" The base Component may implement some default behavior or leave it to concrete classes (by declaring the method containing the behavior as "abstract"). """passclass Leaf(Component):""" The Leaf class represents the end objects of a composition. A leaf can't have any children. Usually, it's the Leaf objects that do the actual work, whereas Composite objects only delegate to their sub-components. """def operation(self) ->str:return"Leaf"class Composite(Component):""" The Composite class represents the complex components that may have children. Usually, the Composite objects delegate the actual work to their children and then "sum-up" the result. # KEY POINT """def__init__(self) ->None:self._children: List[Component] = []""" A composite object can add or remove other components (both simple or complex) to or from its child list. """def add(self, component: Component) ->None:self._children.append(component) component.parent =selfdef remove(self, component: Component) ->None:self._children.remove(component) component.parent =Nonedef is_composite(self) ->bool:returnTruedef operation(self) ->str:""" The Composite executes its primary logic in a particular way. It traverses recursively through all its children, collecting and summing their results. Since the composite's children pass these calls to their children and so forth, the whole object tree is traversed as a result. """ results = []for child inself._children: results.append(child.operation())returnf"Branch({'+'.join(results)})"def client_code(component: Component) ->None:""" The client code works with all of the components via the base interface. """print(f"RESULT: {component.operation()}", end="")def client_code2(component1: Component, component2: Component) ->None:""" Thanks to the fact that the child-management operations are declared in the base Component class, the client code can work with any component, simple or complex, without depending on their concrete classes. """if component1.is_composite(): component1.add(component2)print(f"RESULT: {component1.operation()}", end="")if__name__=="__main__":# This way the client code can support the simple leaf components... simple = Leaf()print("Client: I've got a simple component:") client_code(simple)print("\n")# ...as well as the complex composites. tree = Composite() branch1 = Composite() branch1.add(Leaf()) branch1.add(Leaf()) branch2 = Composite() branch2.add(Leaf()) tree.add(branch1) tree.add(branch2)print("Client: Now I've got a composite tree:") client_code(tree)print("\n")print("Client: I don't need to check the components classes even when managing the tree:") client_code2(tree, simple)
Client: I've got a simple component:
RESULT: Leaf
Client: Now I've got a composite tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))
Client: I don't need to check the components classes even when managing the tree:
RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)
Decorator
We can create decorated objects without having to change any code of real objects, instead, just decorating them. A user can run code on real object or a decorated one. This way we allows adding new behaviors to objects dynamically.
class Component():""" The base Component interface defines operations that can be altered by decorators. """def operation(self) ->str:passclass ConcreteComponent(Component):""" Concrete Components provide default implementations of the operations. There might be several variations of these classes. """def operation(self) ->str:return"ConcreteComponent"class Decorator(Component):""" The base Decorator class follows the same interface as the other components. The primary purpose of this class is to define the wrapping interface for all concrete decorators. The default implementation of the wrapping code might include a field for storing a wrapped component and the means to initialize it. """ _component: Component =Nonedef__init__(self, component: Component) ->None:self._component = component@propertydef component(self) -> Component:""" The Decorator delegates all work to the wrapped component. """returnself._componentdef operation(self) ->str:returnself._component.operation()class ConcreteDecoratorA(Decorator):""" Concrete Decorators call the wrapped object and alter its result in some way. """def operation(self) ->str:""" Decorators may call parent implementation of the operation, instead of calling the wrapped object directly. This approach simplifies extension of decorator classes. """returnf"ConcreteDecoratorA({self.component.operation()})"# KEY POINTclass ConcreteDecoratorB(Decorator):""" Decorators can execute their behavior either before or after the call to a wrapped object. """def operation(self) ->str:returnf"ConcreteDecoratorB({self.component.operation()})"def client_code(component: Component) ->None:""" The client code works with all objects using the Component interface. This way it can stay independent of the concrete classes of components it works with. """# ...print(f"RESULT: {component.operation()}", end="")# ...if__name__=="__main__":# This way the client code can support both simple components... simple = ConcreteComponent()print("Client: I've got a simple component:") client_code(simple)print("\n")# ...as well as decorated ones.## Note how decorators can wrap not only simple components but the other# decorators as well. decorator1 = ConcreteDecoratorA(simple) decorator2 = ConcreteDecoratorB(decorator1)print("Client: Now I've got a decorated component:") client_code(decorator2)
Client: I've got a simple component:
RESULT: ConcreteComponent
Client: Now I've got a decorated component:
RESULT: ConcreteDecoratorB(ConcreteDecoratorA(ConcreteComponent))
Facade
Facade delegates work to subsystems. It is a part of the design process and is not applied after the fact like the adapter pattern.
from __future__ import annotationsclass Facade:""" The Facade class provides a simple interface to the complex logic of one or several subsystems. The Facade delegates the client requests to the appropriate objects within the subsystem. The Facade is also responsible for managing their lifecycle. All of this shields the client from the undesired complexity of the subsystem. """def__init__(self, subsystem1: Subsystem1, subsystem2: Subsystem2) ->None:""" Depending on your application's needs, you can provide the Facade with existing subsystem objects or force the Facade to create them on its own. """self._subsystem1 = subsystem1 or Subsystem1()self._subsystem2 = subsystem2 or Subsystem2()def operation(self) ->str:""" The Facade's methods are convenient shortcuts to the sophisticated 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())return"\n".join(results)class Subsystem1:""" The Subsystem can accept requests either from the facade or client directly. In any case, to the Subsystem, the Facade is yet another client, and it's not a part of the Subsystem. """def operation1(self) ->str:return"Subsystem1: Ready!"# ...def operation_n(self) ->str:return"Subsystem1: Go!"class Subsystem2:""" Some facades can work with multiple subsystems at the same time. """def operation1(self) ->str:return"Subsystem2: Get ready!"# ...def operation_z(self) ->str:return"Subsystem2: Fire!"def client_code(facade: Facade) ->None:""" The client code works with complex subsystems through a simple interface provided by the Facade. When a facade manages the lifecycle of the subsystem, the client might not even know about the existence of the subsystem. This approach lets you keep the complexity under control. """print(facade.operation(), end="")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. subsystem1 = Subsystem1() subsystem2 = Subsystem2() facade = Facade(subsystem1, subsystem2) # KEY POINT client_code(facade)
Facade initializes subsystems:
Subsystem1: Ready!
Subsystem2: Get ready!
Facade orders subsystems to perform the action:
Subsystem1: Go!
Subsystem2: Fire!
Flyweight
Is all about caching shared data to reduce memory footprint.
import jsonfrom typing import Dictclass Flyweight():""" The Flyweight stores a common portion of the state (also called intrinsic state) that belongs to multiple real business entities. The Flyweight accepts the rest of the state (extrinsic state, unique for each entity) via its method parameters. """def__init__(self, shared_state: str) ->None:self._shared_state = shared_statedef operation(self, unique_state: str) ->None: s = json.dumps(self._shared_state) u = json.dumps(unique_state)print(f"Flyweight: Displaying shared ({s}) and unique ({u}) state.", end="")class FlyweightFactory():""" The Flyweight Factory creates and manages the Flyweight objects. It ensures that flyweights are shared correctly. When the client requests a flyweight, the factory either returns an existing instance or creates a new one, if it doesn't exist yet. """ _flyweights: Dict[str, Flyweight] = {}def__init__(self, initial_flyweights: Dict) ->None:for state in initial_flyweights:self._flyweights[self.get_key(state)] = Flyweight(state)def get_key(self, state: Dict) ->str:""" Returns a Flyweight's string hash for a given state. """return"_".join(sorted(state))def get_flyweight(self, shared_state: Dict) -> Flyweight:""" Returns an existing Flyweight with a given state or creates a new one. """ key =self.get_key(shared_state)ifnotself._flyweights.get(key): # KEY POINTprint("FlyweightFactory: Can't find a flyweight, creating new one.")self._flyweights[key] = Flyweight(shared_state)else:print("FlyweightFactory: Reusing existing flyweight.")returnself._flyweights[key]def list_flyweights(self) ->None: count =len(self._flyweights)print(f"FlyweightFactory: I have {count} flyweights:")print("\n".join(map(str, self._flyweights.keys())), end="")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])# The client code either stores or calculates extrinsic state and passes it# to the flyweight's methods. flyweight.operation([plates, owner])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.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")print("\n") factory.list_flyweights()
FlyweightFactory: I have 5 flyweights:
Camaro2018_Chevrolet_pink
C300_Mercedes Benz_black
C500_Mercedes Benz_red
BMW_M5_red
BMW_X6_white
Client: Adding a car to database.
FlyweightFactory: Reusing existing flyweight.
Flyweight: Displaying shared (["BMW", "M5", "red"]) and unique (["CL234IR", "James Doe"]) state.
Client: Adding a car to database.
FlyweightFactory: Can't find a flyweight, creating new one.
Flyweight: Displaying shared (["BMW", "X1", "red"]) and unique (["CL234IR", "James Doe"]) state.
FlyweightFactory: I have 6 flyweights:
Camaro2018_Chevrolet_pink
C300_Mercedes Benz_black
C500_Mercedes Benz_red
BMW_M5_red
BMW_X6_white
BMW_X1_red
Proxy
Client can call both real object or proxy; when calling proxy we can add hooks.
from abc import ABC, abstractmethodclass Subject(ABC):""" The Subject interface declares common operations for both RealSubject and the Proxy. As long as the client works with RealSubject using this interface, you'll be able to pass it a proxy instead of a real subject. """@abstractmethoddef request(self) ->None:passclass RealSubject(Subject):""" The RealSubject contains some core business logic. Usually, RealSubjects are capable of doing some useful work which may also be very slow or sensitive - e.g. correcting input data. A Proxy can solve these issues without any changes to the RealSubject's code. """def request(self) ->None:print("RealSubject: Handling request.")class Proxy(Subject):""" The Proxy has an interface identical to the RealSubject. """def__init__(self, real_subject: RealSubject) ->None:self._real_subject = real_subjectdef request(self) ->None:""" The most common applications of the Proxy pattern are lazy loading, caching, controlling the access, logging, etc. A Proxy can perform one of these things and then, depending on the result, pass the execution to the same method in a linked RealSubject object. """ifself.check_access(): # KEY POINTself._real_subject.request()self.log_access()def check_access(self) ->bool:print("Proxy: Checking access prior to firing a real request.")returnTruedef log_access(self) ->None:print("Proxy: Logging the time of request.", end="")def client_code(subject: Subject) ->None:""" The client code is supposed to work with all objects (both subjects and proxies) via the Subject interface in order to support both real subjects and proxies. In real life, however, clients mostly work with their real subjects directly. In this case, to implement the pattern more easily, you can extend your proxy from the real subject's class. """# ... subject.request()# ...if__name__=="__main__":print("Client: Executing the client code with a real subject:") real_subject = RealSubject() client_code(real_subject)print("")print("Client: Executing the same client code with a proxy:") proxy = Proxy(real_subject) client_code(proxy)
Client: Executing the client code with a real subject:
RealSubject: Handling request.
Client: Executing the same client code with a proxy:
Proxy: Checking access prior to firing a real request.
RealSubject: Handling request.
Proxy: Logging the time of request.