Overview of OOP patterns implementation in JavaScript
· 152
Wajdi Alkayal Wajdi Alkayal

Let's take a look at the Design Patterns from OOP described in 'Gang Of Four' and let's investigate how they are implemented in JavaScript.

What are Design Patterns?

First things first, you need to understand the true definition of Design Patterns. As a software developer, you can write code in “any way”. But, it would be the best practices, which make a big difference in how you maintain code. Code that is written with utmost finesse in mind, would last longer than the amateur ones. This means you don’t need to worry about scalability or maintenance when you choose the right coding style.

  • The role of design patterns is to help you build solutions that don’t make the overall problem complicated.
  • Patterns will help you build interactive objects, and designs that are heavily reusable.
  • Design Patterns are an integral concept in Object-Oriented Programming.

Gang Of Four are your key to design patterns. Currently, there are 23 patterns in Gang Of Four. These patterns are categorised into three unique groups: Creational, Structural and Behavioural.

Creational Design Patterns in JavaScript

  1. Abstract Factory
  2. Builder
  3. Factory
  4. Prototype
  5. Singleton

Abstract Factory

What is a factory? If you ask a kid, what would they describe a factory as? A Factory is nothing but a place, where we manufacture things. For instance, in a Toy Factory, you would see the production of toys. Just like this real-time definition, a Factory in JavaScript is a place where an Object creates other objects. Not all Toy Factories produce teddy bears, and transformers, right? There are separate toy factories, for different types of toys. The toy factory always works with a theme. Similarly, an Abstract Factory works with a theme. Objects that come from an Abstract Factory, will share a common theme.

JavaScript does not support class-based inheritance. So, the implementation of the Abstract Factory pattern is rather interesting.

Let’s understand Abstract Factory, in JavaScript, with an example.

  1. I have to build software, for a Toy Factory.
  2. I have a department, which produces toys themed on the Transformers movie. And, another department for Star Wars-themed toys.
  3. Both the departments have a few common characteristics and a unique theme.
<>
function StarWarsFactory(){ this.create = function(name){ return new StarWarsToy(name) }} function TransformersFactory(){ this.create = function(name){ return new TransformerToy(name) }} function StarWarsToy(name){ this.nameOfToy = name; this.displayName = function(){ console.log("My Name is "+this.nameOfToy); }} function TransformersToy(name){ this.nameOfToy = name; this.displayName = function(){ console.log("My Name is "+this.nameOfToy); }} function buildToys(){ var toys = []; var factory_star_wars = new StarWarsFactory(); var factory_transformers = new TransformersFactory(); toys.push(factory_star_wars.create("Darth Vader")); toys.push(factory_transformers.create("Megatron")); for(let toy of toys) console.log(toy.displayName()); }

Builder

The role of the builder is to build complex objects. The client will receive a final object, without having to worry about the real work done. Most of the time, the builder pattern is an encapsulation of the composite object. Mainly because the entire process is complex and repetitive.

Let’s see how to implement our Toy Factory, with the builder pattern.

  1. I have a toy factory
  2. I have a department for building Star Wars Toys.
  3. Within the Star Wars department, I want to build many Darth Vader toys.
  4. Building Darth Vader toys is a two-step process. I need to build the toy, and then mention the color.
<>
function StarWarsFactory(){ this.build = function(builder){ builder.step1(); builder.step2(); return builder.getToy(); }} function DarthVader_Builder(){ this.darth = null; this.step1 = function () { this.darth = new DarthVader(); } this.step2 = function () { this.darth.addColor(); } this.getToy = function () { return this.darth; } } function DarthVader () { this.color = ''; this.addColor = function(){ this.color = 'black'; } this.say = function () { console.log("I am Darth, and my color is "+this.color); } } function build(){ let star_wars = new StarWarsFactory(); let darthVader = new DarthVader_Builder(); let darthVader_Toy = star_wars.build(darthVader); darthVader_Toy.say(); }

Factory

The role of a factory is to produce similar objects, which have the same characteristics. This helps with easy management, maintenance, and manipulation of objects. For example, in our Toy Factory, every toy will have certain pieces of information. It will have the date of purchase, the origin, and the category.

<>
var ToyFactory = function(){ this.createToy = function(type) { var toy; if(type == "starwars") { toy = new StarWars(); } toy.origin = "Origin"; toy.dop = "2/22/2022"; toy.category="fantasy"; } }

Prototype

Many times, we would need to create a new object, which has default values from another parent object. This prevents the creation of objects with non-initialized values. The Prototype pattern can be used to create such objects.

Prototype Patterns are also known as Properties Pattern.

  1. In our toy factory, we have the Star Wars Department, which produces many characters.
  2. Each Character will have a genre, expiry date, and status field. These fields will remain the same for all toys, from the Star Wars department.
<>
function Star_Wars_Prototype(parent){ this.parent = parent; this.duplicate = function () { let starWars = new StarWarsToy(); starWars.genre = parent.genre; starWars.expiry = parent.expiry; starWars.status = parent.status; return starWars; }; } function StarWarsToy(genre, expiry, status){ this.genre = genre; this.expiry = expiry; this.status = status; } function build () { var star_wars_toy = new StarWarsToy('fantasy', 'NA', 'Jan'); var new_star_wars_toy = new Star_Wars_Prototype(star_wars_toy); //When you are ready to create var darth = new_star_wars_toy.duplicate(); }

Singleton

Singleton Pattern corresponds to a “Single Instance”. A given object can have only a single instance. When systems have data, which needs to be coordinated from a single spot, you can use this pattern.

<>
var Singleton = (function () { var instance; function createInstance() { var object = new Object("I am the instance"); return object; } return { getInstance: function () { if (!instance) { instance = createInstance(); } return instance; } }; })(); function run() { var instance1 = Singleton.getInstance(); var instance2 = Singleton.getInstance(); console.log("Same instance? " + (instance1 === instance2)); }

Structural Design Patterns

  1. Adapter
  2. Bridge
  3. Composite
  4. Decorator
  5. Facade
  6. Flyweight
  7. Proxy

Adapter

The Adapter design pattern is used when an object’s properties or methods need to be translated from one to another. The pattern is extremely useful when components with mismatched interfaces have to work with one another. The Adapter Pattern is also known as the Wrapper Pattern.

Let’s understand this with our Toy Factory:

  1. The Toy Factory has a shipping department.
  2. We are planning to migrate from the old shipping department to a new one.
  3. However, we need to keep the old shipping methods intact, for the current stock.
<>
// old interface function Shipping() { this.request = function (zipStart, zipEnd, weight) { // ... return "$49.75"; } } // new interface function AdvancedShipping() { this.login = function (credentials) { /* ... */ }; this.setStart = function (start) { /* ... */ }; this.setDestination = function (destination) { /* ... */ }; this.calculate = function (weight) { return "$39.50"; }; } // adapter interface function ShippingAdapter(credentials) { var shipping = new AdvancedShipping(); shipping.login(credentials); return { request: function (zipStart, zipEnd, weight) { shipping.setStart(zipStart); shipping.setDestination(zipEnd); return shipping.calculate(weight); } }; } function run() { var shipping = new Shipping(); var credentials = { token: "StarWars-001" }; var adapter = new ShippingAdapter(credentials); // original shipping object and interface var cost = shipping.request("78701", "10010", "2 lbs"); console.log("Old cost: " + cost); // new shipping object with adapted interface cost = adapter.request("78701", "10010", "2 lbs"); console.log("New cost: " + cost); }

Bridge

The bridge is a famous High-Level Architectural Pattern. It helps by offering different levels of abstraction. As a result, Objects will be loosely coupled. Every Object, which becomes a component, will have its very own interface.

In our toy factory, where we produce Star Wars toys, there are two different varieties. One set of toys can be operated using a remote control. Another set of toys is operated using a battery and produces different sounds. The Bridge Pattern helps in building this high-level architecture.

<>
var Remote_Control = function (output) { this.output = output; this.left = function () { this.output.left(); } this.right = function () { this.output.right(); } }; var Battery_Operation= function (output) { this.output = output; this.move = function () { this.output.move(); } this.wheel = function () { this.output.zoom(); } }; var Remote_Controlled_Toy = function () { this.left = function () { console.log("Move Left"); } this.right = function () { console.log("Move Right"); } }; var Battery_Operated_Toy = function () { this.move = function () { console.log("Sound waves"); } this.wheel = function () { console.log("Sound volume up"); } }; function run() { var remote_control = new Remote_Control(); var battery_operation = new Battery_Operation(); var star_wars_type_1 = new Remote_Controlled_Toy(remote_control); var star_wars_type_2 = new Battery_Operated_Toy(battery_operation); star_wars_type_1.left(); star_wars_type_2.wheel(); }

Composite

As suggested by its name, a composite pattern creates objects that are primitive or a collection of objects. This helps in building deeply nested structures.

In our toy factory, the composite pattern helps in the following manner:

  1. We have two departments, one a manual section, and two, an automated one.
  2. In the manual section, we have a bunch of toys (leaves),
  3. In the automated section, we have another set of toys (leaves).
<>
var Node = function (name) { this.children = []; this.name = name; } Node.prototype = { add: function (child) { this.children.push(child); } } function run() { var tree = new Node("Star_Wars_Toys"); var manual = new Node("Manual") var automate = new Node("Automated"); var darth_vader = new Node("Darth Vader"); var luke_skywalker = new Node("Luke Skywalker"); var yoda = new Node("Yoda"); var chewbacca = new Node("Chewbacca"); tree.add(manual); tree.add(automate); manual.add(darth_vader); manual.add(luke_skywalker); automate.add(yoda); automate.add(chewbacca); }

Decorator

The Decorator pattern extends an object’s properties and methods, adding new behavior to it during run time. Multiple decorators can be used to add or override the actual functionalities of an object.

In our toy factory, we have a function to give the toys a name. And, we have an additional decorator to state the genre.

<>
var Toy = function (name) { this.name = name; this.display = function () { console.log("Toy: " + this.name); }; } var DecoratedToy = function (genre, branding) { this.toy = toy; this.name = user.name; // ensures interface stays the same this.genre = genre; this.branding = branding; this.display = function () { console.log("Decorated User: " + this.name + ", " + this.genre + ", " + this.branding); }; } function run() { var toy = new User("Toy"); var decorated = new DecoratedToy(toy, "fantasy", "Star Wars"); decorated.display(); }

Facade

The Facade design pattern offers a high-level interface of properties and methods. These properties and methods can be used by the subsystems.

Content image
Facade Design Pattern

Flyweight

This pattern is used when objects need to be shared between Objects. The Objects will be immutable. Why? Since the objects are shared by others, they must not be changed. The Flyweight pattern is present in the Javascript Engine. For instance, the Javascript Engine maintains a list of strings that are immutable and can be shared across applications.

In our Toy factory:

  1. Every toy has a genre
  2. Every toy has a country of manufacture
  3. Every toy has a year of manufacture. These properties can be kept in the Flyweight model.
<>
function Flyweight(genre, country, year) { this.genre = genre; this.country = country; this.year = year; }; var FlyWeightFactory = (function () { var flyweights = {}; return { get: function (genre, country, year) { if (!flyweights[genre + country]) { flyweights[genre + country] = new Flyweight(genre, country, year); } return flyweights[genre + country]; } } })(); function ToyCollection() { var toys = {}; return { add: function (genre, country, year, brandTag) { toys[brandTag] = new Toy(genre, country, year, brandTag); } }; } var Toy = function (genre, country, year, brandTag) { this.flyweight = FlyWeightFactory.get(genre, country, year, brandTag); this.brandTag = brandTag; } function build() { var toys = new ToyCollection(); toys.add("Fantasy", "USA", "2021", "StarWars_01"); toys.add("Fantasy", "USA", "2021", "Transformers_01"); }

Proxy

The Proxy Pattern offers a placeholder object, instead of the actual object. The placeholder object will have control over the access to the actual object value.

For example, our Toy Factory is situated in many places of the world. Every place produces a number of toys. With the Proxy pattern, it becomes simpler to understand how many toys are produced from each location.

<>
function GeoCoder() { this.getLatLng = function (address) { if (address === "Hong Kong") { return "52.3700° N, 4.8900° E"; } else if (address === "North America") { return "51.5171° N, 0.1062° W"; }. }; } function GeoProxy() { var coder = new GeoCoder(); var geoCollector = {}; return { getLatLng: function (location) { if (!geoCollector[location]) { geoCollector[location] = coder.getLatLng(location); } return geoCollector[location]; }}; }; function run() { var geo = new GeoProxy(); geo.getLatLng("Hong Kong"); geo.getLatLng("North America"); }

Behavioral Design Patterns

  1. Chain of Responsibility
  2. Command
  3. Interpreter
  4. Iterator
  5. Mediator
  6. Memento
  7. Observer
  8. State
  9. Strategy
  10. Template
  11. Visitor

Chain of Responsibility

Anyone using Javascript would have come across the concept of event-bubbling, at least once. This is where events propagate across nested controls. One of the controls can choose to handle the bubbling event. The Chain of Responsibility Pattern can be used to handle this behavior. To be precise, this pattern is widely used by JQuery.

Content image
Chain of Responsibility

Command

Many a time, Objects will have a common set of events to be processed. The Command objects help in building Objects, where actions to handle these events can be encapsulated. Most of the time, the Command Pattern is used to centralize functionalities. For instance, the undo operation in any application is an example of the Command Pattern. Whether you invoke it from the Menu Drop Down, or from the keyboard, the same functionality gets invoked.

Interpreter

Not all solutions are the same. In many applications, you may need to add extra lines of code to manipulate the input, or output to be shown to the user. Here, the output depends on the application.

In our toy factory example, all Star Wars toys need to be prefixed with the tagline: “May the Force be with You”. And, all Bob the Builder toys need to be prefixed with “Are you Ready!” This extra level of customization can be achieved using the Interpreter Pattern.

<>
var Prefix = function (brandTag) { this.brandTag = brandTag; } Prefix.prototype = { interpret: function () { if (this.brandTag == “Star Wars”) { return “May the Force be with You”; } else if (this.brandTag == “Bob the Builder”) { return “Are you Ready?; } } } function run() { var toys = []; toys.push(new Prefix(“Star Wars”)); toys.push(new Prefix(“Bob the Builder”)); for (var i = 0, len = toys.length; i < len; i++) { console.log(toys[i].interpret()); } }

Iterator

As suggested by its name, you can use the Iterator pattern to define how an object or collection of objects has to be effectively looped. In Javascript, you will come across a few common forms of looping: while, for, for-of, for-in, and do while. With the iterator pattern, you can design your own way of looping through objects that will be flexible for the application, created.

Mediator

Another design pattern, which works as suggested by its name would be the Mediator. This pattern provides a central point of control for a group of objects. It is heavily used to perform state management. When one of the objects, changes the state of its properties, it can be broadcasted easily to the other objects.

Here is a simple example, of how the Mediator design pattern helps:

<>
var Participant = function (name) { this.name = name; this.chatroom = null; } //define a Itprototype for participants with receive and send implementation var Talkie = function () { var participants = {}; return { register: function (participant) { participants[participant.name] = participant; participant.talkie = this; }, send: function (message, from, to) { if (to) { // single message to.receive(message, from); } else { // broadcast message for (key in participants) { if (participants[key] !== from) { participants[key].receive(message, from); } } } } }; }; function letsTalk() { var A = new Participant("A"); var B = new Participant("B"); var C = new Participant("C"); var D = new Participant("D"); var talkie = new Talkie(); talkie.register(A); talkie.register(B); talkie.register(C); talkie.register(D); A.send("I love you B."); B.send("No need to broadcast", A); C.send("Ha, I heard that!"); D.send("C, do you have something to say?", C); }

Memento

The Memento represents a repository, where the state of an object is stored. There could be scenarios in an application where the state of an object needs to be saved and restored. Most of the time, the serializing and de-serializing property of an object with JSON helps in implementing this design pattern.

<>
var Toy = function (name, country, year) { this.name = name; this.country = country; this.city = city; this.year = year; } Toy.prototype = { encryptLabel: function () { var memento = JSON.stringify(this); return memento; }, decryptLabel: function (memento) { var m = JSON.parse(memento); this.name = m.name; this.country = m.country; this.city = m.city; this.year = m.year; console.log(m); } } function print() { var darth = new Toy("Darth Vader", "USA", "2022"); darth.encryptLabel(); darth.decryptLabel(); }

Observer

The Observer Pattern is one of the most widely used patterns of all. It has a unique implementation across platforms like Angular, and React. In fact, it is a topic that needs to be discussed on its own. Nevertheless, this pattern brings forth a subscription model. Objects can subscribe to specific events. And, when the event happens, they will be notified. JavaScript is an event-driven programming language. Much of its concepts depend on the Observer Pattern.

State

Now, let’s begin with an example. The Traffic Light comes in three colors: red, amber, and green. The action to be performed differs from one color to another. This means there is color-specific logic. Any object within the color will have to abide by the rules of the color. Likewise, state patterns come with a specific collection of logic for every state. Objects within the state will need to follow this logic.

Strategy

The Strategy Design Pattern is used to encapsulate algorithms, which can be used to complete a specific task. Based on certain pre-conditions, the actual strategy (alias method) to be executed changes. This means the algorithms used within a strategy are highly interchangeable.

In our toy factory, we ship products using three different companies: FedEx, USPS, and UPS. The final cost of shipping depends on the company. Thus, we can use the Strategy design pattern to decide on the final cost.

<>
Distributor.prototype = { setDistributor: function (company) { this.company = company; }, computeFinalCost: function () { return this.company.compute(); } }; var UPS = function () { this.compute = function () { return "$45.95"; } }; var USPS = function () { this.compute = function () { return "$39.40"; } }; var Fedex = function () { this.compute = function () { return "$43.20"; } }; function run() { var ups = new UPS(); var usps = new USPS(); var fedex = new Fedex(); var distributor = new Distributor(); distributor.setDistributor(ups); console.log("UPS Strategy: " + distributor.computeFinalCost()); distributor.setDistributor(usps); console.log("USPS Strategy: " + distributor.computeFinalCost()); distributor.setDistributor(fedex); console.log("Fedex Strategy: " + distributor.computeFinalCost()); }

Template

The Template Pattern presents an outlook of steps. When an object is created against this pattern, all the steps in the template have to be completed. Of course, the steps can be customized to suit the object created. This design pattern is widely used in common libraries and frameworks.

Visitor

Finally, we have the Visitor Design Pattern. It is useful when a new operation has to be defined for a group of objects. But, the original structure of the Objects should remain intact. This pattern is useful when you have to extend a framework or a library. Unlike the other patterns discussed in this post, the Visitor Design Pattern is not widely used in Javascript. Why? JavaScript engine comes with more advanced and flexible ways of adding or removing properties from objects dynamically.

<>
var Employee = function (name, salary, vacation) { var self = this; this.accept = function (visitor) { visitor.visit(self); }; this.getSalary = function () { return salary; }; this.setSalary = function (salary) { salary = salary; }; }; var ExtraSalary = function () { this.visit = function (emp) { emp.setSalary(emp.getSalary() * 2); }; }; function run() { var john = new Employee("John", 10000, 10), var visitorSalary = new ExtraSalary(); john.accept(visitorSalary); } }

Conclusion

Some design patterns are often used by the Javascript engine. And, many of us don’t even know if these patterns are being used or not. The use of patterns will improve the performance, and maintainability of your code. This is why more, and more patterns should be used in your code. The journey of introducing patterns in your code does not have to be overnight. Instead, learn the patterns and gradually include them in your application.


Related Posts
Service Beyond Expectations
05 February
Dive into Innovation with Our Exclusive Packages!
04 February
Digital Alchemy: Transforming Strategies into Success with Our Marketing Wizards
04 February

WMK Tech Copyright © 2024. All rights reserved