knowledge-kitchen

Object-Orientation - Design Patterns in Software Development

Common problems and their solutions.

  1. Overview
  2. History
  3. Criticisms
  4. Creational
  5. Structural
  6. Behavioral
  7. Conclusions

Overview

Concept

Design Patterns are good quality re-usable solutions to common problems.

In other words, Design Patterns represent a set of best practices for particular contexts, often dealing with how objects should be created, composed, and how they should be designed to intercommunicate to solve common problems.

What they offer

Design Patterns describe a particular design problem.

They do not prescribe a solution. Rather, they indicate a set of values to guide the designer toward a solution to the problem that is best for their particular situation.

Nature in Flux

There is no complete set of design patterns.

History

Philosophy

The way is long if one follows precepts, but short and helpful, if one follows patterns.

Seneca the Younger spoke of the value of abstract patterns versus their concrete implementations circa 65 AD in his letter to his friend, Lucilius, now referred to as “On Sharing Knowledge”:

Seneca, part of double-herm in Antikensammlung Berlin

Architecture

A Pattern Language, by Christopher Alexander, Sara Ishikawa, Murray Silverstein

"A Pattern Language", the book

Architecture (continued)

Agricultural Valley design pattern, from A Pattern Language

Architecture (continued again)

Small Work Groups design pattern, from A Pattern Language

Software

Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides

An equally-seminal book published in 1994

Software (continued)

The Gang of Four outlined 23 object-oriented design patterns:

Criticisms

Concept

Most people agree that programming design patterns are useful and that following such patterns in known situations leads to better code that is more robust and easier to maintain by teams.

However, design patterns have their critics…

Language Flaws

A common criticism of Design Patterns is that their necessity indicates flaws or ommissions in the underlying programming language they are designed for.

In fact, some of the design patterns identified in the Gang of Four’s book about the Java language have been resolved by features that are native to other programming languages.

Overly-complicated

Another criticism holds that Design Patterns can lead to overly complex and verbose code, when a simpler unpatterned solution might be simpler and easier to maintain.

Creational

Overview

The following creational design patterns, from the Gang of Four book, describe ways to create objects in various circumstances.

Prototype

A fully initialized instance to be copied or cloned.

Useful when the setup of an object is time/resource consuming, and you do not want to repeat it more than once, if possible.

Prototype (continued)

Python example:

import copy

class Dog:
  def __init__(self, name, breed, age):
    self.name = name
    self.breed = breed
    self.age = age

  def bark(self):
    print(f"{self.name}, the {self.age} year old {self.breed}, says 'Woof!'")

  def clone(self):
    return copy.deepcopy(self)

dog1 = Dog("Fido", "German Shepherd", 6)
dog2 = dog1.clone()

dog2.bark() # "Fido, the 6 year old German Shepherd, says 'Woof!'"

Builder

Separates object construction from its representation.

The builder pattern provides a simple interface to create an object that actually has a complex structure.

Useful when there’s a complex object structure that is complicated to build.

Builder (continued)

Python example:

class Dog:
  def __init__(self):
    self.name = None
    self.breed = None
    self.age = None

  def __str__(self):
    return f"{self.age} year old {self.breed} named {self.name}"

class DogBuilder:
  def __init__(self):
    self.dog = Dog()

  def set_name(self, name):
    self.dog.name = name
    return self

  def set_breed(self, breed):
    self.dog.breed = breed
    return self

  def set_age(self, age):
    self.dog.age = age
    return self

  def build(self):
    return self.dog

builder = DogBuilder()
dog = builder.set_name("Fido").set_breed("German Shephderd").set_age(6).build()
print(dog) # '8 year old German Shepherd named Fido'

Singleton

A class of which only a single instance is allowed to exist.

For example you have one object that represents the entire running application or service… there is only one instance at any time.

Singleton (continued)

Python example of a Dog singleton where only 1 dog ever exists, named Fido, a 6 year old German Shepherd.

