Mark As Completed Discussion

What are software design patterns?

Software design patterns are general, reusable solutions to commonly occurring problems in software development.

Design patterns are not libraries or frameworks that you can plug in and use right away. Rather, they are established suggested ways of thinking to use when you're faced with a problem that many developers have solved before.

In essence, design patterns help avoid reinventing the wheel. Along with anti-patterns and architectural patterns, they form an established vocabulary that software engineers often use to discuss common problems and solutions.

It's important to have an understanding of various patterns and know where to use them. However, it's equally important to avoid implementing them all over the place, which may reduce readability and maintainability without providing a noticeable benefit.

Where do design patterns come from?

The idea of design patterns -- best practices to solve similar problems in different contexts -- was originally developed by architects. Adopted and reimagined in software engineering in the late 1980s, design patterns gained popularity when they were collected and codified in Design Patterns: Elements of Reusable Object-Oriented Software, a 1994 book by Gamma, Helm, Johnson and Vlissides. The four authors have since been commonly referred to as the Gang Of Four (GoF).

Design patterns have gained significant traction in Java and C# communities, but they can be used in all object oriented languages.

How many patterns are there?

The original Gang of Four book included 23 patterns divided into 3 categories:

  • Creational patterns used to create objects without instantiating them directly. This category includes Abstract Factory, Builder, Factory Method, Prototype, and Singleton.
  • Structural patterns that define ways of class and object composition. Adapter, Bridge, Composite, Decorator, Facade, Flyweight, and Proxy are all structural patterns.
  • Behavioral patterns that suggest various ways for objects to communicate and/or evolve. Behavioral patterns include Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, and Visitor.

This is the original list, but it's not set in stone. If you look around, you will find variations where some patterns are omitted and others are introduced.

Selected design patterns

Some software engineers argue that the most common design pattern is The Big Ball of Mud. However, we'll look at a selection of patterns that are arguably more useful. We'll use C# as the language of choice for sample code.

Singleton

One of the most used design patterns, Singleton makes sure that only one instance of a particular class is created, and provides a global point of access to that instance.

Singleton is most useful when you need a single object to coordinate other objects.

A singleton is usually implemented as a class that contains:

  • a private constructor (so that only members of the same class can access it),
  • a private static member,
  • a static public method that returns a reference to the private static member.

Here's a class that represents the president as a singleton. Note the private static property holding the instance, the private constructor that can only be called from inside the class, and the GetInstance() method that creates a single instance representing the president if it's null, or returns the previously created instance if it's not null:

1class President {
2  constructor(firstName, lastName, party) {
3    this.FirstName = firstName;
4    this.LastName = lastName;
5    this.Party = party;
6  }
7
8  static GetInstance() {
9    if (!this.instance) this.instance = new President("Donald", "Trump", "Republican");
10    return this.instance;
11  }
12
13  get LastName() { return this.LastName; }
14  get FirstName() { return this.FirstName; }
15  get Party() { return this.Party; }
16
17  toString() { return this.FirstName + " " + this.LastName + ", " + this.Party; }
18}

At call site, we'd call the GetInstance() method to get the one and only instance. Note how trying to use the constructor directly doesn't compile because the constructor is private.

1// In JavaScript
2let constructed = new President("Bernie", "Sanders", "Democratic"); // Won't compile, constructor is private
3let one = President.getInstance();
4let two = President.getInstance();
5
6console.log(one);
7console.log(two);
8console.log(one === two);  // Will return true as both variables point to the same instance

There are multiple legitimate scenarios for using singletons, including caching, logging, and hardware interface access.

Factory

The Factory pattern helps create an instance of an object without exposing the instantiation logic to the code that needs the object. It's best used with objects that are not trivial to create.

Normally, when you create an object, you just call a class constructor directly:

1// In JavaScript
2const myObject = new MyObject();

Most of the time, there's nothing wrong with creating objects via constructors. However, sometimes you may want to make the parts of your code that need objects independent from the implementation of these objects. That's when Factory can help.

When using Factory, you don't call a constructor. Instead, you call a method in a static factory class and provide information about the type of object that you need. The factory returns an instance of the object you're looking for.

Here's a sample where you have a class representing a tennis ball and a static class for a factory that delivers a tennis ball:

