Design Patterns
Building more beautiful and repeatable software!
Building more beautiful and repeatable software!
Up to this point, we’ve mainly been developing our programs without any underlying patterns between them. Each program is custom-written to fit the particular situation or use case, and beyond using a few standard data structures, the code and structure within each program is mostly unique to that application. In this chapter, we’re going to learn about software design patterns, a very powerful aspect of object-oriented programming and a way that we can write code that is more easily recognized and understood by other developers. By using these patterns, we can see that many unrelated programs can actually share similar code structures and ideas.
Some of the key terms we’ll cover in this chapter:
After reviewing this chapter, we should be able to recognize and use several of the most common design patterns in our code.
While the first discussions of patterns in software architecture occurred much earlier, the concept was popularized in 1994 with the publication of Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Collectively, the four authors of this book have been referred to as the “Gang of Four” or “GoF” within the software development community, so it is common to see references to that name when discussing the particular software design patterns discussed in the book.
In their book, the authors give their thoughts on how to build software following the object-oriented programming paradigm. This includes focusing on the use of interfaces to design how classes should appear to function to an outside observer, while leaving the actual implementation details hidden within the class. Likewise, they favor the use of object composition over inheritance - instead of inheriting functionality from another class, simply store an internal instance of that class and use the public methods it contains.
The entire first chapter of the book is a really great look at one way to view object-oriented programming, and many of the items discussed by the authors have been implemented by software developers as standard practice. In fact, it is still one of the best selling books on software architecture and design, even decades after its release!
Of course, it isn’t without criticism. One major complaint of this particular book is that it was developed to address several things that cannot be easily done in C++, which have been better handled in newer programming languages. In addition, the reliance on reusable software design patterns may feel a bit like making the problem fit the solution instead of building a new solution to fit the problem.
The most important part of the book by the “Gang of Four,” as evidenced by the title, are the 23 software design patterns that are discussed within the book.
A software design pattern is a reusable structure or solution to a common problem or usage within software development. Throughout the course of developing many different programs in the object-oriented paradigm, developers may find that they are reusing similar ideas or code structures within applications with completely different uses, which leads to the idea of formalizing these ideas into reusable structures.
However, it is important to understand that a design pattern is not a finished piece of code that can simply be dropped into a program. Instead, a design pattern is a framework, structure, or template that can be used to achieve a particular result or solve a given problem. It is still up to the developer to actually determine if the design pattern can be used, and how to make it work.
The power of these design patterns lies in their common structure and the familiarity that other developers have with the pattern. For example, when building a program that requires a single global instance of class, there are many ways to do it. One way is the singleton pattern, which we’ll explore later in this chapter. If we choose to use that pattern, we can then tell other developers “this uses the singleton pattern” and, hopefully, they’ll be able to understand how it works as long as they are familiar with the pattern. If they aren’t, then the usefulness of the pattern is greatly reduced. So, it is very helpful for developers to be familiar with commonly-used design patterns, while constantly being on the lookout for new and interesting patterns to learn about and add to their ever growing list of patterns that can be used.
A great analogy is poetry. If we write a simple poem containing 5 lines, where the first, second, and fifth all end in a rhyming word and have the same number of syllables, and the third and fourth also rhyme and have fewer syllables, it could be very difficult to explain that structure to another writer. However, if we just say “I’ve written a limerick” to another writer, that writer might instantly understand what we mean, just based on their own familiarity with the format. However, if the writer is not familiar with a limerick, then referencing that pattern might not be helpful at all.
In Design Patterns, the “Gang of Four” introduced 23 patterns, which were grouped into three categories:
In addition, many modern references also include a fourth category: Concurrency Patterns, which are specifically related to building programs that run on multiple threads, multiple processes, or even across multiple systems within a supercomputer. We won’t deal with those patterns in this course since they are greatly outside the scope of what we’re going to cover.
Instead, we’re going to primarily focus on three creational patterns: the builder pattern, the factory method pattern, and the singleton pattern. Each one of these is commonly used in many object-oriented programs today, and we’ll be able to make use of each of them in our ongoing course project.
We’ll also look at a few of the structural and behavioral patterns: the iterator pattern, the template method pattern, and the adapter pattern.
The first pattern we’ll look at is the builder pattern. The builder pattern is used to simplify building complex objects, where the class that needs the object shouldn’t have to include all of the code and details for how to construct that object. By decoupling the code for constructing the complex object from the classes that use it, it becomes much simpler to change the representations or usages of the complex object without changing the classes that use it, provided they all adhere to the same general API.
The UML diagram above gives one possible structure for the builder pattern. It includes a Builder
interface that other objects can reference, and Builder1
is a class that implements that interface. There could be multiple builders, one for each type of object. The Builder1
class contains all of the code needed to properly construct the ComplexObject
class, consisting of ProductA1
and ProductB1
. If a different ComplexObject
must be created, we can create another class Builder2
that also implements the Builder
interface. To the Director
class, both Builder1
and Builder2
implement the same interface, so they can be treated as the same type of object.
A great example of this would be creating a deck of cards for various card games. There are actually many different types of card decks, depending on the game that is being played:
As we can see, even though each individual card is similar, constructing a deck for each of these games might be quite the complex process.
Instead, we can use the builder pattern. Let’s look a how this could work.
First, we’ll assume that we have a very simple Card
class, consisting of three attributes:
SuitOrColor
- the suit or color of the card. We’ll use a special color for cards that aren’t associate with a group of other cardsNumberOrName
- the number or name of the cardRank
- the sorting rank of the card (lowest = 1).public class Card{
String suitOrColor;
String numberOrName;
int rank;
public Card(String suit, String number, int rank) {
this.suitOrColor = suit;
this.numberOrName = number;
this.rank = rank;
}
}
class Card:
def __init__(self, suit: str, number: str, rank: int) -> None:
self._suit_or_color: str = suit
self._number_or_name: str = number
self._rank: int = rank
The Deck
class will only consist of an aggregation, or list, of the cards contained in the deck. So, our builder class will return an instance of the Deck
object, which contains all of the cards in the deck.
The Deck
class could also include generic methods to shuffle, draw, discard, and deal cards. These would work with just about any of the games listed above, regardless of the details of the deck itself.
import java.util.LinkedList;
import java.util.List;
public class Deck{
List<Card> deck;
public Deck() {
deck = new LinkedList<>();
}
void shuffle();
Card draw();
void discard(Card card);
List<List<Card>> deal(int hands, int size);
}
from typing import List
class Deck:
def __init__(self) -> None:
self._deck: List[Card] = list()
def shuffle(self) -> None:
def draw(self) -> Card:
def discard(self, card: Card) -> None:
def deal(self, hands: int, size: int) -> List[List[Card]]:
Our DeckBuilder
interface will be very simple, consisting of a single method: buildDeck()
. The type of the class that implements the DeckBuilder
interface will determine which type of deck is created. If the decks created by the builder have additional options, we can add additional methods to our DeckBuilder
interface to handle those situations.
Finally, we can create our builder classes themselves. These classes will handle actually building the different decks required for each game. First, let’s build a standard 52 card deck.
public class Standard52Builder implements DeckBuilder {
String[] suits = {"Spades", "Hearts", "Diamonds", "Clubs"};
public Deck buildDeck() {
Deck deck = new Deck();
for (String suit : suits) {
for (int i = 2; i <= 14; i++) {
if (i == 11) {
deck.add(new Card(suit, "Jack", i));
} else if (i == 12) {
deck.add(new Card(suit, "Queen", i));
} else if (i == 13) {
deck.add(new Card(suit, "King", i));
}else if (i == 14) {
deck.add(new Card(suit, "Ace", i));
} else {
deck.add(new Card(suit, "" + i, i));
}
}
}
return deck;
}
}
from typing import List
class Standard52Builder(DeckBuilder):
suits: List[str] = ["Spades", "Hearts", "Diamonds", "Clubs"]
def build_deck(self):
deck: Deck = Deck()
for suit in suits:
for i in range(2, 15):
if i == 11:
deck.append(Card(suit, "Jack", i))
elif i == 12:
deck.append(Card(suit, "Queen", i))
elif i == 13:
deck.append(Card(suit, "King", i))
elif i == 14:
deck.append(Card(suit, "Ace", i))
else:
deck.append(Card(suit, str(i), i))
return deck
As we can see, the heavy lifting of actually building the deck is offloaded to the builder class. We can easily use this same framework to create additional Builder
classes for the other types of decks listed above.
Finally, once we’ve created all of the builders that we’ll need, we can use them directly in our code anywhere we need them:
public class CardGame{
public static void main(String[] args) {
DeckBuilder builder = new Standard52Builder();
Deck cards = builder.buildDeck();
// game code goes here
}
}
from typing import List
class CardGame:
@staticmethod
def main(args: List[str]) -> None:
builder: DeckBuilder = Standard52Builder()
cards: Deck = builder.build_deck()
# game code goes here
From here, if we want to use any other decks of cards, all we have to do is switch out the single line for the type of builder we instantiate, and we’ll be good to go! This is the powerful aspect of the builder pattern - we can move all of the complex code for creating objects to a builder class, and then any class that uses it can quickly and easily construct the objects it needs in order to function.
On the next page, we’ll see how we can expand this pattern by including the factory pattern to help simplify things even further.
The next pattern we’ll explore is the factory method pattern. The factory method pattern is used to allow us to construct an object of a desired type without actually having to specify that type explicitly in our code. Instead, we just provide the factory with an input specifying the type of object we need, and it will return an instance of that type. By making use of the factory method pattern, classes that require access to these object don’t need to be updated any time an underlying object type is modified. Instead, they can simply reference the parent or interface data types, and the factory handles creating and returning objects of the correct type whenever needed.
As we can see in the UML diagram for this pattern, it looks very similar to the builder pattern we saw previously. There is a Creator
interface, which defines the interface that each factory uses. Then, the concrete Creator1
class is actually used to create the class required.
Let’s continue our deck of cards example from the previous page to include the factory method pattern.
To simplify this process, we’ll create a quick enumeration of the possible decks available in our system. This makes it easy to expand later and include more decks of cards.
public enum DeckType {
STANDARD52("Standard 52"),
STANDARD52ONEJOKER("Standard 52 with One Joker"),
STANDARD52TWOJOKER("Standard 52 with Two Jokers"),
PINOCHLE("Pinochle"),
OLDMAID("Old Maid"),
UNO("Uno"),
ROOK("Rook");
}
from enum import Enum
class DeckType(str, Enum):
STANDARD52 == "Standard 52"
STANDARD52ONEJOKER == "Standard 52 with One Joker"
STANDARD52TWOJOKER == "Standard 52 with Two Jokers"
PINOCHLE == "Pinochle"
OLDMAID == "Old Maid"
UNO == "Uno"
ROOK == "Rook"
Next, we’ll define a simple factory class, which is able to build each type of card deck. We’ll leave out the parent interface for now, since this project will only ever have a single factory object available.
import java.lang.IllegalArgumentException;
public class DeckFactory{
public Deck getDeck(DeckType deck) {
if(deck == DeckType.STANDARD52){
return new Standard52Builder().buildDeck();
}else if(deck == DeckType.STANDARD52ONEJOKER){
return new Standard52OneJokerBuilder().buildDeck();
}else if(deck == DeckType.STANDARD52TWOJOKER){
return new Standard52TwoJokerBuilder().buildDeck();
}else if(deck == DeckType.PINOCHLE){
return new PinochleBuilder().buildDeck();
}else if(deck == DeckType.OLDMAID){
return new OldMaidBuilder().buildDeck();
}else if(deck == DeckType.UNO){
return new UnoBuilder().buildDeck();
}else if(deck == DeckType.ROOK){
return new RookBuilder().buildDeck();
}else {
throw new IllegalArgumentException("Unsupported DeckType");
}
}
}
class DeckFactory:
def get_deck(self, deck: DeckType) -> Deck:
if deck == DeckType.STANDARD52:
return Standard52Builder().buildDeck()
elif deck == DeckType.STANDARD52ONEJOKER:
return Standard52OneJokerBuilder().buildDeck()
elif deck == DeckType.STANDARD52TWOJOKER:
return Standard52TwoJokerBuilder().buildDeck()
elif deck == DeckType.PINOCHLE:
return Standard52Builder().buildDeck()
elif deck == DeckType.OLDMAID:
return OldMaidBuilder().buildDeck()
elif deck == DeckType.UNO:
return UnoBuilder().buildDeck()
elif deck == DeckType.ROOK:
return RookBuilder().buildDeck()
else:
raise ValueError("Unsupported DeckType");
Now that we’ve created our factory class, we can update our main method to use it instead. In this case, we’ll get the type of deck to be used directly from the user as input:
public class CardGame{
public static void main(String[] args) {
// ask user for input and store in `deckType`
String deckType = "Standard 52";
Deck cards = DeckFactory().getDeck((DeckType.valueOf(deckType)));
// game code goes here
}
}
from typing import List
class CardGame:
@staticmethod
def main(args: List[str]) -> None:
# ask user for input and store in `deck_type`
deck_type: str = "Standard 52"
cards: Deck = DeckFactory().get_deck(DeckType(deck_type))
# game code goes here
This code is actually doing quite a bit in only two lines, so let’s go through it step by step. First, we’re assuming that we are getting user input to determine which deck should be used. This could be done via a GUI, the terminal, or some other means. We’re storing that input in a string, just to demonstrate the power of the factory method pattern. As long as the string matches one of the available deck types in the DeckType
enum, it will work. Of course, this may be difficult to do, so our input code might need to verify that the user inputs a valid option.
However, if we have a valid option, we can convert it to the correct enum value, and then pass that as an argument to the getDeck()
method of our DeckFactory
class. The factory will look at the parameter, construct the correct deck using the appropriate builder class, and then return it back to our application. Pretty handy!
One of the most common places the factory method pattern appears is in the construction of database connections. In theory, we’d like any of our applications to be able to use different types of databases, so many database connection libraries use the factory method pattern to create a database connection. Here’s what that might look like - this code will not actually work, but is representative of what it looks like in practice:
public class DbTest{
public static void main(String[] args) {
// connect to Postgres
DbConnection conn = DbFactory.get("postgres");
conn.connect("username", "password", "database");
// connect to MySql
DbConnection conn2 = DbFactory.get("mysql");
conn2.connect("username", "password", "database");
// connect to Microsoft SQL Server
DbConnection conn3 = DbFactory.get("mssql");
conn3.connect("username", "password", "database");
}
}
class DbTest:
@staticmethod
def main(args: List[str]) -> None:
# connect to Postgres
conn: DbConnection = DbFactory.get("postgres")
conn.connect("username", "password", "database")
# connect to MySql
conn2: DbConnection = DbFactory.get("mysql")
conn2.connect("username", "password", "database")
# connect to Microsoft SQL Server
conn3: DbConnection = DbFactory.get("mssql")
conn3.connect("username", "password", "database")
In each of these examples, we can get the database connection object we need to interface with each type of database by simply providing a string that specifies which type of database we plan to connect to. This makes it quick and easy to switch database types on the fly, and as a developer we don’t have to know any of the underlying details for actually connecting to and interfacing with the database. Overall, this is a great use of the factory method pattern in practice today.
Finally, let’s look at one other common creational pattern: the singleton pattern. The singleton pattern is a simple pattern that allows a program to enforce the limitation that there is only a single instance of a class in use within the entire program. So, when another class needs an instance of this class, instead of instantiating a new one, it will simply get a reference to the single existing object. This allows the entire program to share a single instance of an object, and that instance can be used to coordinate actions across the entire system.
The UML diagram for the singleton pattern is super simple. The class implementing the singleton pattern simply defines a private constructor, making sure that no other class can construct it. Instead, it stores a static reference to a single instance of itself, and includes a get
method to access that single instance.
Let’s look at how this could work in our ongoing example.
Let’s update our DeckFactory
class to use the singleton pattern.
public class DeckFactory{
// private static single reference
private static DeckFactory instance = null;
// private constructor
private DeckFactory(){
// do nothing
}
public static DeckFactory getInstance() {
// only instantiate if it is called at least once
if DeckFactory.instance == null{
DeckFactory.instance = new DeckFactory();
}
return DeckFactory.instance;
}
public Deck getDeck(DeckType deck) {
// existing code omitted
}
}
There are actually two different ways to implement this in Python. The first is closer to the implementation seen in Java above and in C++ in the original book.
class DeckFactory:
# private static single reference
_instance: DeckFactory = None
# constructor that cannot be called
def __init__(self) -> None:
raise RuntimeError("Cannot Construct New Object!")
@classmethod
def get_instance(cls) -> DeckFactory:
# only instantiate if it is called at least once
if cls._instance is None:
# call `__new__()` directly to bypass __init__
cls._instance = cls.__new__(cls)
return cls._instance
def get_deck(self, deck: DeckType) -> Deck:
# existing code omitted
A more Pythonic way would be to simply make use of the __new__()
method itself to create the singleton and return it anytime the __init__()
method is called. In Python, when any class is constructed normally, as in DeckFactory()
, the __new__()
method is called on the class first to create the instance, and then the __init__()
method is called to set the instance’s attributes and perform any other initialization. So, by ensuring that the __new__()
method consistently returns the same instance, we can guarantee that only a single instance exists.
class DeckFactory:
# private static single reference
_instance: DeckFactory = None
# new method to construct the instance
def __new__(cls) -> DeckFactory:
if cls._instance is None:
# call `__new__()` on the parent `Object` class
cls._instance = super().__new__(cls)
return cls._instance
def get_deck(self, deck: DeckType) -> Deck:
# existing code omitted
In this way, any calls to construct a DeckInstance()
in the traditional way would just return the same object. Very Pythonic!
See Singleton on the excellent Python Design Patterns website for a discussion of these two implementations.
Now we can update our main method code to use our singleton DeckFactory
instance instead of creating one when it is needed:
public class CardGame{
public static void main(String[] args) {
// ask user for input and store in `deckType`
String deckType = "Standard 52";
Deck cards = DeckFactory.getInstance().getDeck((DeckType.valueOf(deckType)));
// game code goes here
}
}
from typing import List
class CardGame:
@staticmethod
def main(args: List[str]) -> None:
# ask user for input and store in `deck_type`
deck_type: str = "Standard 52"
cards: Deck = DeckFactory.get_instance().get_deck(DeckType(deck_type))
# Python method described above means the code doesn't change!
# cards: Deck = DeckFactory().get_deck(DeckType(deck_type))
# game code goes here
Why would we want to do this? Let’s assume we’re writing software for a multiplayer game server. In that case, we may not want to instantiate a new copy of the DeckFactory
class for each player. Instead, using the singleton pattern, we can guarantee that only one instance of the class exists in the entire system.
Likewise, if we need a system to assign unique numbers to objects, such as orders in a restaurant, we can create a singleton class that assigns those numbers across all of the point of sale systems in the entire store. This might be useful in your ongoing class project.
Let’s review three other commonly used software design patterns. These are either patterns that we’ve seen before, or ones that we might end up using soon in our code.
The first pattern is the iterator pattern. The iterator pattern is a behavioral pattern that is used to traverse through a collection of objects stored in a container. We explored this pattern in several of the data structures introduced in earlier data structures courses such as CC 310 and CC 315, as well as CIS 300.
In it’s simplest form, the iterator pattern simply includes a hasNext()
and next()
method, though many implementations may also include a way to reset the iterator back to the beginning of the collection.
Classes that use the iterator can use the hasNext()
method to determine if there are additional elements in the collection, and then the next()
method is used to actually access that element.
In the examples below, we’ll rely on the built-in collection classes in Java and Python to provide their own iterators, but if we must write our own collection class that doesn’t use the built-in ones, we can easily develop our own iterators using documentation found online.
In Java, classes can implement the Iterable interface, which requires them to return an Iterator object. In doing so, these objects can then be used in the Java enhanced for or for each loop.
import java.lang.Iterable;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;
public class Deck implements Iterable<Card> {
List<Card> deck;
public Deck() {
deck = new LinkedList<>();
}
@Override
public Iterator<Card> iterator() {
return deck.iterator();
}
public int size() {
return this.deck.size();
}
}
Here, we are making use of the fact that the Java collections classes, such as LinkedList
, already implement the Iterable
interface, so we can just return the iterator from the collection contained in our object. Even though it is not explicitly required by the Iterable
interface, it is also a good idea to implement a size()
method to return the size of our collection.
With this code in place, we can iterate through the deck just like any other collection:
public class CardGame{
public static void main(String[] args) {
String deckType = "Standard 52";
Deck cards = DeckFactory.getInstance().getDeck((DeckType.valueOf(deckType)));
for(Card card : cards) {
// do something with each card
}
}
}
In Python, we can simply provide implementation for the __iter__()
method in a class to return an iterator object, and that iterator object should implement the __next__()
method to get the next item, as well as the __iter__()
method, which just returns the iterator itself. Python does not define an equivalent to the has_next()
method; instead, the __next__()
method should raise a StopIteration
exception when the end of the collection is reached.
For the purposes of type checking, we can use the Iterator
type and the Iterable
parent class (which works similar to an interface).
from typing import Iterable, Iterator
class Deck(Iterable[Card]):
def __init__(self) -> None:
self._deck: List[Card] = list()
def __iter__(self) -> Iterator[Card]:
return iter(self._deck)
def __len__(self) -> int:
return len(self._deck)
def __getitem__(self, position: int) -> Card:
return self._deck[position]
Here, we are making use of the fact that the built-in Python data types, such as list and dictionary, already implement the __iter__()
method, so we can just return the iterator obtained by calling iter()
on the collection.
In addition, we’ve also implemented the __len__()
and __getitem__()
magic methods, or “dunder methods”, that help our class act more like a container. With these, we can use len(cards)
to get the number of cards in a Deck
instance, and likewise we can access each individual card using array notation, as in cards[0]
. There are several other magic methods we may wish to implement, which are described in the link above.
With this code in place, we can iterate through the deck just like any other collection:
from typing import List
class CardGame:
@staticmethod
def main(args: List[str]) -> None:
deck_type: str = "Standard 52"
cards: Deck = DeckFactory.get_instance().get_deck(DeckType(deck_type))
for card in cards:
# do something with each card
See Iterator on Python Design Patterns for more details.
Another pattern is the adapter pattern. The adapter pattern is a structural pattern that is used to make an existing interface fit within a different interface. Just like we might use an adapter when traveling abroad to allow our appliances to plug in to different electrical outlets around the world, the adapter pattern lets us use one interface in place of another, similar interface.
In the UML diagram above, we see two different approaches to using the adapter pattern. First, we see the object adapter, which simply stores an instance of the object to be adapted, and then translates the incoming method calls (or messages) to match the appropriate ones available in the object it is adapting.
The other approach is the class adapter, which typically works by subclassing or inheriting the class to be adapted, if possible. Then, our code can call the operations on the adapter class, which can then call the appropriate methods in its parent class as needed.
Let’s look at a quick example to see how we can use the adapter pattern in our code.
Let’s assume we have a Pet
class that is used to record information about our pets. However, the original class was written to use metric units, and we’d like our program to use the United States customary units system instead. In that case, we could use the adapter pattern to adapt this class for our use.
To make it simple, we’ll assume that our Pet
class includes attributes weight
, measured in kilograms, as well as age
, measured in years. Each of those attributes includes getters and setters in the Pet
class.
First, let’s look at the adapter pattern using the object adapter approach. In this case, our adapter will store an instance of the Pet
class as an object, and then use its methods to access methods within the encapsulated object.
import java.lang.Math;
public class PetAdapter{
private Pet pet;
public PetAdapter() {
this.pet = new Pet();
}
public int getWeight() {
// convert kilograms to pounds
return Math.round(this.pet.getWeight() * 2.20462);
}
public void setWeight(int pounds) {
// convert pounds to kilograms
this.pet.setWeight(Math.round(pounds * 0.453592));
}
public int getAge() {
// no conversion needed
return this.pet.getAge();
}
public void setAge(int years) {
// no conversion needed
this.pet.setAge(years);
}
}
class PetAdapter:
def __init__(self) -> None:
self.__pet = Pet()
@property
def weight(self) -> int:
# convert kilograms to pounds
return round(self.__pet.weight * 2.20462)
@weight.setter
def weight(self, pounds: int) -> None:
# convert pounds to kilograms
self.__pet.weight = round(pounds * 0.453592)
@property
def age(self) -> int:
# no conversion needed
return self.__pet.age
@age.setter
def age(self, years: int) -> None:
# no conversion needed
self.__pet.age = years
As we can see, we can easily write methods in our PetAdapter
class that perform the conversions needed and call the appropriate methods in the Pet
object contained in the class.
The other approach we can use is the class adapter approach. Here, we’ll inherit from the Pet
class itself, and implement any updated methods.
import java.lang.Math;
public class PetAdapter extends Pet{
public PetAdapter() {
super();
}
@Override
public int getWeight() {
// convert kilograms to pounds
return Math.round(super.getWeight() * 2.20462);
}
@Override
public void setWeight(int pounds) {
// convert pounds to kilograms
super.setWeight(Math.round(pounds * 0.453592));
}
// Age methods are already inherited and don't need adapted
}
class PetAdapter(Pet):
def __init__(self) -> None:
super().__init__()
@property
def weight(self) -> int:
# convert kilograms to pounds
return round(super().weight * 2.20462)
@weight.setter
def weight(self, pounds: int) -> None:
# convert pounds to kilograms
super().weight = round(pounds * 0.453592)
# Age methods are already inherited and don't need adapted
In this approach, we override the methods that need adapted in our subclass, but leave the rest of them alone. So, since the age
getter and setter can be inherited from the parent Pet
class, we don’t need to include them in our adapter class.
The last pattern we’ll review in this course is the template method pattern. The template method pattern is a pattern that is used to define the outline or skeleton of a method in an abstract parent class, while leaving the actual details of the implementation to the subclasses that inherit from the parent class. In this way, the parent class can enforce a particular structure or ordering of the steps performed in the method, making sure that any subclass will behave similarly.
In this way, we avoid the problem of the subclass having to include large portions of the code from a method in the parent class when it only needs to change one aspect of the method. If that method is structured as a template method, then the subclass can just override the smaller portion that it needs to change.
In the UML diagram above, we see that the parent class contains a method called templateMethod()
, which will in part call primitive1()
and primitive2()
as part of its code. In the subclass, the code for the two primative
methods can be overridden, changing how parts of the templateMethod()
works, but not the fact that primitive1()
will be called before primitive2()
within the templateMethod()
.
Let’s look at a quick example. For this, we’ll go back to our prior example involving decks of cards. The process of preparing for most games is the same, and follows these three steps:
Then, each individual game can modify that process a bit based on the rules of the game. So, let’s see what that might look like in code.
import java.util.List;
public abstract class CardGame {
protected int players;
protected Deck deck;
protected List<List<Card>> hands;
public CardGame(int players) {
this.players = players;
}
public void prepareGame() {
this.getDeck();
this.prepareDeck();
this.dealCards(this.players);
}
protected abstract void getDeck();
protected abstract void prepareDeck();
protected abstract void dealCards(int players);
}
from abc import ABC, abstractmethod
from typing import List, Optional
class CardGame(ABC):
def __init__(self, players: int) -> None:
self._players = players
self._deck: Optional[Deck] = None
self._hands: List[List[Card]] = list()
def prepare_game(self) -> None:
self._get_deck()
self._prepare_deck()
self._deal_cards(self._players)
@abstractmethod
def _get_deck(self) -> None:
raise NotImplementedError
@abstractmethod
def _prepare_deck(self) -> None:
raise NotImplementedError
@abstractmethod
def _deal_cards(self, players: int) -> None:
raise NotImplementedError
First, we create the abstract CardGame
class that includes the template method prepareGame()
. It calls three abstract methods, getDeck()
, prepareDeck()
, and dealCards()
, which need to be overridden by the subclasses.
Next, let’s explore what this subclass might look like for the game Hearts. That game consists of 4 players, uses a standard 52 card deck, and deals 13 cards to each player.
import java.util.LinkedList;
public class Hearts extends CardGame {
public Hearts() {
// hearts always has 4 players.
super(4);
}
@Override
public void getDeck() {
this.deck = DeckFactory.getInstance().getDeck(DeckType.valueOf("Standard 52"));
}
@Override
public void prepareDeck() {
this.deck.suffle();
}
@Override
public void dealCards {
this.hands = new LinkedList<>();
for (int i = 0; i < this.players; i++) {
LinkedList<Card> hand = new LinkedList<>();
for (int i = 0; i < 13; i++) {
hand.add(this.deck.draw());
}
this.hands.add(hand);
}
}
}
from typing import List
class Hearts(CardGame):
def __init__(self):
# hearts always has 4 players
super().__init__(4)
def _get_deck(self) -> None:
self._deck: Deck = DeckFactory.get_instance().get_deck(DeckType("Standard 52"))
def _prepare_deck(self) -> None:
self._deck.shuffle()
def _deal_cards(self, players: int) -> None:
self._hands: List[List[Card]] = list()
for i in range(0, players):
hand: List[Card] = list()
for i in range(0, 13):
hand.append(self._deck.draw())
self._hands.append(hand)
Here, we can see that we implemented the getDeck()
method to get a standard 52 card deck. Then, in the prepareDeck()
method, we shuffle the deck, and finally in the dealCards()
method we populate the hands
attribute with 4 lists of 13 cards each. So, whenever anyone uses this Hearts
subclass and calls the prepareGame()
method that is defined in the parent CardGame
class, it will properly prepare the game for a standard game of Hearts.
To adapt this to another type of game, we can simply create a new subclass of CardGame
and update the implementations of the getDeck()
, prepareDeck()
and dealCards()
methods to match.
In this chapter, we explored several software design patterns introduced by the “Gang of Four” in their 1994 book: Design Patterns: Elements of Reusable Object-Oriented Software. Design patterns are a great way to build our code using reusable, standard structures that can solve a particular problem or perform a particular task. By using structures that are familiar to other developers, it makes it easier for them to understand our code.
Software design patterns are loosely grouped into three categories:
We studied 6 different design patterns. The first three were creational patterns:
We also studied a structural pattern:
Finally, we looked at two behavioral patterns:
In the future, we can use these patterns in our code to solve specific problems and hopefully make our program’s structure easier for other developers to understand.
Check your understanding of the new content introduced in this chapter below - this quiz is not graded and you can retake it as many times as you want.
Quizdown quiz omitted from print view.