The primary theme in software engineering is that of reuse over reiteration. You’ve seen this several times in this book already with the concepts of abstraction, modularity, inheritance, data structures, and algorithms. This same concept can be applied to software design. Design patterns are a set of best practices to use when encountering a specific problem in object-oriented software design. Similar to algorithms, these are proven solutions that should be reused in similar scenarios rather than being rewritten or rediscovered. Unlike algorithms, design patterns are typically not measured in terms of time complexity because their focus pertains more to writing code that can be easily understood, maintained, and decoupled from other parts of the code base.
There are three main categories of design patterns: creational, structural, and behavioral. In this chapter, you will be provided a description of the objective of each design pattern category, an example of a pattern for each category, and the names of a few additional patterns belonging to each category that you can research further on your own. Like algorithms, entire books have been written on the subject of design patterns. As such, this chapter will not provide an exhaustive list of patterns, but rather a solid foundation on which to build upon in the future.
Creational Patterns
The objective of creational design patterns is to provide clean, idiomatic strategies for creating objects. Creational design patterns control the process of object creation by abstracting or decoupling the creation of the object away from the call site to reduce complexity in your software. Examples of creational design patterns include the builder pattern, the singleton pattern, the prototype pattern, and the factory pattern. We will explore the factory pattern in this chapter to further solidify the concept of creational design patterns.
Demonstration of the factory design pattern
In this example, we’ve created an abstract class, Character, that each character type must extend. This is an important aspect of the factory pattern. In order to build a dynamic type, each class that the factory can generate must be a sub-type of a parent abstract class or interface. Because we want to ensure type safety within our software, the return type of the factory must be of this Character class, which therefore implies any class that the factory could build must extend the Character class.
After defining the parent abstract class, we next define three case classes that extend the Character class. These three case classes (Barbarian, Magician, Archer) are private classes, meaning they cannot be instantiated outside of our factory object. The case classes are each marked with default values for the properties they need to override in order to meet the requirements of extending the abstract class. By making them case classes, we have access to a toString method that will print these sub-classes in our program in a pre-formatted manner.
Finally, we define an apply function that acts as a constructor to our CharacterFactory object. That function takes a single parameter, character, which is of type String. The argument passed to that string parameter is then put through a pattern match which returns a newly constructed Character object that is of a dynamic sub-type depending on the input parameter of the method. Our main object, which extends App to be considered the entry point of our program, asks for a user’s input and then makes a call to our character factory’s apply method. The resulting dynamically generated character sub-type is then printed to the console using the built-in toString method of the case class.
This perfectly illustrates an example of abstracting the construction of a new object that is inherent in creational design patterns. Many creational design patterns follow similar steps to protect classes from direct instantiation. This helps remove the possibility of complex code implementations that need to keep track of several object types and how to construct them, allowing for a better developer experience downstream.
Exercise 14-1
For further study, look up the builder pattern, singleton pattern, and prototype pattern. Compare and contrast these patterns with the factory pattern.
Structural Patterns
The objective of structural design patterns is to efficiently simplify the composition of objects and their inheritance relationships with other objects. Examples of common structural design patterns include the composite pattern, the proxy patterns, the extensibility pattern, the bridge pattern, and the decorator pattern. We will drill into the decorator pattern to demonstrate the concept behind structural design patterns.
The decorator pattern is a common construct wherein an object is wrapped by another object in order to modify the structure of the original object. This provides a way to dynamically add functionality to an object without changing the underlying object. The benefit of this pattern is that the additional functionality is decoupled from the base class and can be reused to decorate many other classes of the same super-type as necessary. This decoupling allows the added functionality of the decorator to only be added to instantiated objects that are required to have this functionality, which differs from traditional inheritance wherein functionality or behavior added to the parent class will impact all instantiated sub-types of that parent.
Example of the decorator design pattern
In this example, we added two new wrapper classes which take in a character as an argument, the ExtraHealth class and the ExtraRange class. Both of these classes extend the parent Character class. At construction, these classes override the properties of the parent class to satisfy the requirement of extending it by copying the values of the character that was passed in as an argument. In the case of the ExtraHealth class, when copying the hitpoints property, it adds 10 points. In the case of the ExtraRange class, when copying the reach property, it adds 10 to the reach amount. In this way, these decorators have modified the object that was passed in as an argument without changing the underlying object.
If we tried to accomplish this same functionality without a decorator, we might try to override the default values of the character that the user picked. But in order to add 10 to the value, we would have to know what the default value is, which we won’t know until after construction. We could get around this by creating a hash table of character types and their corresponding default values, but then every time we added a new character we would also have to update the table, which is less than ideal. We could instead try to modify the object after it has been constructed; however, in this case the properties of the object are immutable and can’t be changed. Because of this, the decorator pattern is the most ideal way to add bonus functionality to the existing objects without modifying their underlying implementation.
Now, to use these decorators, in the apply constructor of the CharacterFactory object, we pull the dynamically constructed object out of our original pattern matching operation and capture it in a variable called selectedCharacter. From there, we do another pattern match that we return as the value for the overall constructor on a new parameter defined for the constructor named bonus. If the argument passed into the constructor matches one of the bonuses, then the selectedCharacter variable will be wrapped by the decorator in question and returned as a newly constructed object. If no matches occur, the selectedCharacter is returned with no decoration.
Finally, we add a line to our main object to read in a user selection for the bonus of their choice. After reading in their bonus selection, we pass that bonus as an argument to the bonus parameter in the apply constructor of the CharacterFactory object. When the resulting object is then printed to the terminal, if a correct bonus keyword was entered by the user, you will notice the corresponding property being modified. By walking through this example, you will notice that wrapping an object in another object is a great example of a structural design pattern meant to enhance the composition capabilities of our software.
Exercise 14-2
For further study on this category of design patterns, look up the composite pattern, extensibility pattern, and the bridge pattern. Compare and contrast these patterns with the decorator pattern.
Behavioral Patterns
The objective of behavioral design patterns is to identify common interactions between objects and abstract their behavior. In this way, these interactions can become more flexible among different types of objects. It also allows for these objects to be loosely coupled with an overall implementation which encourages modularity and reuse. Common behavioral patterns include the observer pattern, the strategy pattern, the visitor pattern, and the iterator pattern. Let’s explore the iterator pattern in more detail.
Collection type agnostic iterator pattern
In this example, the printAll function defines an input parameter, group, which is of type Iterator[String]. This provides the flexibility to take in any collection that implements the iterator trait. The iterator trait, in Scala, is an interface that defines the methods that any collection should have in order to be considered an iterator. All iterators are required to have a hasNext method, which determines whether or not another item in the collection exists before we traverse to the next item. They also must contain a next() method which will actually traverse to the next item, if it exists. If you were defining your own custom collection and you wanted it to be able to be used by anyone implementing the iterator pattern, you would need to add the Iterator trait to your custom collection. All of the standard collections in Scala extend the iterator trait and therefore have iterator methods that can be called on them to return an iterator.
As you can see, we have defined four different collection types that each contain the same strings. If we were to write a function that printed each item in these collections, regardless of the collection type and without using higher-order functions, we wouldn’t be able to. To loop over the items of an array or a list, you might choose to use the length property of the collection to construct the loop. For the set and map collections, you would need to use the size property instead. To access the items of the map, you would need to traverse its keys and pull out the values, whereas the other collections are simply index accessible. Instead, when passing these collections to the printAll function, we can access their iterator methods to change them to a common iterator that the function can use. Because of this, our function is now decoupled from the implementation details of the overall program, leaving it open for downstream developers to use whatever collection they like.
As you can see, the iterator pattern modifies the behavior of a collection and provides an abstract process for communication between objects, which is the overall goal of behavioral patterns in general. This should allow for a better downstream developer experience and future-proof your software from potential refactors due to collection-specific implementations.
Exercise 14-3
For further study on this category of design patterns, look up the observer pattern, the strategy pattern, and the visitor pattern. Compare and contrast these patterns with the iterator pattern.
Summary
In this chapter, you were introduced to the concept of design patterns which are used predominantly in the object-oriented programming paradigm. We broke down the design patterns into three main categories: creational, structural, and behavioral. Each had their own goal for making software easier to read, use, and maintain long term. If object-oriented program is a paradigm that resonates strongly with you, I encourage you to dig in and learn more about each of the patterns that were mentioned in this chapter.