Is it Standard Practice to Use the Decorator Pattern in JavaScript?

Is it Standard Practice to Use the Decorator Pattern in JavaScript?

Designs and Javascript have forever been exciting topics. While configuration designs rose out of the consistency of generally executed arrangements, they were, as a rule, impacted by various languages in which they get implemented. Not many designs fit cozily inside JavaScript, principally inferable from its intrinsic difference of legacy conduct compared with C, Java, etc.

The decorator design is one of only a handful of exceptional plan designs that appears as though it was imagined while keeping languages like JavaScript. One of the earliest well-known JavaScript libraries, jQuery, utilizes decorators to increase jQuery with module functionalities. Various NodeJS libraries design the EventEmitter to assemble valuable kinds of the pub-sub design.

Configuration designs are "associated" with specific parts of the issue you're attempting to address. Mostly, innocent power fit and the absence of information on anti-patterns make decorator designs or some other plan design hard to work with.

**So, when do we use decorators? **

This blog will discuss the use and working of javascript decorator patterns.

Each time you draw on the blank area of a formerly painted material, you risk changing the importance of the first work of art. Accordingly, you are good to go if you guarantee that your enrichments don't damage the main structure construction and that you don't add things that go totally against the main framework/system/module.

Decorators include wrapping a piece of code such as a function or class with another decorator function. The point is to expand the functionality of the covered code without changing it. This design is now famous among other languages like Python, Java, and C#.

Decorators In JavaScript

In JavaScript, decorators are a stage 2 proposal but can utilize through Babel and the TypeScript compiler.

JavaScript decorators are higher-order functions that get a capability contention, a class, or an individual from a class and add to the usefulness of that contention without modifying it. More or less, JavaScript decorators are capabilities that fold over a piece of code and add to its way of behaving without changing the nature of the code.

Since the decorator design includes wrapping a piece of code with one more without changing the covered code, decorators have been used in JavaScript as higher-order functions and function composition.

Why Decorators?

While we can extend ES6 classes, we want something better to share usefulness across different classes. We want a cleaner and better technique for disseminating functionality across classes.

The decorator proposition gives a definitive syntax structure to explaining and modifying JavaScript classes, properties, and item literals. The decorator design gives us a cleaner, better, and more reusable way to deal with disseminating usefulness across numerous classes. This is exemplified by libraries, for example, center decorators that give an assortment of decorators to designing a class and individuals from a class.

Instances of specific decorators it gives are:

  • Autobind

  • Readonly

  • Decorate

  • Nonconfigurable

  • ExtendDescriptor

These decorators can essentially be imported and utilized in our code as seen:

import { autobind } from 'core-decorators';

class Vehicle {
  @autobind // binds the this variable to an instance of Vehicle
  getVehicle() {
      return this;
  }
}

let car = new Vehicle(); // creates an instance of Vehicle called car
let { getVehicle } = car;

getVehicle() === car; // returns true;

From the code above, we can see that the Autobind decorator ties this variable in the getVehicle strategy to an example of the Vehicle class. Indeed, even in situations where the capability ought to lose this unique circumstance, it refers to the vehicle example.

How JavaScript Decorators Work

The Descriptor Object Property

An object in JavaScript comprises key-value matches. Every property has a descriptor property which is a variable item. The descriptor property contains the property estimation and other meta-information portraying the property.

Check the code below:

const Car = {name: "Rav4", type: "SUV", plateNo: 12345}

Object.getOwnPropertyDescriptor(Car, 'name');
// returns 
{ value: 'Rav4', writable: true, enumerable: true, configurable: true }

In the code above, we utilized the Object.getOwnPropertyDescriptor() technique to get the descriptor property object of the name property of the Car object.

The writable, enumerable, and configurable properties of the descriptor object decide the way of behaving of the name property. It decides whether the name property is writable, configurable, or enumerable.

Likewise, we can make another property or update a current property of the Car object with a custom descriptor by utilizing the Object.defineProperty().

Check the code below:

const Car = { name: "Rav4", type: "SUV", plateNo: 12345 };

Object.defineProperty(Car, "name", {
  writable: false
});

let carProp = Object.getOwnPropertyDescriptor(Car, "name");

console.log(carProp); // prints
{value: "Rav4", writable: false, enumerable: true, configurable: true}

In the code above, we have made the name property uneditable by adjusting the writable property of its descriptor object. So if we attempt to change the name property, we get a blunder as seen:

const Car = { name: "Rav4", type: "SUV", plateNo: 12345 };
Object.defineProperty(Car, "name", {
  writable: false
});

console.log((Car.name = "Venza")); // prints
// TypeError: Cannot assign to read only property 'name' of object '#<Object>'

With this information, we should learn how to decorate a property.

Decorating a Property

Consider the code:

class Car {
    getPlateNO() {`My plate number is ${this.platNO}`}
}

