Top 5 Most Used Patterns in OOP with TypeScript
We all use design patterns in our code. Sometimes, it's unnecessary, but it could give a nice and understandable structure to your architecture. Since TypeScript is getting more popular, I decided to show some of the popular patterns with its implementation.
Singleton
Overview
Singleton is most of the known patterns in the programming world. Basically, you use this pattern if you need to instantiate a restricted number of instances. You do this by making a private constructor and providing a static method, which returns an instance of the class.
Usage
This pattern is used in other patterns. Abstract factory and builder uses it in itself implementations. Sometimes, you use singletons with facades, because you want to provide only one instance of a Facade.
Code
class Singleton {
private static instance: Singleton | null;
private constructor() {}
static getInstance() {
if (!this.instance) {
this.instance = new Singleton();
}
return this.instance;
}
}
console.log(Singleton.getInstance() === Singleton.getInstance());
//true
Fluent Interface
Overview
Often used in testing libraries (e.g., Mocha, Cypress), a fluent interface makes code readable as written prose. It is implemented by using method chaining. Basically, every method returns this. (self) and the chaining ends when a chain method returns void. Also, other techniques used for a fluent interface - nested functions and object scoping.
Usage
As we’ve mentioned earlier, it is used to create more readable code. Also, you can easily compose objects with this pattern and create readable queries.
Code
class Book {
private title: string | undefined;
private author: string | undefined;
private rating: number | undefined;
private content: string | undefined;
setTitle(title: string) {
this.title = title;
return this;
}
setAuthor(author: string) {
this.author = author;
return this;
}
setRating(rating: number) {
this.rating = rating;
return this;
}
setContent(content: string) {
this.content = content;
return this;
}
getInfo() {
return `A ${this.title} book is written by ${this.author} with ${
this.rating
} out of 5 stars`;
}
}
console.log(
new Book()
.setTitle('Voyna i Mir')
.setAuthor('Lev Tolstoy')
.setRating(3)
.setContent('A very long and boring book... Once ago...')
.getInfo(),
);
Observer
Overview
This pattern suggests, that you have a subject and some observers. Every time you update your subject state, observers get notified about it. This pattern is very handy when you need to tie several objects to each other with abstraction and freedom of implementation. Also, this pattern is a key part of the familiar model-view-controller (MVC) architectural pattern. Strongly used in almost every GUI library.
Usage
You have a basic Subject class, which has 3 methods: attach, detach, notify and a list of observers, which had implemented the Observer interface. Observer - it’s an interface, which has only one method - update(). You add observers by attach(), remove them by detach(), and by notify() - calling method update() in each of them.
Code
async function sleep(msec: any) {
return new Promise(resolve => setTimeout(resolve, msec));
}
interface Observer {
update: Function;
}
class Observable {
constructor(protected observers: Observer[] = []) {}
attach(observer: Observer) {
this.observers.push(observer);
}
detach(observer: Observer) {
this.observers.splice(this.observers.indexOf(observer), 1);
}
notify(object = {}) {
this.observers.forEach(observer => {
observer.update(object);
});
}
}
class GPSDevice extends Observable {
constructor(private coordinates: any = { x: 0, y: 0, z: 0 }) {
super();
}
process() {
this.coordinates.x = Math.random() * 100;
this.coordinates.y = Math.random() * 100;
this.coordinates.z = Math.random() * 100;
this.notify(this.coordinates);
}
}
class Logger implements Observer {
update(object: any) {
console.log(`Got the next data ${object.x} ${object.y} ${object.z}`);
}
}
class TwoDimensionalLogger implements Observer {
update(object: any) {
console.log(`Got the next 2D data ${object.x} ${object.y}`);
}
}
const gps = new GPSDevice();
const logger = new Logger();
const twoDLogger = new TwoDimensionalLogger();
gps.attach(logger);
gps.attach(twoDLogger);
(async () => {
for (let tick = 0; tick < 100; tick++) {
await (async () => {
await sleep(1000);
if (tick === 3) gps.detach(logger)
gps.process();
})();
}
})();
Composite
Overview
The composite pattern is a pattern, which abstracts a group of objects into the one single instance of the same type. The composite pattern is easy to run, test and understand. It is used whenever is a need of representing a part-whole hierarchy as a tree structure and to treat part and whole object equally.
Usage
Basically, there are two ways of using this pattern: uniform and safe.
In a uniform way, you define the child-related operations in the Component interface - that means, that Leaf and Composite are treated same. The bad thing is that you lose type safety because child-related operations can be performed on Leaf objects.
In a safe way, child-related operations are defined only in the Composite class. Leaf and Composite objects are treated differently. Since child-related operations can’t be performed on Leaf, you gain type safety.
Code
interface Component {
operation: Function;
}
abstract class Leaf implements Component {
operation() {}
}
abstract class Composite implements Component {
protected childs: Component[] = [];
operation() {
this.childs.forEach(child => {
child.operation();
});
}
add(component: Component) {
this.childs.push(component);
}
remove(component: Component) {
this.childs.splice(this.childs.indexOf(component), 1);
}
getChild() {
return this.childs;
}
}
class Duck extends Composite {
constructor(childs: Component[]) {
super();
this.childs = childs;
}
}
class DuckVoice extends Leaf {
operation() {
console.log('Quack.');
}
}
class DuckFly extends Composite {
operation() {
console.log('It flies.');
super.operation();
}
add(component: Component) {
super.add(component);
return this;
}
}
class Wing extends Leaf {
operation() {
console.log('Flap-flap-flap');
}
}
const wings = [new Wing(), new Wing()];
const flyAbility = new DuckFly().add(wings[0]).add(wings[1]);
const voiceAbility = new DuckVoice();
const duck = new Duck([flyAbility, voiceAbility]);
duck.operation();
Abstract factory
Overview
Abstract factory is a specific pattern, which used to create an abstract object with an abstract factory. That basically means, that you can put every factory that implements the Abstract Factory and it would return an instance, that implements the Abstract Object interface.
Usage
You define two interfaces: an Abstract Factory’s one and a Subject’s one. Then, you implement whatever you want and expose the interface. A client doesn’t know what is inside, he just gets an object with implement methods of an interface.
Code
interface SoundFactory {
create: Function;
}
interface Sound {
enable: Function;
}
class FerrariSound implements Sound {
enable() {
console.log('Wrooom-wrooom-wrooooom!');
}
}
class BirdSound implements Sound {
enable() {
console.log('Flap-flap-flap');
}
}
class FerrariSoundFactory implements SoundFactory {
create() {
return new FerrariSound();
}
}
class BirdSoundFactory implements SoundFactory {
create() {
return new BirdSound();
}
}
(() => {
let factory: SoundFactory | null = null ;
const type = Math.random() > 0.5 ? 'ferrari' : 'bird';
switch (type) {
case 'ferrari':
factory = new FerrariSoundFactory();
break;
case 'bird':
factory = new BirdSoundFactory();
break;
}
if (factory) {
const soundMaker = factory.create();
soundMaker.enable();
}
})();
/*
ts-node abstract-factory.ts
Flap-flap-flap
ts-node abstract-factory.ts
Wrooom-wrooom-wrooooom!
ts-node abstract-factory.ts
Flap-flap-flap
ts-node abstract-factory.ts
Wrooom-wrooom-wrooooom!
*/
Summary
We have revealed some patterns implementations in this article. Thank you for your attention.