Notes on Common Design Patterns
What are desgn patterns and why should we use them. Common tried and tested solutions (found in nature repeating) to recurring architectural problems in software. Blueprint as opposed to boilerplate
Described by
- Intent: describes problem and solution
- Motivation: further explanation ?
- Stucture: often visual diagram of how the parts are related
- Code Example: specific example in language or framework
Pros
- Common language among software developers
Cons
- Hinders creative new solutions (an over-reliance on a familiar or favourite tool)
- Dogmatic
- Needed in languages that lack certain levels of abstraction
Three main categories of patterns: Creational, Structural, and Behavioural
Singleton
- Creational pattern
- Shared state of an application, single class instance
- Global access point to instance with added protection from being overwritten
- private constructor with static creation method
class CurrentUser {
private static instance: CurrentUser;
private constructor() {
if (instance) {
throw new Error("You can only create one instance!");
}
instance = this;
}
public static getInstance(): CurrentUser {
if (!CurrentUser.instance) {
CurrentUser.instance = new CurrentUser();
}
return CurrentUser.instance;
}
}
const user1 = CurrentUser.getInstance()
const user2 = CurrentUser.getInstance()
console.log(user1 === user2); // true
Pros
- strict control over global varibales
- global access point
- initialised only when first requested
Cons
- Violates Single Responsibility Principle (A module should be responsible to one, and only one, actor. –Robert C. Martin)
- No subclass
- No abstract or interface classes
- High coupling across the application
- Difficult to unit test
- can mask bad design
Common Uses
- Database object
- User authentification
Factory
Creational pattern Creates objects in a superclass but allows subclasses to alter. This pattern uses factory methods to create new objects rather than relying on the
new
keyword. Note: thenew
keyword can still be used to create the original object. Objects returned from a factory are often refferred to as products.Base class must be sufficiently abstract
Common Uses
- multiple smaller objects sharing the same properties (ex: new types of Cards)
- avoids multiple conditional statements each time new type is added
Pros
- can be a good way to deal with future unknowns
- extension of framework internal components (ex:
<svelte:component>
) - saves system resources by reusing objects
- avoids tight coupling between product and its creator
- Adheres to Single Responsibility Principle
- Adheres to Open/Closed Principle.
Cons
- Base class must be sufficiently abstract or inherent problems can occur in subclasses
- Need storage to keep track of created objects
- Products need to follow the same interface
- code complexity and readablilty
abstract class CardCreator {
public abstract factoryMethod(): Product;
public someOperation(): string {
const product = this.factoryMethod();
return `CardCreator: card created with ${product.operation()}`;
}
}
class PodcastCreator extends CardCreator {
public factoryMethod(): Product {
return new PodcastCreator1();
}
}
interface Card {
operation(): string;
}
class PodcastCard implements Card {
public operation(): string {
return '{PodcastCard}';
}
}
function createCard(creator: CardCreator) {
// ...
console.log(creator.someOperation());
// CardCreator: card created with {PodcastCard}
// ...
}
Prototype
- Creational pattern A clone, relies on class inheritance.
const cycle = {
sound: [ 'ding-ding']
}
const tricycle = Object.create(cycle, { wheels: { value: 3 }});
console.log(tricycle) // { wheels: 3 }
Object.getPrototypeOf(tricycle); // sound: ['ding-ding']
Builder
- Creational pattern Allows for piece by piece construction of complex objects. Extracts object construction from it’s own class into separate builder objects. Not all steps need be called. A director class can be added to define step order in case reusable routines are required.
- The object is intanciated and then methods for building it are chained
class Pizza {
this.crust = null;
this.sauce = null;
this.cheese: string | [] = '';
this.toppings: [] = [];
addCrust(type = 'classic') {
this.crust = type;
return this;
}
addSauce(type: 'tomato' | 'cream' | 'pesto' = 'tomato') {
this.sauce = type;
return this;
}
addCheese(type = 'mozzarella') {
this.cheese = type;
return this;
}
build() {
return this;
}
}
Pros
- avoids use of multiple subclasses or large class constructors
Cons
Mediatior
- Behavioural Pattern
example: form validation on clicking submit button
Submit button notifies of click and Mediator takes care of validation of form elements
- allows extraction of relationship between classes into a separtae class
- individial components don’t need to have knowledge of each other
Pros
- Adheres to Single Responsibility Principle
- Adheres to Open/Closed Principle.
- Improves component reusability
Cons
- Danger of becoming a God Object
Observer
- Behavioural Pattern
interface User {
id: string;
email: string;
createId: (email: string) => string;
}
class AlertNotification {
users: User[];
constrctor() {
this.users = [];
}
subscribe(email) {
const user = new User(email)
this.users.push()
}
unSubscribe(id) {
}
nofifyAll(event) {
this.users.forEach((user) => notifyUser(event));
}
}
In this example a single method (notifyAll) only needs to be called once to notify all the users; examples: Svelte stores
Iterator
- Behavioural Pattern an object returning a value and a next method (from MDN)
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false };
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true };
},
};
return rangeIterator;
}
const it = makeRangeIterator(1, 10, 2);
let result = it.next();
while (!result.done) {
console.log(result.value); // 1 3 5 7 9
result = it.next();
}
// [5 numbers returned, that took interval in between: 0 to 10]
console.log("Iterated over sequence of size:", result.value);
Iterables can be user defined or built in examples: String, Array, TypedArray, Map, Set
User defined:
const myIterable = {
*[Symbol.iterator]() {
yield 1;
yield 2;
yield 3;
},
};
Adapter
- Structural pattern Used to adapt a object or class to an incompatible interface to allow for usage. example convert JSON data to XML. The adapter can act as a translator or like a physical plug adapter.
Pros
- Adheres to Single Responsibility Principle
- Adheres to Open/Closed Principle.
Cons
- introduces overhead
- complexity