Mark Houston, Software Engineer II
Previously, we discussed how design patterns guide developers through common recurring design challenges. We also looked at how creational and structural design patterns can be applied in real-world scenarios. In this post, we will conclude the series by exploring the role of behavioral design patterns within software design. In addition, we will identify ways in which common behavioral patterns can be applied to our mom-and-pop sandwich shop to express real-world examples of behavioral patterns.
Behavioral Design Patterns
While structural design patterns (discussed in the previous post) extend properties and logic across object definitions, behavioral design patterns identify communication patterns between objects and outline strategies by which objects can extend and support one another during runtime. Unlike objects that adopt the traits or functionality of other objects as seen in structural patterns, behavioral patterns provide solutions that separate concerns between objects to create flexible, decoupled code adaptable throughout a program’s lifecycle. Reasons to implement a behavioral pattern include:
Communication between objects is needed
Runtime events affect an entity’s usage of other objects and their functionalities
The desire for decoupled code that allows entities to focus on unique responsibilities
As a first look at behavioral design patterns, this article will focus on the template and mediator patterns. These patterns, along with other behavioral patterns such as the chain of responsibility, command, interpreter, observer, and visitor patterns, all aim to streamline the communication among entities by separating responsibilities appropriately between objects in a system.
The Template Pattern
The Template pattern serves as a method to implement entities that share some identical instantiation steps but require the flexibility to define their own implementation of other steps in the creation process. In this pattern, a method stub is used to define the implementation of shared implementation steps, while the other implementation steps are deferred to each child class extending the method stub. A common use case for the template pattern is a code generator. Since generated code typically consists of source code and hand-written modifications, the template method extends the source code with the flexibility to inject hand-written code where applicable.
In the case of our mom-and-pop shop, we can use the template pattern to implement various types of pizza from the menu. Since all kinds of pizza share common characteristics like dough, cheese, and cook time, a method stub PizzaTemplate can implement these steps. The remaining measures, such as choosing a sauce and adding toppings, can be implemented inside each child class since these implementations will be unique for each type of pizza:
The Mediator Pattern
In complex systems, it is often necessary to centralize the communication between entities so that commands can be delegated across the system in a controlled manner. The mediator pattern suggests implementing an entity that serves as a delegator for communication to other entities in the system to assist with this task. This design pattern allows for decoupled code since components do not need to communicate directly and instead rely on the mediator to handle the communication and delegation of tasks. For example, a common application of the mediator pattern is a chat room application (Facebook, WhatsApp, etc.) where a server acts as the mediator, and each user’s chat box communicates through a web socket to the server.
In the setting of our mom-and-pop shop, we can apply the mediator pattern to the
restaurant’s order-placing process. Without a mediator, customers would have to talk directly to both the cashier and the cook; the cashier would take the customer’s order, then the customer would travel to the kitchen to tell the chef what food item to cook. This system is poor because it would be inconvenient for the customer and
interfere with the cashier’s role of placing orders and the cook’s job of preparing the food.
The mediator pattern would suggest that another separate entity should handle communication with the customer, along with the delegation of tasks to the cashier and the cook. To accomplish this, we can implement the OrderMediator entity:
By implementing the OrderMediator, each Person can rely on the OrderMediator to send messages to the desired receivers. In addition, each child class extending Person implements unique logic under receiveMessage to handle the message and perform an action depending on the message content and the role of the child class (Customer, Cashier, or Chef).
As we have seen throughout this series, design patterns are helpful tools for identifying common software design challenges and provide strategies to build flexible solutions. In general, design patterns fall under one of the following classifications:
Creational patterns center around object instantiation.
Structural patterns focus on extending object definitions based on their association to other entities.
Behavioral patterns decouple related entities through solutions that are adaptable during runtime.
As a developer, keeping design patterns at one’s disposal will encourage the development of code that is nimble, comprehensive, and sustainable. Furthermore, software developers can more confidently foster a healthy and maintainable codebase by using design patterns to guide code design in the face of architectural decisions.