1class ITennisBall {
2    getDiameter() {};
3    getMake() {};
4}
5
6class GrassCourtTennisBall extends ITennisBall {
7    constructor() {
8        super();
9        this.color = "Optic yellow";
10        this.diameter = 67;
11        this.bounce = 145;
12        this.make = "Slazenger Wimbledon";
13    }
14
15    getDiameter() { return this.diameter; }
16    getMake() { return this.make; }
17}
18
19const TennisBallFactory = {
20    deliverBall: function() { return new GrassCourtTennisBall(); }
21}

At call site, instead of calling a constructor for TennisBall, you use TennisBallFactory to deliver the ball:

1let ball = TennisBallFactory.deliverBall();
2console.log(`This is a ${ball.getMake()} ball with a diameter of ${ball.getDiameter()} mm`);

This is a simplified version of the Factory Method pattern. Another related pattern is Abstract Factory.

Builder

The Builder pattern helps... well, build objects! It can be useful in two situations:

  • You have an object that could go in a variety of flavors.
  • An object uses a non-trivial, multi-step creation process.

When an object could go in a variety of flavors, a common solution is to use a constructor with many parameters that define whether each of these flavors should be used:

1// In JavaScript
2constructor(size, salami, mozzarella, ruccola) { }

An alternative solution is to use the Builder pattern where a separate builder class manages adding flavors to an object and provides methods that make adding flavors explicit.

In the code below, we have two classes: Pizza represents a pizza but isn't intended to be used to construct pizzas, and PizzaBuilder is there for the sole purpose of constructing a custom pizza with a set of possible flavors:

1class Pizza {
2    constructor(builder) {
3        this.size = builder.size;
4        this.salami = builder.salami;
5        this.mozzarella = builder.mozzarella;
6        this.ruccola = builder.ruccola;
7    }
8
9    getDescription() {
10        return `This is a ${this.size} cm pizza.`;
11    }
12}
13
14class PizzaBuilder {
15    constructor(size) {
16        this.size = size;
17        this.salami = false;
18        this.mozzarella = false;
19        this.ruccola = false;
20    }
21
22    addSalami() {
23        this.salami = true;
24        return this;
25    }
26
27    addMozzarella() {
28        this.mozzarella = true;
29        return this;
30    }
31
32    addRuccola() {
33        this.ruccola = true;
34        return this;
35    }
36
37    build() {
38        return new Pizza(this);
39    }
40}

At call site, we create a pizza via PizzaBuilder that makes the customization process very explicit:

1// In JavaScript
2const pizza = new PizzaBuilder(28)
3    .addMozzarella()
4    .addSalami()
5    .addRuccola()
6    .build();
7
8console.log(pizza.getDescription());

Note that many programming languages allow using named parameters on call sites, which can be a valid alternative to using the Builder pattern in some cases:

1var pizza = new Pizza(28, true, false, true);

However, if a builder goes beyond adding parameters to an object and applies custom logic on each step of object construction, then named parameters aren't a viable replacement.

Adapter

If two classes cannot work together because their interfaces are different, applying the Adapter pattern helps establish compatibility between them.

For example, if one class has a function that returns XML and another class has a function that takes JSON as input, and you want to take advantage of functionality in both of these classes, you may want to implement the Adapter pattern to enable interaction between them.

It's very similar to how adapters work with consumer electronics:

  • To read a memory card from your laptop, you use an adapter.
  • To plug your European laptop into a socket in the US, you use an adapter.

Speaking of US sockets, here's an example where we use the USSocketAdapter class to make the USSocket class compatible with the ISocket interface:

1class ISocket {
2    Charge() {}
3}
4
5class GermanSocket extends ISocket {
6    Charge() {}
7}
8
9class BritishSocket extends ISocket {
10    Charge() {}
11}
12
13class Laptop {
14    Charge(socket) {}
15}
16
17class USSocket {
18    Magic() {}
19}
20
21class USSocketAdapter extends ISocket {
22    constructor(socket) {
23        super();
24        this.AmericanSocket = socket;
25    }
26
27    Charge() {
28        this.AmericanSocket.Magic();
29    }
30}

At call site, we can now wrap a US socket into the socket adapter and charge our laptop:

1// In JavaScript
2const americanSocket = new USSocket();
3const americanSocketAdapter = new USSocketAdapter(americanSocket);
4
5const laptop = new Laptop();
6laptop.charge(americanSocketAdapter);

A related pattern is Facade that provides a simplified interface to a complex subsystem. However, while Facade creates a new, simpler interface, Adapter serves to design to an existing interface.

