© Jason Lee Hodges 2019
J. L. HodgesSoftware Engineering from Scratchhttps://doi.org/10.1007/978-1-4842-5206-2_14

14. Design Patterns

Jason Lee Hodges1 
(1)
Draper, UT, USA
 

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.

The factory pattern is a design pattern that is used when you don’t know what type of class object needs to be constructed at compile time. So, instead of instantiating a class at compile time, the class object you want to use during runtime will need to be dynamically generated. For example, say you are building a video game with a medieval theme. The very first thing you want your game to be able to do is allow the user to pick their character. The result of the user picking your character is the game building a class of that character’s type. When designing your program, you do not know what your user is going to pick ahead of time. In this scenario, you can create a character factory to instantiate a class for you based on the user’s selection. Listing 14-1 demonstrates this factory.
object CharacterFactory {
    abstract class Character {
        val weapon: String
        val hitpoints: Int
        val reach: Int
    }
    private case class Barbarian(
        weapon: String = "Long Sword",
        hitpoints: Int = 50,
        reach: Int = 5
    ) extends Character
    private case class Magician(
        weapon: String = "Staff",
        hitpoints: Int = 30,
        reach: Int = 15
    ) extends Character
    private case class Archer(
        weapon: String = "Bow",
        hitpoints: Int = 25,
        reach: Int = 30
    ) extends Character
    def apply(character: String): Character =  {
        character match {
            case "Barbarian" => new Barbarian
            case "Magician" => new Magician
            case "Archer" => new Archer
            case _ => throw new Exception("Invalid Selection")
        }
    }
}
object main extends App {
    val choice = readLine("Choose your character: ")
    println(CharacterFactory(choice))
}
Listing 14-1

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.

As an example, suppose that you want to extend your medieval game to allow the user to select a bonus for the selected character to add to the character’s inherent abilities. This type of dynamic selection needs to be applied to the character at runtime. In this scenario, you can define a wrapper class for each bonus that will not affect the code for each of the character classes that we’ve already built, but rather extend their capabilities. To do this, we must first define the wrapper classes and then use them in our apply constructor method. Listing 14-2 provides an example implementation of this scenario.
...
    def apply(character: String, bonus: String): Character =  {
        val selectedCharacter = character match {
            case "Barbarian" => new Barbarian
            case "Magician" => new Magician
            case "Archer" => new Archer
            case _ => throw new Exception("Invalid Selection")
        }
        return bonus match {
            case "Extra Health" => new ExtraHealth(selectedCharacter)
            case "Extra Range" => new ExtraRange(selectedCharacter)
            case _ => selectedCharacter
        }
    }
    private case class ExtraHealth(character: Character) extends Character {
        override val weapon = character.weapon
        override val hitpoints = character.hitpoints + 10
        override val reach = character.reach
        override def toString(): String = {
            return s"${character.getClass.getSimpleName}(${weapon},${hitpoints},${reach})"
        }
    }
    private case class ExtraRange(character: Character) extends Character {
        override val weapon = character.weapon
        override val hitpoints = character.hitpoints
        override val reach = character.reach + 10
        override def toString(): String = {
            return s"${character.getClass.getSimpleName}(${weapon},${hitpoints},${reach})"
        }
    }
}
object main extends App {
    val choice = readLine("Choose your character: ")
    val bonus = readLine("Choose bonus: ")
    println(CharacterFactory(choice, bonus))
}
Listing 14-2

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.

The main goal of the iterator pattern is to provide a common interface for traversing different types of data collections. By creating this common interface, algorithms that use data collections can be decoupled from any particular collection type. This allows the algorithm to simply focus on its own implementation logic rather than collection specific logic, which provides the flexibility to use all collection types in that algorithm. Listing 14-3 provides an example of using an iterator in a function that is collection type agnostic.
val charactersList = List("Barbarian", "Magician", "Archer")
val charactersArray = Array("Barbarian", "Magician", "Archer")
val charactersSet = Set("Barbarian", "Magician", "Archer")
val charactersMap = Map(1 -> "Barbarian", 2 -> "Magician", 3 -> "Archer")
printAll(charactersList.iterator)
printAll(charactersArray.iterator)
printAll(charactersSet.iterator)
printAll(charactersMap.valuesIterator)
def printAll(group: Iterator[String]) {
    while (group.hasNext) {
        println(group.next())
    }
}
Listing 14-3

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset