For other translations of this series, see [JS Working Mechanism – Xiaobai’s 1991 column – Nuggets (juejin. Cn)] (juejin.cn/column/6988…

This article is a simple introduction to design patterns. Design patterns are a very important practice, but most front ends either don’t understand it at all or understand it and rarely practice it. I recommend that you learn more about design patterns.

The overview

Design patterns have become standard solutions to common development problems. I think these design patterns have become the industry standard

Learning design patterns will not only make you a better developer, but it will also give you a better understanding of how frameworks are created. Most of the frameworks use some kind of design pattern, and learning about them will make it easier to understand some of the new frameworks.

Design patterns can be implemented in any language. It’s flexible, you can use it and expand it.

In this chapter we will look at JS design patterns, why they are needed, and the different types of design patterns.

What are design patterns? Why do we need it?

Design patterns have become the industry standard, something we can call ‘templates’. Using design patterns allows us to avoid the madness of code duplication.

The main reasons for using design patterns are as follows:

  • Help us write clean and organized code. Because design mode makes our code structure cleaner, easier to debug and maintain
  • They solved similar problems. Problems can easily arise when we build classes, decouple code, and reuse code and objects. In the future, decoupling code will make developing changes to code much less buggy.
  • Using design patterns wisely can save a lot of time. Because these patterns are mature enough, they can obviously be solved to save time.

Classification of design patterns

There are three main types of design patterns: creation, structure and behavior. Look at how they are classified.

Create a type

This class is mainly for creating objects. It creates special objects for specific user scenarios and hides the creation logic, exposing only the interface to us.

In general, we use interfaces to create objects for specific scenarios. The main patterns include:

  • The singleton
  • The factory
  • The abstract factory
  • The builders
  • The prototype

We’ll look at how the singleton pattern works

The singleton pattern

This pattern ensures that a class can create an instance.

This pattern has some misleading parameters, but is still easy to implement. The main steps are as follows:

  • Your class creates an object
  • Create an instance
  • Prevents the application from instantiating the object again elsewhere
  • Share instances as resources

To get straight to the code, let’s create a class and then singleton it later.

Step 1: Declare a Manufacture

class Database { constructor() { this.connectionURL = { name: "", options: {} } } // Our connect method taking in two arguments connect(name, options) { this.connectionURL.name = name; this.connectionURL.options = options; console.log(`DB: ${name} connected! `); } // Disconnect method disconnect() { console.log(`${this.connectionURL.name} is disconnected! `); } } // Instantiating our class const db = new Database() console.log(db.connect("Facebook"))Copy the code

Step 2: After instantiation, make your properties unmodifiable

class Database { constructor() { this.connectionURL = { name: "", options: {} } // This disallows modifying the instance we created Object.freeze(this); } // Our connect method taking in two arguments connect(name, options) { this.connectionURL.name = name; this.connectionURL.options = options; console.log(`DB: ${name} connected! `); } // Disconnect method disconnect() { console.log(`${this.connectionURL.name} is disconnected! `); } } // Instantiating our class const db = new Database(); console.log(db.connect("Facebook"));Copy the code

In the above code, no more attributes are allowed to be added or changed. In other languages, such as JAVA, we can create a getInstance() method to implement the singleton pattern. In the JS above, we use constructor() instead.

Step 3. Let our class instantiate itself and check to see if it has already been instantiated.

class Database { constructor() { // Check if our first instance has already been created if (Database.instance instanceof Database) { return Database.instance; } this.connectionURL = { name: "", options: {} } // This disallows modifying the instance we created Object.freeze(this); // Make our class an instance of itself Database.instance = this; } // Our connect method taking in two arguments connect(name, options) { this.connectionURL.name = name; this.connectionURL.options = options; console.log(`DB: ${name} connected! `); } // Disconnect method disconnect() { console.log(`${this.connectionURL.name} is disconnected! `); } } // Instantiating our class const db = new Database(); console.log(db.connect("Facebook"));Copy the code
// Check if our first instance has already been created
if (Database.instance instanceof Database) {
   return Database.instance;   
}
Copy the code

In the above code, after we instantiate for the first time, we check to see if it has been instantiated before, and if so, we return the previously instantiated one. As a result, repeated instantiation is avoided.

Let’s create two instances and verify that they are the same

class Database { constructor() { if (Database.instance instanceof Database) { return Database.instance; } this.connectionURL = { name: "", options: {} } Database.instance = this; } connect(uri, options) { this.connectionURL.name = name; this.connectionURL.options = options; console.log(`DB: ${uri} connected! `); } disconnect() { console.log(`${this.connectionURL.name} is disconnected! `); } } const db = new Database() const db1 = new Database() console.log(db === db1) // trueCopy the code

You can see that creating another instance is not allowed. There are other caveats to using this pattern

  • Concurrent scenarios. When more than two threads want to access a shared resource in a singleton, it may not be immediately available, causing a performance bottleneck.
  • Singletons are very much like global variables, so it’s hard to test everything because every part of the application uses them.

structured

This pattern mainly represents the relationship between entities, mainly a combination of objects and classes. The two key words of this pattern are composition and inheritance.

The main models include:

  • Adapter pattern
  • The appearance model
  • The bridge model
  • The proxy pattern
  • The flyweight pattern

Let’s focus on adaptation patterns

Adapter pattern

This pattern Bridges two incompatible classes. I use this pattern as a wrapper, splicing together two separate interfaces.

In a real-world case, for example, we could generate a socket adapter that connects sockets to incompatible plug-ins. This bridge is the adaptation mode. Let’s see how this works in JS. We’ll explain this briefly, but not to be confused with appearance patterns.

import { first, middle, last } from "random-name";

class randomName {
  generateFirstName() {
    return first();
  }
  
  generateMiddleName() {
    return middle();
  }
  
  generateLastName() {
    return last();
  }
}

export default new randomName();
Copy the code

The above code is the adapter, and we can use any library. Like this:

import name from "./random-name";

class PlugComponent {
  constructor() {
    this.firstName = name.generateFirstName();
    this.middleName = name.generateMiddleName();
    this.lastName = name.generateLastName();
  }
  
  generateFullName() {
    return `${this.firstName} ${this.middleName} ${this.lastName}`
  }
}

const names = new PlugComponent()
console.log(names.generateFullName()) // Victor Victor Jonah
Copy the code

This pattern improves reusability and flexibility.

Behavior type

This pattern focuses on communication between objects. Developers can maintain decoupling and flexibility when allowing objects to communicate.

This mode mainly includes:

  • Chain of responsibility
  • Command mode
  • The interpreter
  • The observer
  • Empty object mode

Let’s look at the empty object pattern

Empty object mode

This mode avoids returning NULL, encapsulates the NULL behavior, and returns the value expected by the client. Most of the time, null references are not allowed. So we’re going to do a Null check, which will make our code a lot more if/else. Using this pattern eliminates the need to write this logic.

Empty object mode is useful when we don’t want to return Null. It is also useful for catching exceptions in everyday situations. Let’s look at the implementation:

class Cat {
  sound() {
    return 'meoow';
  }
}

class NullAnimal {
  sound() {
    return "not an animal";
  }
}

const getAnimal = (type) => {
  return type === 'cat' ? new Cat() : new NullAnimal();
}

const results = ['cat', null]; 

const response = results.map((animal) => getAnimal(animal).sound());
// ["meoow", "not an animal"]
Copy the code

Instead of returning a Null reference, we return an expected value.

Best practices

For best practices, look at some big principles

  • Design before code: Design before code implementation is a weapon.
  • KISS – Keep it simple without presupposition: If you can’t explain it, it’s not simple enough. Design patterns are designed to keep the code simple and easy to understand.
  • DRY – Don’t repeat yourself: make your functions reusable. You don’t have to repeat it all over the place.
  • Focus on separation points: Separate services so that each subroutine has a single responsibility.