- Design Principles and Patterns
A class should have only 1 reason to change. Responsibility implies a reason for change.
Python:
Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Python:
Subtypes must be substitutable for their base types.
Python:
Clients should not be forced to depend on methods that they do not use.
Python:
High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
Python:
- When piecewise object construction is complicated, provide an API for doing it succinctly
- Motivation
- some objects are simple and can be created in a single initialiser call
- other objects require a lot of ceremony to create
- having an object with 10 initialiser arguments is not productive
- instead, opt of piecewise construction
- Builder provides an API for constructing an object step-by-step
Python:
builder.py
builder_test.py
builder_facets.py
builder_facets_test.py
builder_inheritance.py
builder_inheritance_test.py
- Motivation
- object creation logic becomes too convoluted
- initialiser is not descriptive
- cannot overload with the same sets of arguments with different names
- can turn into 'optional parameter hell'
- Wholesale object creation (not piecewise like Builder) can be outsourced to:
- a separate method (Factory Method)
- a static method that creates objects
- a separate class (Factory)
- any entity that can take care of object creation
- a hierarchy of factories (Abstract Factory)
- correspond to a hierarchy of types
- a separate method (Factory Method)
Python:
factory_method.py
factory_method_test.py
factory.py
factory_test.py
abstract_factory.py
abstract_factory_test.py
- Motivation
- when it's easier to copy an existing object to fully initialise a new one
- Prototype - a partially or fully initialised object that you copy (clone) and make use of
- We make a copy (clone) of the prototype and customise it
- requires 'deep copy' support
- We make cloning convenient (e.g., via a Factory)
Python:
- A component that is instantiated only once
- Motivation
- for some components it only makes sense to have one in the system
- database repository
- object factory
- the initialiser call is expensive
- object represents a resource and there is only one instance of the resource
- for some components it only makes sense to have one in the system
- Provide everyone with the same instance
- prevent anyone creating additional copies
- Lazy instantiation
- initialise only when someone actually asks for it
Python:
- Adapt the interface you are given to the interface that you actually need
Python:
- Decouple an abstraction from its implementation so that the two can vary independently
- Complex elements in a system can sometimes vary in both their external functionality and their underlying
implementation
- in such cases, inheritance is an undesirable solution
- the number of classes you must create increases as a function of both these aspects
- 2 representations and implementations yield 4 classes to develop
- 3 representations and implementations result in 9 classes
- in such cases, inheritance is an undesirable solution
- Inheritance ties a component into a static model, making it difficult to change in the future
- It would be preferable to create a dynamic way to vary both aspects of the component on an as-needed basis
- Bridge solves the problem by decoupling the 2 aspects of the component
- 2 separate inheritance chains
- one devoted to functionality (abstraction)
- the other to implementation
- it's much easier to mix and match elements from each side
- 2 separate inheritance chains
- The coding requirements for Bridge give you an overall savings in the number of classes written as you increase the number of variations
- We refer to the relationship as a bridge, because it bridges the abstraction and its implementation, letting them vary independently
- You want to avoid a permanent binding between an abstraction and its implementation
- when the implementation must be selected or switched at run-time
- Both the abstractions and their implementations should be extensible by subclassing
- the Bridge pattern lets you combine the different abstractions and implementations and extend them independently
- Changes in the implementation of an abstraction should have no impact on clients
- You want to hide the implementation of an abstraction completely from clients
- You have a proliferation of classes
- such a class hierarchy indicates the need for splitting an object into two parts
- Abstraction
- defines the abstraction's interface
- maintains a reference to an object of type Implementor
- RefinedAbstraction
- extends the interface defined by Abstraction.
- Implementor
- defines the interface for implementation classes
- doesn't have to correspond exactly to Abstraction's interface - the two interfaces can be quite different
- typically the Implementor interface provides only primitive operations, and Abstraction defines higher-level operations based on these primitives
- ConcreteImplementor
- implements the Implementor interface and defines its concrete implementation
- Abstraction forwards client requests to its Implementor object
- Decoupling interface and implementation
- an implementation is not bound permanently to an interface
- the implementation of an abstraction can be configured at run-time
- it's possible for an object to change its implementation at run-time
- eliminates compile-time dependencies on the implementation
- changing an implementation class doesn't require recompiling the Abstraction class and its clients
- encourages layering that can lead to a better-structured system
- the high-level part of a system only has to know about Abstraction and Implementor
- an implementation is not bound permanently to an interface
- Improved extensibility
- you can extend the Abstraction and Implementor hierarchies independently
Consider the following implementation issues when applying the Bridge pattern:
- Only one Implementor
- in situations where there's only one implementation - a degenerate case of the Bridge pattern - creating an abstract Implementor class isn't necessary
- this separation is still useful when a change in the implementation of a class must not affect its existing clients
- Creating the right Implementor object
- how, when, and where do you decide which Implementor class to instantiate when there's more than one?
- if Abstraction knows about all ConcreteImplementor classes, then it can instantiate one of them in its constructor
- another approach is to choose a default implementation initially and change it later according to usage
- it's also possible to delegate the decision to another object altogether
- we can introduce a factory object (see Abstract Factory)
- the factory knows what kind of object to create
- a benefit of this approach is that Abstraction is not coupled directly to any of the Implementor classes
- Using multiple inheritance
- you can use multiple inheritance to combine an interface with its implementation
- e.g., a class can inherit publicly from Abstraction and privately from a ConcreteImplementor
- this approach relies on static inheritance - it binds an implementation permanently to its interface
- you can't implement a true Bridge with multiple inheritance
- An Abstract Factory can create and configure a particular Bridge
- The Adapter pattern is geared toward making unrelated classes work together
- it is usually applied to systems after they're designed
- Bridge, on the other hand, is used up-front in a design to let abstractions and implementations vary independently
Python:
- Compose objects into tree structures to represent part-whole hierarchies
- Composite lets clients treat individual objects and compositions of objects uniformly
- Some applications let users build complex entities out of simple components
- the user can group components to form larger components, which in turn can be grouped to form even larger components
- A simple implementation could define classes for the primitives (simple components) plus other classes that act as containers for these primitives
- the problem is client code that uses these classes must treat primitive and container objects differently, even if most of the time the user treats them identically
- makes the application more complex
- The Composite pattern describes how to use recursive composition so that clients don't have to make this distinction
- The key to the Composite pattern is an abstract class that represents both primitives and their containers
- declares operations that are specific to the primitives
- declares operations that all composite/container objects share, such as operations for accessing and managing its children
- Primitive objects have no child components, so they don't implement child-related operations
- A container object defines an aggregate of primitive objects
- implements primitive objects' operations to call the corresponding operations of its children
- implements child-related operations
- can compose other container objects recursively
- You want to represent part-whole hierarchies of objects
- You want clients to be able to ignore the difference between compositions of objects and individual objects; clients will treat all objects in the composite structure uniformly
- Component
- declares the interface for objects in the composition
- implements default behaviour for the interface common to all classes, as appropriate
- declares an interface for accessing and managing its child components
- (optional) defines an interface for accessing a component's parent in the recursive structure, and implements it if that's appropriate
- Leaf
- represents leaf objects in the composition
- has no children
- defines behaviour for primitive objects in the composition
- Composite
- defines behaviour for components having children
- stores child components
- implements child-related operations in the Component interface
- Client
- manipulates objects in the composition through the Component interface
- Clients use the Component class interface to interact with objects in the composite structure
- If the recipient is a Leaf, then the request is handled directly
- If the recipient is a Composite, then it usually forwards requests to its child components, possibly performing additional operations before and/or after forwarding
- Defines class hierarchies consisting of primitive objects and composite objects
- Makes the client simple
- clients can treat composite structures and individual objects uniformly
- Makes it easier to add new kinds of components
- clients don't have to be changed for new Component classes
- Can make your design overly general
- sometimes you want a composite to have only certain components
- you can't rely on the type system to enforce those constraints for you
- you'll have to use run-time checks
- Explicit parent references
- maintaining references from child components to their parent can simplify the traversal and management of a composite structure
- the usual place to define the parent reference is in the Component class
- Leaf and Composite classes can inherit the reference and the operations that manage it
- essential to maintain the invariant that all children of a composite have as their parent the composite that in turn has them as children
- can be implemented once in the
add
andremove
operations of the Composite class, then it can be inherited by all the subclasses
- can be implemented once in the
- Sharing components
- it's often useful to share components, for example, to reduce storage requirements
- but when a component can have no more than one parent, sharing components becomes difficult
- the Flyweight pattern shows how to rework a design to avoid storing parents altogether
- works in cases where children can avoid sending parent requests by externalising some or all of their state
- Maximising the Component interface
- in order to make clients unaware of the specific Leaf or Composite classes they're using, the Component class should define as many common operations for Composite and Leaf classes as possible
- sometimes conflict with the principle of class hierarchy design that says a class should only define operations that are meaningful to its subclasses
- the interface for accessing children is a fundamental part of a Composite class but not necessarily Leaf classes
- Declaring the child management operations
- an important issue in the Composite pattern is which classes declare these operations in the Composite class hierarchy
- the decision involves a trade-off between safety and transparency:
- defining the child management interface at the root of the class hierarchy gives you transparency, because you can treat all components uniformly
- it costs you safety because clients may try to do meaningless things like add and remove objects from leaves
- defining child management in the Composite class gives you safety, but you lose transparency because leaves and composites have different interfaces
- defining the child management interface at the root of the class hierarchy gives you transparency, because you can treat all components uniformly
- we emphasise transparency over safety in this pattern
- define default
add
andremove
operations in Component - usually it's better to make
add
andremove
fail by default (perhaps by raising an exception) if the component isn't allowed to have children or if the argument ofremove
isn't a child of the component, respectively
- define default
- Should Component implement a list of Components?
- putting the child pointer in the Component class incurs a space penalty for every leaf, even though a leaf never has children
- Caching to improve performance
- if you need to traverse or search compositions frequently, the Composite class can cache traversal or search information about its children
- changes to a component will require invalidating the caches of its parents
- works best when components know their parents
- define an interface for telling composites that their caches are invalid
- Often the component-parent link is used for a Chain of Responsibility
- Decorator is often used with Composite
- Flyweight lets you share components, but they can no longer refer to their parents
- Iterator can be used to traverse composites
- Visitor localises operations and behaviour that would otherwise be distributed across Composite and Leaf classes
Python:
- Nesteruk, Dmitri. "Design Patterns in Python for Engineers, Designers, and Architects." Udemy, Udemy, Inc., Aug. 2020, www.udemy.com/course/design-patterns-python/.
- Johnson, Ralph, et al. "Design Patterns CD: Elements of Reusable Object-oriented Software." United Kingdom, Addison-Wesley, 1998.
- Stelting, Stephen, and Olav Maassen. "Applied Java Patterns." Prentice Hall PTR, 2001.