Optimize your code with Decorator, Façade, and Flyweight Patterns!
Design patterns are all about relationships, but in Creational Design Patterns, we do not find many relationships as the focus is on creating instances of objects. The only pattern dealing with this is the Factory Pattern, which creates other objects.
Structural Design Patterns simplify object relationships and composition, creating flexible and maintainable software systems that can adapt to changing requirements. This makes them essential for producing high-quality code.
In Structural Design Patterns, everything revolves around relationships. In each case, we have an object and check how it relates to something else. Dealing with relationships is accomplished in two ways:
- extending functionality,
- simplifying functionality.
Let’s give an example of two extremes. On one side, we have the Decorator Pattern, which extends functionality. On the other side, we have the Flyweight and Façade Patterns, which simplify functionality or at least mimic simplified functionality.
Decorator Pattern
The Decorator Pattern is used to add new functionality to an existing object without being obtrusive. The problem we’re trying to solve with the decorator pattern is not to mess with the constructor. We do not want to introduce bugs into thoroughly tested, working code.
We, for example, want to extend the properties of the constructor without changing the behaviour of the constructor itself. In the following example, we’re decorating the sportsCar
with a property called hp
and a method called calculateSpeed
:
var Car = function(name) {
this.name = name;
this.active = false;
}
Car.prototype.activate = function() {
console.log("Activating:", this.name);
this.active = true;
}
Car.prototype.save = function() {
console.log("Saving car:", this.name);
}
var car = new Car("BMW");
car.activate();
car.save();
var sportsCar = new Car("M Performance");
// decorate with property 'hp'
sportsCar.hp = 355;
// decorate with method 'calculateSpeed'
sportsCar.calculateSpeed = function() {
console.log("Max speed is 250 km/h!");
}
// calling 'activate' method attached to a construtor 'Car'
sportsCar.activate();
// decorating (overriding) with new 'save' method
sportsCar.save = function() {
this.calculateSpeed();
// bind the save execution to the 'this' scope
// calling original save from Car
Car.prototype.save.call(this);
}
// calling 'save' method attached to an object 'sportsCar'
sportsCar.save();
The previous approach is good, but if we want to create another instance, such as a sportsCar
, we have to start from scratch again. Therefore, we are going to apply something called sub-classing. The main idea is to create a class called Car
and decorate it with properties without changing the Car
class itself:
var Car = function(name) {
this.name = name;
this.active = false;
}
Car.prototype.activate = function() {
console.log("Activating:", this.name)
this.active = true;
}
Car.prototype.save = function() {
console.log("Saving car:", this.name);
}
var bmw = new Car("BMW");
bmw.activate();
bmw.save();
// sub-classing (wrapping 'Car')
var SportsCar = function(name, hp) {
// call super-class
Car.call(this, name);
this.hp = hp;
}
// wire up the prototype object
// do not assign it to 'Car.prototype', as they are both going to change together
// create new instance of 'Car.prototype'
SportsCar.prototype = Object.create(Car.prototype);
// add new method 'calculateSpeed' on prototype
SportsCar.prototype.calculateSpeed = function() {
console.log("Max speed is 250 km/h!");
}
// decorating 'save' method
SportsCar.prototype.save = function() {
this.calculateSpeed();
console.log("Tuning car...")
// bind the 'save' execution to the 'this' scope
// calling original 'save' from 'Car'
Car.prototype.save.call(this);
}
var m3 = new SportsCar("M3", 543);
var m5 = new SportsCar("M5", 600);
// calling 'activate' methods attached to a constructor 'Car'
m3.activate();
m5.activate();
// calling 'save' methods attached to a constructor 'SportsCar'
m3.save();
m5.save();
console.log({ m3, m5 });
We haven’t changed anything, as we have just wrapped the constructor Car
with additional properties and methods.
Façade Pattern
We use it to provide a simplified interface for a potentially complicated subsystem. It gives us a way to put something in front of it and makes interacting with the system very simple and easy. It serves to cover the messy logic behind something hard to understand.
What makes jQuery such a great example of this pattern is its ability to sit on top of the DOM, providing a simple and clean interface when you’re trying to interact with a web page. With jQuery, you can easily manipulate elements, traverse the DOM, and handle events with just a few lines of code.
In the following example, there is a bunch of functions in the CarService
method that acts as a service that helps us deal with the car object. As you can see, the code is messy, and it’s hard to see what’s going on. But that’s the point of this type of pattern:
In the following example, there are a bunch of functions in the CarService
to help us deal with the car object. As you can see, the code is messy and it’s hard to understand what’s going on. But that’s the point of this type of pattern:
var Car = function(data) {
this.name = data.name;
this.model = data.model;
this.series = data.series;
this.active = data.active;
this.user = data.user;
}
var CarService = function() {
return {
activate: function(car) {
car.active = true;
console.log("Activating car:", car.name);
},
setProductionYear: function(car) {
car.productionYear = "2020-10-18T00:00:00Z";
console.log(car.name + " is released in " + car.productionYear);
},
notifyActivation: function(car, user) {
console.log("Notifying " + user.first + " of the activation!");
},
save: function(car) {
console.log("Saving car:", car.name);
}
}
}();
var car = new Car({
name: "BMW",
model: "M850i xDrive Coupé",
series: "8",
active: false,
user: {
first: "John",
last: "Smith"
}
})
CarService.activate(car);
if (car.active) {
CarService.setProductionYear(car);
CarService.notifyActivation(car, car.user);
CarService.save(car);
}
console.log(car);
We’re going to create a wrapper (façade) on top of this service. This may be the same thing as what we did with the Decorator pattern, but it’s not. We’re not adding any functionality; we’re covering up and building a better interface for the same functionality:
var Car = function(data) {
this.name = data.name;
this.model = data.model;
this.series = data.series;
this.active = data.active;
this.user = data.user;
}
// acts as a Module Pattern
// not the Revealing Module Pattern
var CarService = function() {
return {
activate: function(car) {
car.active = true;
console.log("Activating car:", car.name);
},
setProductionYear: function(car) {
car.productionYear = "2020-10-18T00:00:00Z";
console.log(car.name + " is released in " + car.productionYear);
},
notifyActivation: function(car, user) {
console.log("Notifying " + user.first + " of the activation!");
},
save: function(car) {
console.log("Saving car:", car.name);
}
}
}();
// Façade
var CarServiceWrapper = function (){
var setProductionYearAndNotify = function(car){
CarService.activate(car);
if (car.active) {
CarService.setProductionYear(car);
CarService.notifyActivation(car, car.user);
CarService.save(car);
}
}
// Revealing Module Pattern
return {setProductionYearAndNotify};
// If this were a CommonJS, it will be exported as an executed function (singleton)
}();
var car = new Car({
name: "BMW",
model: "M850i xDrive Coupé",
series: "8",
active: false,
user: {
first: "John",
last: "Smith"
}
});
// Only one line of code in the main exectution context
CarServiceWrapper.setProductionYearAndNotify(car);
console.log(car);
The more we can simplify the code with the Façade Pattern, the more maintainable it will be.
Flyweight Pattern
The Flyweight Pattern is used to conserve memory by sharing portions of an object between objects. Instead of replicating properties and methods multiple times, the pattern allows us to share them across all objects’ instances.
It’s similar to using objects prototype
. Instead of creating functions many times for every object instance, we add them to the prototype
. The pattern provides us with a similar solution but in a slightly different way. The overall result is a smaller memory footprint.
In the browser, memory is not as important as it is on mobile devices. In that case, we have to keep memory usage as small as possible, and we can achieve that with this pattern.
One important thing to note is that this pattern is useful when there are many instances of the object. Do not implement it if there are only a small number of objects (less than 500), as you will not get awesome results regarding memory management:
// you have to run the code in NodeJS environment
var Car = function(data) {
this.name = data.name;
this.model = data.model;
this.series = data.series;
this.active = data.active;
this.user = data.user;
}
var CarCollection = function() {
var cars = {};
var count = 0;
var add = function(data) {
cars[data.name] = new Car(data);
count++;
}
var get = function(name) {
return cars[name];
}
var getCount = function() {
return count;
}
return {
add,
get,
getCount
}
}
var cars = new CarCollection();
var series = [1, 2, 3, 4, 5, 6, 7, 8];
var models = ["M3", "M5", "M Performance", "xDrive Coupé", "X7"];
var users = ["John", "Adam", "Eva", "Emma"];
var active = [true, false];
var initMemory = process.memoryUsage().heapUsed;
// one million iterations
for (var i = 0; i < 1000000; i++) {
cars.add({
name: "BMW" + i,
model: models[Math.floor((Math.random() * 5))],
series: series[Math.floor((Math.random() * 8))],
active: active[Math.floor((Math.random() * 2))],
user: users[Math.floor((Math.random() * 4))],
});
}
var afterMemory = process.memoryUsage().heapUsed;
// Used Memory 196.444432
console.log("Used Memory", (afterMemory - initMemory) / 1000000);
Let’s apply the pattern:
// you have to run the code in NodeJS environment
var Flyweight = function(data) {
this.model = data.model;
this.series = data.series;
this.active = data.active;
this.user = data.user;
}
var FlyweightFactory = function() {
// it's a subset of cars
// only unique property for each 'car' is a 'name'
// everything else is shared (random values)
var flyweights = {};
var get = function(model, series, active, user) {
if (!flyweights[model + series + active + user]) {
flyweights[model + series + active + user] = new Flyweight(model, series, active, user);
}
return flyweights[model + series + active + user];
}
var getCount = function() {
var count = 0;
for (var f in flyweights) {
count++;
}
return count;
}
return {
get,
getCount
};
}();
var Car = function(data) {
this.flyweight = FlyweightFactory.get(data.model, data.series, data.active, data.user);
this.name = data.name;
}
// use a setter without exposing the fact that the Flyweight Pattern is used
Car.prototype.getModel = function() {
this.flyweight.model;
}
var CarCollection = function() {
var cars = {};
var count = 0;
var add = function(data) {
cars[data.name] = new Car(data);
count++;
}
var get = function(name) {
return cars[name];
}
var getCount = function() {
return count;
}
return {
add,
get,
getCount
}
}
var cars = new CarCollection();
var series = [1, 2, 3, 4, 5, 6, 7, 8];
var models = ["M3", "M5", "M Performance", "xDrive Coupé", "X7"];
var users = ["John", "Adam", "Eva", "Emma"];
var active = [true, false];
var initMemory = process.memoryUsage().heapUsed;
// one million iterations
for (var i = 0; i < 1000000; i++) {
cars.add({
name: "BMW" + i,
model: models[Math.floor((Math.random() * 5))],
series: series[Math.floor((Math.random() * 8))],
active: active[Math.floor((Math.random() * 2))],
user: users[Math.floor((Math.random() * 4))],
});
}
var afterMemory = process.memoryUsage().heapUsed;
// Used Memory 171.417472
console.log("Used Memory", (afterMemory - initMemory) / 1000000);
// Cars: 1000000
console.log("Cars:", cars.getCount());
// Flyweights: 320
console.log("Flyweights:", FlyweightFactory.getCount());
We have 1000 car
objects, but only 320 are flyweights
, which means that their common data is shared. The combination of model
, series
, active
, and the user
was shared only 320 times, allowing us to cut down on the amount of memory used (~25 MB is saved).
Conclusion
In conclusion, we have explored the fascinating world of structural design patterns and their ability to alter the composition of objects. We have looked at a selection of these patterns, including:
- Decorator Pattern (add additional functionality, without changing the object itself),
- Façade Pattern (hide complicated API behind a clean wrapper),
- Flyweight Pattern (sharing a piece of an object to reduce memory footprint).
Structural design patterns can boost project efficiency and flexibility while reducing memory usage and complexity.