class Dog:
  _instance = None # will store the only instance of this class

  def __new__(cls):
    if cls._instance is None:
      cls._instance = super().__new__(cls)
      cls._instance.name = "Fido"
      cls._instance.breed = "German Shepherd"
      cls._instance.age = 6
    return cls._instance

  def bark(self):
    print(f"{self.name}, the {self.age} year old {self.breed}, says 'Woof!'")

dog1 = Dog()
dog2 = Dog() # dog2 is the same object as dog1

Factory

Separates the instantiation of objects from their use. Makes it streamlined to create objects and can allow hiding of objects that there is no reason to know about.

Python example:

class Dog:
  def __init__(self, name, breed, age):
    self.name = name
    self.breed = breed
    self.age = age

  def bark(self):
    print(f"{self.name}, the {self.age} year old {self.breed}, says 'Woof!'")

class DogFactory:
  def create_dog(self, name, breed, age):
    return Dog(name, breed, age)

factory = DogFactory()
dog = factory.create_dog("Fido", "German Shepherd", 6)
dog.bark() # "Fido, the 6 year old German Shepherd, says 'Woof!'"

Structural

Proxy

An object representing another object. It’s an imposter! Hides the complexity of the object it represents.

Example: credit card is a proxy for a bank account.

Programming world example: Mongoose is a framework that streamlines interacting with a MongoDB database from a Node.js application. The Node.js application issues simplified commands to Mongoose, which in turn validates those commands, modifies them as necessary, and relays them to the MongoDB instance. This is a form of proxying.

const mongoose = require('mongoose')
mongoose.connect('mongodb://localhost/test:27017') // connect to database

const Dog = mongoose.model('Dog', { name: String, breed: String, age: Number }) // design a data model
const fido = new Dog({ name: 'Fido', breed: 'German Shepherd', age: 6 }) // instantiate an object from that model
fido.save().then(() => console.log('Woof!')) // save that object to database

Facade

A single class that represents the entire subsystem.

Useful for web applications. Reducing network calls. Reduces coupling (between web layer and back-end). Helps rollback all steps if one fails.

Example: Event manger. Imagine organizing an event - it requires management of the decorations, food categering, invitations, music group, etc. The facade is such a manager that takes care of doing all the things necessary.

Example: online book order. The user might click a single button that on the back-end involves checking whether the book is in stock, reserving the book, making payment, updating the stock, generating an invoice, etc. The facade pattern allows a separation between all these complicated steps and where they are called. So a single call can trigger all of them.

Facade (continued)

Python example:

class WebServer:
  def __init__(self):
    self.inventory = InventorySystem()
    self.reservations = ReservationsSystem()
    self.payment = PaymentSystem()

  def order_book(self, request):
    if self.inventory.available(request) and self.reservations.reserve(request):
      try:
        self.payment.charge(request)
        self.inventory.reduce(request)
        return True
      except PaymentError:
        self.reservations.cancel(request)
        return False

Decorator

Adds responsibility to objects dynamically. Makes it easy to add behavior at runtime.

Example: imagine a pizza shop with 10 types of pizza. The pizza could be thought of as one code class. Then imagine that this shop decides to allow 3 types of toppings. This would require 3 new code classes. Then imagine this shop decides to allow 3 different sizes of these toppings: small, medium, and large. These would require more classes that probably inherit from other topping classes. When creating the pizza class, you wouldn’t possibly imagine that it needs to support all these different classes.

The decorator pattern would allow the toppings to be dynamic. Rather than put the toppings on the pizza, the decorator pattern would have the toppings be created, and the pizza added to them.

Behavioral

Chain of responsibility

A way of passing a request between a chain of objects.

A message passes through a series of objects until one of them is able to handle it, at which point is doesn’t get passed any further.

Iterator

Sequentially access the elements in a collection.

Useful when the internal representation of a collection of objects isn’t important, and you simply want to be able iterate through the objects in the collection.

Observer

A way of notifying a change to an object to a number of other objects. Objects can be registered to receive notifications.

Situation: When one of the objects changes, it needs to let several of the other objects know.

Conclusions

This has been a brief, but inspirational introduction to design patterns.