8 common front-end design patterns, which ones have you used?

1. What is a design pattern?

A design pattern is a solution to a problem in a certain situation. A design pattern is a template summed up through concepts, and a fixed thing summed up. Each pattern describes a problem that recurs all around us, and the core of the solution to that problem.

In front-end development, design patterns are a widely used idea. Design patterns can help developers solve common problems and provide reusable solutions. This article will introduce common front-end design patterns, and explain their implementation in detail through code.

2. Design principles

1. S – Single Responsibility Principle single responsibility principle

  • A program does one thing well
  • If the function is too complex, split it and keep each part independent

2. O – OpenClosed Principle open/closed principle

  • Open for extension, closed for modification
  • Extend new code instead of modifying existing code as requirements increase

3. L – Liskov Substitution Principle

  • Subclasses can override parent classes
  • Where the parent class can appear, the subclass can appear

4. I – Interface Segregation Principle Interface Segregation Principle

  • Keep the interface single and independent
  • Similar to the single responsibility principle, here more attention is paid to the interface

5. D – Dependency Inversion Principle Dependency Inversion Principle

  • Interface-oriented programming, relying on abstraction rather than concrete
  • The user only pays attention to the interface and not to the implementation of the concrete class

3. Types of Design Patterns

1. Structural Patterns: 

        Simplifies system design by identifying simple relationships between components in the system.

2. Creational Patterns: 

        Handle the creation of objects, and create objects in an appropriate way according to the actual situation. Conventional object creation methods may cause design problems or increase design complexity. Creational patterns solve problems by controlling the creation of objects in some way.

3. Behavioral Patterns: 

        Used to identify common interaction patterns between objects and implement them, thus increasing the flexibility of these interactions.

4. Eight common design patterns in the front end

1. Singleton mode

The singleton pattern means that a class can only be instantiated once and provides a global access point. This mode is very suitable for those scenarios that need to share resources. For example, in front-end development, we often need to ensure that certain resources are only loaded once, rather than being reloaded every time. Here is an example using the singleton pattern:

class Singleton {
  constructor() {
    if (typeof Singleton.instance === 'object') {
      return Singleton.instance;
    }
    this.name = 'Singleton';
    Singleton.instance = this;
    return this;
  }
}

const instance1 = new Singleton();
const instance2 = new Singleton();

console.log(instance1 === instance2); // true

In the above code, we created a Singleton class, which can only be instantiated once. If multiple attempts are made to instantiate the class, the same instance will be returned. This ensures that certain resources are only loaded once, improving performance.

2. Observer mode

Observer mode means that when an object changes state, all its dependents will be notified and updated automatically. This mode is very suitable for those scenarios that need to update the user interface in real time. Here is an example using the Observer pattern:

class ObserverList {
  constructor() {
    this.observerList = [];
  }

  add(observer) {
    return this.observerList.push(observer);
  }

  remove(observer) {
    this.observerList = this.observerList.filter((obs) => obs !== observer);
  }

  count() {
    return this.observerList.length;
  }

  get(i) {
    return this.observerList[i];
  }
}

class Subject {
  constructor() {
    this.observers = new ObserverList();
  }

  addObserver(observer) {
    this.observers.add(observer);
  }

  removeObserver(observer) {
    this.observers.remove(observer);
  }

  notify(context) {
    const observerCount = this.observers.count();
    for (let i = 0; i < observerCount; i++) {
      this.observers.get(i).update(context);
    }
  }
}

class Observer {
  constructor() {
    this.update = () => {};
  }
}

const subject = new Subject();

const observer1 = new Observer();
observer1.update = function (context) {
  console.log(`Observer 1: ${context}`);
};

const observer2 = new Observer();
observer2.update = function (context) {
  console.log(`Observer 2: ${context}`);
};

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.notify('Hello World');

In the above code, we created a Subject class and an Observer class. The Subject class contains a list of observers and provides methods for adding, removing, and notifying observers. The Observer class contains an update method for handling notifications from the Observed. In the main program, we create two observers and add them to the observer's list of observers. We then notify the observable, which automatically notifies all observers and executes their update method.

3. Factory mode

The factory pattern refers to creating instances of other classes through a factory class. This pattern is very suitable for those scenarios that need to create different instances according to different conditions. Here is an example using the factory pattern:

class ProductA {
	constructor(name) {
		this.name = name;
	}
	operation() {
		console.log(`Product A (${this.name}) is working.`);
	}
}
class ProductB {
	constructor(name) {
		this.name = name;
	}
	operation() {
		console.log(`Product B (${this.name}) is working.`);
	}
}
class Factory {
	createProduct(type, name) {
		switch (type) {
			case 'A':
				return new ProductA(name);
			case 'B':
				return new ProductB(name);
			default:
				throw new Error('Invalid product type.');
		}
	}
}
const factory = new Factory();
const productA1 = factory.createProduct('A', 'productA1');
const productA2 = factory.createProduct('A', 'productA2');
const productB1 = factory.createProduct('B', 'productB1');
const productB2 = factory.createProduct('B', 'productB2');
productA1.operation(); // Product A (productA1) is working.
productA2.operation(); // Product A (productA2) is working.
productB1.operation(); // Product B (productB1) is working.
productB2.operation(); // Product B (productB2) is working.

In the above code, we created two product classes `ProductA` and `ProductB`, and a factory class `Factory`. The factory class provides a method `createProduct` to create a product instance, which determines which product instance to create based on the parameters passed in. In the main program, we create four different product instances through the factory class, and execute their operation methods respectively.

4. Decorator mode

The decorator pattern refers to dynamically adding some additional functionality to an object. This pattern is very suitable for scenarios that need to dynamically change the behavior of objects at runtime. Here is an example using the decorator pattern:

class Shape {
  draw() {
  }
}

class Circle extends Shape {
  draw() {
    console.log('Drawing a circle.');
  }
}

class Rectangle extends Shape {
  draw() {
    console.log('Drawing a rectangle.');
  }
}

class Decorator {
  constructor(shape) {
    this.shape = shape;
  }

  draw() {
    this.shape.draw();
  }
}

class RedShapeDecorator extends Decorator {
  draw() {
    this.shape.draw();
    this.setRedBorder();
  }

  setRedBorder() {
    console.log('Setting red border.');
  }
}

const circle = new Circle();
const rectangle = new Rectangle();

circle.draw(); // Drawing a circle.
rectangle.draw(); // Drawing a rectangle.

const redCircle = new RedShapeDecorator(new Circle());
const redRectangle = new RedShapeDecorator(new Rectangle());

redCircle.draw(); // Drawing a circle. Setting red border.
redRectangle.draw(); // Drawing a rectangle. Setting red border.

In the above code, we created two shape classes Circle and Rectangle, and a decorator class Decorator. The decorator class contains a shape object, which is used to decorate it. Then, we created a red shape decorator class RedShapeDecorator to add a red border around the shape. In the main program, we first execute the drawing method of the original shape, and then decorate it with the red decorator.

5. Proxy mode

The proxy pattern refers to using a proxy object to control access to another object. This pattern is very suitable for those scenarios that need to control access to some sensitive resources. Here is an example using the proxy pattern:

class Image {
  constructor(url) {
    this.url = url;
    this.loadImage();
  }

  loadImage() {
    console.log(`Loading image from ${this.url}`);
  }
}

class ProxyImage {
  constructor(url) {
    this.url = url;
  }

  loadImage() {
    if (!this.image) {
      this.image = new Image(this.url);
    }
    console.log(`Displaying cached image from ${this.url}`);
  }
}

const image1 = new Image('https://example.com/image1.jpg');
const proxyImage1 = new ProxyImage('https://example.com/image1.jpg');

proxyImage1.loadImage(); // Loading image from https://example.com/image1.jpg
proxyImage1.loadImage(); // Displaying cached image from https://example.com/image1.jpg

const image2 = new Image('https://example.com/image2.jpg');
const proxyImage2 = new ProxyImage('https://example.com/image2.jpg');

proxyImage2.loadImage(); // Loading image from https://example.com/image2.jpg
proxyImage2.loadImage(); // Displaying cached image from https://example.com/image2.jpg

In the above code, we created an image class `Image` and a proxy image class `ProxyImage`. The proxy image class contains an image object to control access to its loading and display. In the main program, we first create a real image object and use the proxy image object to access it. On the first access, the proxy image object will load and display the real image; on the second access, the proxy image object will directly obtain and display the image from the cache.

6. Adapter mode

The adapter pattern refers to converting an object of an incompatible interface into an object of a compatible interface. This mode is very suitable for those scenarios that need to change the interface without affecting the original code. Here is an example using the Adapter pattern:

class OldCalculator {
  operations(a, b, operation) {
    switch (operation) {
      case 'add':
        return a + b;
      case 'sub':
        return a - b;
      default:
        return NaN;
    }
  }
}