Strategy

The Strategy pattern helps define a family of related algorithms, and make decisions to use one or another based on user input when the application runs.

One benefit of using Strategy is that it helps avoid complex chains of conditions for switching between algorithms, replacing them with delegation to different Strategy objects.

For example, in an order processing system, prices will probably be calculated in different ways depending on customer type, purchase quantity, and other factors. In this situation, using Strategy can help select the right pricing algorithm at runtime.

Another example would be a tennis game where defence and offence are two distinct strategies:

1class IMatchStrategy 
2{
3    hitTheBall() {}
4}
5
6class DefensiveMatchStrategy extends IMatchStrategy
7{
8    hitTheBall()
9    {
10        return { 
11            Type : "Slice",
12            Power: 0.7
13        }
14    }
15}
16
17class AggressiveMatchStrategy extends IMatchStrategy
18{
19    hitTheBall()
20    {
21        return {
22            Type : "Flat",
23            Power: 1.2
24        }
25    }
26}

At call site, we can start a match aggressively, but then switch to defense depending on external factors:

1let match = new Match(new AggressiveMatchStrategy());
2
3let shot1 = match.hitTheBall();
4
5console.log(match.matchStrategy);
6console.log(shot1.toString());
7
8if (Math.floor(Math.random() * (10 - 1) + 1) > 5)
9    match.matchStrategy = new DefensiveMatchStrategy();
10
11let shot2 = match.hitTheBall();
12
13console.log(match.matchStrategy);
14console.log(shot2.toString());

A popular technique related to the Strategy pattern is Dependency Injection.

Observer

The Observer pattern helps objects learn about changes to the state of other objects. It does so by establishing a one-to-many dependency between objects. It's used extensively for event management.

You use the pattern when you have a critical object ("the subject"), a group of objects that depend on it ("observers"), and you need to make sure that observers are notified of any changes to the subject -- typically by calling one of their methods. Following this pattern helps circulate updates between objects without unnecessarily coupling them, and makes adding new observers easy.

Have you ever visited a website of a product that was yet to launch? Have you subscribed to receive an e-mail as soon as the product becomes publicly available? If you have, you acted as an observer, and the product acted as the subject.

Here's an example where a carsharing pool is the subject and implements the IObservable interface from C#'s Base Class Library, and a driver who uses the carsharing service is an observer and implements the IObserver interface:

1class SharedCar
2{
3    constructor(model) { this.model = model; }
4    getModel() { return this.model; }
5}
6
7class CarsharingCustomer
8{
9    constructor(name) { this.name = name; }
10    update(sharedCar) {
11        console.log(`Hey ${this.name}, a new car is available: ${sharedCar.getModel()}`);
12    }
13}
14
15class CarsharingPool
16{
17    constructor() {
18        this.availableCars = [];
19        this.observers = [];
20    }
21
22    addObserver(observer) {
23        this.observers.push(observer);
24    }
25
26    addCar(sharedCar)
27    {
28        this.availableCars.push(sharedCar);
29        this.observers.forEach(observer => observer.update(sharedCar));
30    }
31}

(The implementation above also uses a class called Unsubscriber that is omitted for brevity.)

At call site, the driver is subscribed to updates from the carsharing service, and gets a notification whenever a new shared car becomes available:

1const driver = new CarsharingCustomer("Louise Sawyer");
2
3const availableCars = new CarsharingPool();
4availableCars.subscribe(driver);
5availableCars.addCar(new SharedCar("Ford Thunderbird"));

Visitor

The Visitor pattern lets you add operations to objects without having to modify these objects -- in other words, it's a way of separating an algorithm from an object structure on which it operates. This pattern is used when:

  • you have a complex structure that contains objects of different types,
  • similar operations need to be performed on all these objects.

Visitor defines behavior in abstract classes or interfaces. This helps avoid the use of conditional blocks and type checks, which tend to become clunky and hard to maintain.

Below is a code sample where a sports fan (visitor) wants to visit the Olympic Games to watch competitions in various sports (visitees). All visitees have the Accept method that takes a parameter representing a visitor:

1class SummerOlympicSport {
2    accept(visitor) { }
3}
4
5class DivingCompetition extends SummerOlympicSport {
6    jump() { }
7    accept(visitor) { visitor.watchDiving(this); }
8}
9
10class CyclingTrackRace extends SummerOlympicSport {
11    ride() { }
12    accept(visitor) { visitor.watchCycling(this); }
13}
14
15class BadmintonMatch extends SummerOlympicSport {
16    hit() { }
17    accept(visitor) { visitor.watchBadminton(this); }
18}

