There is nothing new under the sun. It has all been done before.
—Sherlock Holmes in Sir Arthur Conan Doyle’s “A Study in Scarlet” (1887)
In Chapter 2, we started taking a look at some of the possible dos and don’ts around the design phase and introduced why software architectures are important. However, this aspect requires a deeper look. In particular, a strong emphasis on using design patterns properly is needed when dealing with architectures.
Design patterns
What each pattern does, how they are used, when to use them, and what not to do
Main issues at design time
Code Under the Shower
Creational: Which deals with common pattern for object creation
Structural: Which aims at simplifying the relations between components
Behavioral: Which establishes common pattern for communications
Concurrency: Which is designed to address multithreading scenarios
Design Patterns
Class | Patterns |
---|---|
Creational | Singleton |
Lazy initialization | |
Builder | |
Abstract factory | |
Factory method | |
Structural | Adapter |
Decorator | |
Façade | |
Composite | |
Behavioral | Publisher-subscriber |
Iterator | |
Visitor | |
State | |
Chain of responsibility | |
Concurrency | See Chapter 9 |
The images accompanying the pattern descriptions aim to illustrate the general concepts. They are not meant to model the relative Python structure.
Creational Design Patterns: The Days of Creation
Creational Design Pattern
Class | Patterns |
---|---|
Creational | Singleton |
Lazy initialization | |
Builder | |
Abstract factory | |
Factory method |
Singleton
A singleton is probably the easiest pattern you can encounter. It aims at restricting the number of instances of a class.
How
When
There is a need to implement controls to access to shared resources (e.g., in the context of concurrency).
A single resource is accessed from several parts of the system.
Only a single instance of a class is needed.
A common scenario is to use a singleton for logging purposes.
Guideline
Singleton inspires controversial thoughts. Some people love it; some do not. Someone else both hates it and loves it at the same time. From time to time, the singleton has been pointed out as a “bad smell.” One of the reasons is that it can easily fall into the first lady category analyzed in Chapter 4. You might have this big giant global instance trying to do everything. Personally, I do not see a problem in the singleton per se. If something similar happens, you are just using the wrong pattern for the problem you are trying to solve.
Lazy Initialization
This pattern allows to instantiate an object only when actually required.
How
When
Lazy instantiations are used when the heavy lifting computational job the object needs to do can be postponed for performance reasons. A common example of implementation is its alternative lazy load. A lazy load is used when integration is needed with a database (DB). Data from the DB is loaded in memory only when required.
This pattern is more on the performance side of the house rather than just clean code. There is no strict rule on when it can be used.
I’ve referred to the lazy load as a common implementation for reading data from a database. However, lazy initialization can be applied to a multitude of applications including web applications.
Guideline
If you don’t have an actual bottleneck in performances that can be improved by using this pattern, don’t use it. Simple, isn’t it?
Builder
The builder pattern follows the KISS (keep it simple stupid) principle discussed in Chapter 2. It breaks down the creation of a complex object into smaller separated creational tasks.
How
The example code shows the creation of a laptop. Virtually, a laptop can be broken down into several creational tasks that need to be performed in order to have the final product such as CPU, RAM, disk, and so on.
When
- 1.
Better control of the creational process.
- 2.
Hiding complex creation with the builder allows for easier to read and use objects.
- 3.
If any of the creational steps need to change, this will only affect a limited portion of the code (i.e., the builder itself)—not directly impacting the code that builds on top of the created object.
Guideline
The definition is fairly simple; thus, stick with it, taking into account that embracing this design pattern has minor disadvantages including writing more lines of code (LOCs). A signal that a builder is not appropriate, or not appropriately used, is when the builder constructor has a long list of parameters required to deal with each concrete builder.
Consider, for example, the case where you have a pub and you offer to customers the option to customize their hamburger meal with several toppings.
Building such hamburger might have a long list of toppings (cheese, bacon, lettuce, tomato, onion, ketchup, etc.)
In such case, proper use of the builder would entail the possibility of customizing the order by, for example, set functions instead of providing support for each and every topping at creation time.
When using this pattern, do not neglect to think longer term. Back to our burger example. What if you are offering only two options, hamburger and cheeseburger, but you ideally will add also customization with toppings in the future? Looking a bit ahead of times can help in achieving the right design and usage from the start.
Abstract Factory
The abstract factory allows you to hide complexity during object creation. This pattern enables to create different versions of the same object.
How
When
In the example code, we have our laptop, but no operating system (OS) is on top yet. The OS can be modeled as an abstract factory, and it returns an instance of one of the different flavors (different components) it supports (e.g., Linux, Mac, Windows). In general, this pattern can be used every time we support different variations of the same object.
Guideline
This pattern is a nice way to hide complexity when the caller does not need to deal with underlying computation. Common sense, do not add complexity when not required. Indeed, adding a new product is not that scalable since it requires new implementations for each factory.
Factory Method
The factory method is similar to the abstract factory, thus often confused. Guess what? Instead of building a factory object, this pattern can be synthesized as a factory (actual) method.
How
When
In general, instead of dealing with the composition of different sub-objects, this pattern is meant to create an object hiding internal details, while being a single concrete product. This is opposite to the abstract factory.
Guideline
The recommendation is exactly the same as the abstract factory. Do not opt for factories when object abstraction is not needed.
Structural Patterns: The Big Puzzle
Structural Design Pattern
Class | Patterns |
---|---|
Structural | Adapter |
Decorator | |
Façade | |
Composite |
Adapter
The adapter is also known as wrapper. It wraps another object, redefining its interface.
How
When
The adapter pattern provides a simple way for solving compatibility issues between different interfaces. Suppose a caller is expecting a different interface from a certain object (callee), it can be made compatible by means of the adapter. They can be handy for legacy software. It enables reusability for a lower price.
Guideline
Likewise, all the patterns we discuss in this book add complexity to the code. Always follow the KISS principle and make sure you have interface problems between various components when deciding to implement an adapter.
As for the builder pattern, do not neglect to think longer term. Do you foresee possible changes to an interface you are designing? Adding an adapter might be very well appropriate. At the same time, if an interface can flexibly be changed without causing dependencies to break, the adapter would add unneeded complexity.
Decorator
The decorator enables reusability by means of enhancing an object behavior.
How
Similar to the adapter pattern, it wraps the object adding the wanted functionalities. See Figure 5-7.
When
The decorator design pattern helps in fighting the first lady component smell. Thus, it adds functionalities to an object, while maintaining single responsibility principle. Indeed, the decorator allows for additional behavior without impacting the decorated component. They provide a nice alternative to inheritance and are useful when the behavior needs to be modified at runtime.
Guideline
Simple yet powerful. But, don’t make the decorator become the new first lady. Decorators can complicate the initialization process and the overall design (depending on how many decorators you implement). Make sure to not overcomplicate the design (special attention to multiple layers of decorators): you might be pushing decorators beyond the purpose they are meant to serve.
Facade
A facade can be somehow ideally associated with the abstract factory. However, instead of creating an object, it provides a simpler interface for other more complex interfaces.
How
When
If you are looking at your architecture and it is highly coupled, a facade might help in reducing it.
Guideline
It is very easy to make a facade that acts as a first lady. Please avoid it at all costs.
Composite
The composite pattern provides an interface that aims at managing a group of complex objects and single objects exposing similar functionalities in a uniform manner.
How
When
This pattern can be used when you have to selectively manage a group of heterogeneous and hierarchical objects as they would ideally be the same object. Indeed, this pattern allows for same exploration of the hierarchy, independently from the node type (i.e., leaf and composite).
As a concrete example, think about the hierarchical structure of folders, subfolders, and files on a computer. And consider that the only operation allowed is deletion. For every folder, subfolder, or file, you want the delete operation to be uniformly applied to any substructure (if any). In other words, if you delete a folder, you want to delete also all the subfolders and files contained in it. Modeling this structure as a composite would allow you to perform the deletion in a simpler and cleaner manner.
Guideline
Take a deeper look at your tree structure. A lot of initialized while not used nodes at the frontier (i.e., leaves) might signal that some refactoring is required.
Behavioral Design Patterns: Behave Code, Behave!
Behavioral Design Pattern
Class | Patterns |
---|---|
Behavioral | Observer |
Publisher-subscriber | |
Iterator | |
Visitor | |
State | |
Chain of responsibility |
Observer
In operating systems, a common way of notifying changes happening in the system is the polling and interrupts mechanisms. In the context of higher-level programming, a smarter way for notifying changes has been ideated: the observer pattern.
How
The ordering of notifications with this pattern is not strictly related to the ordering of registration.
When
The observer pattern can be used when you need different objects to perform—automatically—some functions based on the state of another one (one to many). It generally suits well cases where broadcasting of information needs to happen and the subject does not need to know specifics or the number of observers.
Guideline
Once again, keep it simple and do not add unnecessary complexity. Always carefully consider the context: do you have observers that might not be interested to all the status changes? This pattern may notify observers also of changes that they are not interested in.
Publisher-Subscriber
Similar to observer patterns, publisher-subscriber patterns enable you to monitor state changes.
How
As confusing as it might initially sound, this pattern is very similar to observer patterns, but they are not actually the same. There are two basic components: publisher, the entity whose state is monitored, and subscriber, the one that is interested in receiving state changes.
The main difference is that the dependency between them is abstracted by a third component—often referred to as broker—that manages the state’s update as shown in Figure 5-11. As a consequence, different from the observer, publisher and subscribers do not know about each other.
A common way for brokers to identify which message needs to be sent to whom is by means of topics. A topic is nothing else than an expression of interest in a specific category of message to be received. Think about subscribing to a mailing list of a library, but you only want to receive messages only for programming and fantasy books. Programming and fantasy would be the topics you subscribed to.
Be aware that the preceding code is only for showcasing the interactions between the two main components. Some methods—for example, trigger()—are added only to allow a simple flow of execution.
When
This third-party exchange is helpful in any context where message exchange is required without components (publisher with relative subscribers) being aware of each other’s existence. It is common to find pub-sub applications in almost any distributed message exchanging scenarios. For example, in the Internet of Things (IoT) world, tools such as Mosquitto and MQTT1 are commonly used to implement publisher-subscriber that allows for message exchanges between distributed elements in the network.
As you can imagine from the IoT example, any application of this pattern can use, but it is not limited to, a single publisher. Indeed, this pattern can be generalized to allow exchange of messages between any arbitrary number of publishers and subscribers.
Guideline
Do not overlook the scalability requirements of your solution. The broker might constitute a bottleneck for the entire message exchange.
Iterator
The iterator allows to navigate elements within an object, abstracting internal management.
How
- 1.
Iter: Which returns the instance object
- 2.
Next: Which will return the next value of the iterable
In the code example, StopIteration signals that there are no more elements in the collection.
When
Probably one of the most common applications is for data structures where elements within them can be (oftentimes sequentially) accessed without knowing inner functioning. However, it can be used any time a traversal is needed without introducing changes to current interfaces.
Guideline
If the collection is small and simple, it might be not really required.
Visitor
The visitor allows you to decouple operational logic (i.e., algorithms) that would be—otherwise—scattered throughout different similar objects.
How
When
An example of an application of the visitor pattern is within data structures for tree traversal (e.g., pre-order, in-order, post-order). It suites fairly well treelike structures (e.g., syntax parsing), but is not strictly tight to these cases. Visitor pattern is not used only for treelike structures. Generally speaking, it can be applied when a complex computation needs to be applied depending on the object traversed. Back to our folder example, folder deletion can be performed at every layer (subfolder, files), yet the actual deletion requires different code for the operation to be performed.
Guideline
Avoid building visitors around unstable components. If the hierarchy is likely to change over time, the visitor pattern may not be appropriate. If the structure is stable and you want to apply the same function within it, it is more likely that the visitor pattern might suit your needs.
State
The state pattern enables context-aware objects.
How
The state pattern design is fairly simple: a context that represents the external interface, a state abstract class, and different state implementations that define the actual states. See Figure 5-14.
When
The state pattern is helpful each time the behavior of an object is dependent of its state. In other words, it is applicable when the objects require changes in behavior depending on the state’s changes.
The state pattern is commonly used in user interface (UI) development. React2 provides a state built-in object that allows to dynamically reflect changes in state into the displayed UI.
Guideline
Pay close attention to when you actually use it. The number of states might exponentially grow, hence impacting the complexity of the code. It is also worth noticing that storing data that does not change in a state object is not considered a good practice due to its impact on readability: it does not necessarily affect performances, yet storing data elsewhere might increase how easy to use it would be.
Back to our react example: if the state does not change (e.g., all we want the user to see is a blank page with the title “Hello World!”), there is no real need to increase the complexity of the code for a page which is not dynamic.
Chain of Responsibility
The chain of responsibility pattern fosters decoupling between the sender of a request and the receiver.
How
When
In cases when you want to abstract the processing pipeline by allowing a request to travel until it finds a handler able to take charge of it. It is a pretty handy pattern because you can decide which and in which order handlers are added to the chain.
Guideline
Back to nonfunctional requirements. Keep an eye on the required performances. Too many handlers (executed sequentially, in worst case skipping up to the very last handler in the chain) might impact code performances. Also bear in mind that debugging this pattern could be fairly difficult.
Summary
In this chapter, we provided guidance on the most common design patterns: how do they work, when to use them, and issues to consider for each of them.
Design patterns are meant to provide solutions for common problems. When choosing or reviewing design patterns in the codebase, consider if the problem you are trying to solve is very similar to the specific goal of the design pattern you want to use in your implementation.
As always, keep it simple.
In the next chapter, we will go another step higher from the nitty-gritty details of code alone by providing guidelines from a design perspective.
Further Reading
Design patterns are widely used and discussed given their importance, ranging from basic design patterns explored in Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma and colleagues (Addison-Wesley Professional, 1994) to more advanced enterprise patterns in Patterns of Enterprise Application Architecture by Martin Fowler (Addison-Wesley Professional, 2002). The latter is probably one of my absolutely preferred books on the topic; give it a try if you are serious about design patterns.
Code Review Checklist
- 1.
Are you using design patterns properly?
- 2.
Are the patterns the optimal choice based on requirements?
- 3.
Are performances taken into account when choosing the design pattern you are inspecting?
- 4.
Is the decorator a first lady?
- 5.
Does the pattern hinder performance requirements?
- 6.
Is your singleton behaving like a first lady?