class NewCalculator {
  add(a, b) {
    return a + b;
  }

  sub(a, b) {
    return a - b;
  }
}

class CalculatorAdapter {
  constructor() {
    this.newCalculator = new NewCalculator();
  }

  operations(a, b, operation) {
    switch (operation) {
      case 'add':
        return this.newCalculator.add(a, b);
      case 'sub':
        return this.newCalculator.sub(a, b);
      default:
        return NaN;
    }
  }
}

const oldCalculator = new OldCalculator();
console.log(oldCalculator.operations(10, 5, 'add')); // 15

const newCalculator = new NewCalculator();
console.log(newCalculator.add(10, 5)); // 15

const calculatorAdapter = new CalculatorAdapter();
console.log(calculatorAdapter.operations(10, 5, 'add')); // 15

In the above code, we created an old calculator class OldCalculator and a new calculator class NewCalculator. Then, we created an adapter class CalculatorAdapter, which contains a new calculator object to convert the operation of the old calculator into the operation of the new calculator. In the main program, we use the old calculator, the new calculator, and the adapter to do the addition and get the same result.

7. Command mode

The command mode refers to encapsulating a request into an object and providing all information related to the execution of the request. This pattern is great for scenarios where you need to perform several different operations. Here is an example using the command pattern:

class Receiver {
  run() {
    console.log('Receiver is running.');
  }
}

class Command {
  constructor(receiver) {
    this.receiver = receiver;
  }

  execute() {}
}

class StartCommand extends Command {
  execute() {
    this.receiver.run();
  }
}

class Invoker {
  setCommand(command) {
    this.command = command;
  }

  executeCommand() {
    this.command.execute();
  }
}

const receiver = new Receiver();
const startCommand = new StartCommand(receiver);
const invoker = new Invoker();
invoker.setCommand(startCommand);
invoker.executeCommand(); // Receiver is running.

In the above code, we created a receiver class Receiver and a command base class Command. Then, we created a specific command class StartCommand, which inherits from the command base class and implements the execute method to start the receiver. Finally, we created a caller class Invoker, which contains a command object and provides a method to execute the command. In the main program, we create a receiver object, a concrete command object and a caller object, and set the concrete command object as the command of the caller object. Then, we execute the execute method of the caller object, which calls the execute method of the concrete command object, thus starting the receiver. This example is relatively simple, but the command pattern can be applied to many complex scenarios, such as undo/redo operations, transaction management, etc.

For more content, please pay attention to the official account [Programmer Style] to get more exciting content!

8. Observer mode

Observer mode refers to defining a one-to-many dependency relationship between objects, so that whenever an object changes state, all objects that depend on it will be notified and automatically updated. This pattern is very suitable for those scenarios that need to implement event handling in the application. Here is an example using the Observer pattern:

class Subject {
  constructor() {
    this.observers = [];
  }

  addObserver(observer) {
    this.observers.push(observer);
  }

  removeObserver(observer) {
    const index = this.observers.indexOf(observer);
    if (index >= 0) {
      this.observers.splice(index, 1);
    }
  }

  notifyObservers() {
    for (const observer of this.observers) {
      observer.update(this);
    }
  }
}

class ConcreteSubject extends Subject {
  constructor(state) {
    super();
    this.state = state;
  }

  getState() {
    return this.state;
  }

  setState(state) {
    this.state = state;
    this.notifyObservers();
  }
}

class Observer {
  update() {}
}

class ConcreteObserver extends Observer {
  update(subject) {
    console.log(`The subject has changed to ${subject.getState()}.`);
  }
}

const subject = new ConcreteSubject('state1');
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();

subject.addObserver(observer1);
subject.addObserver(observer2);

subject.setState('state2'); // The subject has changed to state2.

In the above code, we created a subject base class Subject and a concrete subject class ConcreteSubject. The subject class contains a state property and a set of observer objects, and provides methods to add, delete, and notify observers. Then, we created an observer base class Observer and a specific observer class ConcreteObserver, which inherits from the observer base class and implements the update method. In the main program, we create a concrete subject object, two concrete observer objects, and add the observer objects to the subject objects. Then, we modify the state of the subject object, and notify the observer object to update through the subject object.

The above are eight common design patterns and their application scenarios and sample codes. Of course, this is just the tip of the iceberg, there are many other design patterns that can be applied to different scenarios. Familiarity with various design patterns and being able to use them flexibly can make you a better developer.

Guess you like

Origin blog.csdn.net/dreaming317/article/details/129837200