Here's how the visitor is implemented:

1// In JavaScript
2class ICompetitionVisitor {
3    watchDiving(divingCompetition) {}
4    watchCycling(cyclingTrackRace) {}
5    watchBadminton(badmintonMatch) {}
6}
7
8class CompetitionVisitor extends ICompetitionVisitor {
9    watchBadminton(badmintonMatch) {
10        badmintonMatch.hit();
11    }
12
13    watchCycling(cyclingTrackRace) {
14        cyclingTrackRace.ride();
15    }
16
17    watchDiving(divingCompetition) {
18        divingCompetition.jump();
19    }
20}

At call site, we can now initialize a visitor (competitionVisitor), and walk them through the list of visitees (competitionsToVisit) in a loop where we call the Accept() method that hides dissimilarity of the visitees:

1let competitionsToVisit = [
2    new DivingCompetition(), 
3    new BadmintonMatch(), 
4    new CyclingTrackRace()
5];
6
7let competitionVisitor = new CompetitionVisitor();
8
9for (let competition of competitionsToVisit) {
10    competition.accept(competitionVisitor);
11}

There's a related pattern called Iterator. However, Iterator is used on collections that contain objects of the same type. Visitor has wider applicability and can be used on hierarchical and/or composite data structures.

Related concepts

Where there are patterns, there should be anti-patterns. Indeed, a mere year after the original Gang of Four book, another influential book was released, AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis. The book covered bad practices in software development, software architecture, and project management. Some of the most well-known anti-patterns covered in the book include Spaghetti Code, Cut-and-Paste Programming, Vendor Lock-In, Architecture by Implication, Design by Committee, Reinvent the Wheel, Analysis Paralysis, and Death by Planning.

Interestingly, when design patterns get overused, many in the development community start to treat them as anti-patterns. The Singleton pattern is the primary example of such evolution.

In addition to design patterns, there are architectural patterns that are similar in spirit but have a broader scope and apply to the design of an application as a whole. Some of the most popular architectural patterns include Model-View-Controller (MVC), Model-View-ViewModel (MVVM), Enterprise Service Bus (ESB), and Peer-to-Peer (P2P).

Where to learn more?

One Pager Cheat Sheet

  • Design Patterns are general and reusable solutions that help developers avoid reinventing the wheel, and should be used judiciously to provide the most benefit.
  • Design patterns were originally developed by architects, and gained popularity when codified by the "Gang of Four" in 1994 as elements of reusable object-oriented software.
  • There are 23 patterns from the Gang of Four book, divided into 3 categories (Creational, Structural and Behavioral) with The Big Ball of Mud being one of the most commonly used design patterns.
  • The Singleton design pattern ensures that only one instance of a class is created, and provides a global point of access to that instance.
  • The Factory Method pattern makes it easy to create instances of objects without exposing the instantiation logic, which is especially useful for non-trivial objects.
  • The Builder pattern is a useful solution for when an object needs to be customized through a non-trivial, multi-step process and as an alternative to numerous parameters in the constructor.
  • The Adapter pattern helps to bridge the gap between incompatible objects by transforming one object's interface into an interface expected by the other object, similar to physical adapters used in hardware.
  • The Strategy pattern allows for a family of related algorithms to be defined, and for a decision of which one to use at runtime to be made based on user input, thereby avoiding complex chains of conditions, and making it possible to switch strategies at call site in order to adapt to different situations, while related techniques such as Dependency Injection offer added benefits.
  • The Observer pattern allows an object (the subject) to let others (the observers) know of any changes to its state by establishing a one-to-many dependency between them.
  • The Visitor pattern lets you add operations to objects without modifying them, separating an algorithm from the object structure on which it operates, and avoiding conditional blocks and type checks.
  • The concept of anti-patterns, which can evolve from overused design patterns, as well as architectural patterns such as MVC, MVVM, ESB and P2P, are related to patterns in software development.
  • With resources such as Awesome Software and Architectural Design Patterns, Design Patterns for Humans and Learning JavaScript Design Patterns by Addy Osmani, as well as OODesign.com and the GoF Design Patterns Reference, anyone can deepen their understanding of Design Patterns.