This class results in installing the getPlateNO method onto Car. Prototype. Such as:

Object.defineProperty(Car.prototype, 'getPlateNO', {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
});

In the code above, the specified function alludes to the worth of the getPlateNO property, which is the getPlateNO method.

Presently to make the getPlateNO uneditable, that is read-only. We can accomplish this by creating a readonly decorator, as seen below:

function readonly (target, key, descriptor) {
    descriptor.writable = false;
    return descriptor;
}

What's more, since a decorator goes before the syntax that characterizes a property, we can decorate our getPlateNO technique as follows:

class Car {
    @readonly
    getPlateNO() {`My plate number is ${this.plateNO}`}
}

Inside before introducing the descriptor onto the Car. Prototype the JavaScript engine initially conjures the decorator. It has the chance of mediating before the applicable defineProperty happens.

Consider the code below:


let descriptor = {
  value: specifiedFunction,
  enumerable: false,
  configurable: true,
  writable: true
};

descriptor = readonly(Car.prototype, 'getPlateN0', descriptor) || descriptor;
Object.defineProperty(Car.prototype, 'getPlateN0', descriptor);

From the code above, we see that the read-only decorator is conjured with our descriptor object as its third contention. It changes this descriptor and returns a custom descriptor object with the writable property set to false.

This custom descriptor object is currently passed to the Object.defineProperty technique to alter the descriptor property of the getPlateNO strategy. Successfully making the getPlateNO method read-only. This decorator design is cleaner, simpler to utilize, and more transparent.

Types of Decorators

There are mainly two types of decorators:

  • Class decorators

  • Member of a class decorators

Class Decorators

Designing a class is conceivable. For this situation, the decorator accepts the target constructor as contention, as seen below:

function annotation(target) {
   // Add a property on target
   target.isAnnotated = true;
}

// A simple decorator
@annotation
class Book {
      // some code 
 }

console.log(Book.isAnnotated) // prints true

The above is transpiled into:

"use strict";
var _class;
function annotation(target) {
  // Add a property on target
  target.isAnnotated = true;
}
let Book = annotation(
    (_class = class Book { 
        // some code 
    })) || _class;
console.log(Book.isAnnotated); // prints true

From the code above, we see that we quickly made a version of the Book by decorating the Book class with the annotation decorator.

Since we realize that the annotation expression returns a function, we can take things further by passing a contention as seen below:

function annotation (published) {
    return (target) => {
        // Add a property on target
        target.isAnnotated = true;
        target.isPublished = published;
    }
}
@annotation(true)
class Book {}
console.log(Book.isPublished) // prints true

It is transpiled to:

"use strict";
var _dec, _class;
function annotation(published) {
  return (target) => {
    // Add a property on target
    target.isAnnotated = true;
    target.isPublished = published;
  };
}
let Book =
  ((_dec = annotation(true)), _dec((_class = class Book {})) || _class);
console.log(Book.isPublished); // prints true

In the code above, we see that we decided if the Book is distributed or not by passing a boundary to our group decorator. Passing false as a contention to the decorator would set the published property misleading.

In the pattern above, our decorator function acts as a decorator factory. A decorator factory is a function that returns the expression that the decorator would call at runtime.

This technique can be used on property decorators.

Member of a Class Decorators

Consider the code below:

function logDescription (description) {
    return (target, key, descriptor) => {
        let fn = descriptor.value;
        descriptor.value = (...args) => {
          console.log(`The following payment is for ${descrition}`);
          return fn.call(this, ...args);
        }

        return descriptor;
    }
}

In the code above, we access the class technique utilizing the descriptor. It is conceivable since the descriptor works on the target. We supplanted this technique with a custom strategy that logs the installment depiction.

We can utilize it as seen here:

class Person {
    @logDescription("School fees")
    makePayment(currency,amount) {
        console.log(`${currency}${amount} has been debited from your account`)
    }
}
const father = new Person();
console.log(father.makePayment("$", 1000)); // prints
// The following payment is for School fees
// $1000 has been debited from your account

Wrapping Up

Decorators are exceptionally strong, and they have a wide range of applications. We can undoubtedly memoize our function calls with a solitary line, transform our class technique into a RESTendpoint, log valuable data, etc.

They improve code reuse, make our code perfect, and make it easier to peruse. Making your code more reusable by adding this extra layer of abstractions can accompany a likely expense — and increase troubleshooting trouble. This issue isn't intended for decorators; however, for each additional layer of abstraction added to your code.

Since code quality and extensibility are essential to each project, notably greater ones, you can settle on the choice of adding more layers of abstraction and make up for the gamble by adding better debugging and troubleshooting tools.

I hope the above info gives you a better understanding of javascript decorators. Please drop your feedback in the comments section.

Did you find this article valuable?

Support Quokka Labs' Blogs by becoming a sponsor. Any amount is appreciated!