Front-end design patterns and design principles design patterns

As a front-end developer, you will more or less practice design patterns when coding, but do you know what design patterns you are using?

Why must front-end developers understand design patterns?

If you don’t follow design patterns when coding, what can you do?

The above questions can be left for thinking. Here we first introduce some design patterns and design principles often encountered in front-end development.

Common front-end design patterns

1 Singleton mode

A pattern that allows only one instance to be created in the entire application.
In front-end development, it is often used to manage global state or resources, such as:

  • In React applications, use the Redux library to manage the state of the application;
  • In a Vue application, the Vuex library is used to manage the state of the application;
  • In Angular applications, Service is used to manage the state of the application;

Angular services are injectable classes that are used to provide shared data, functionality, or logic to components throughout the application; since services exist in the form of singletons, the same instance will be returned each time the service is injected .

Using @Injectable({ providedIn: 'root' })the decorator will MyServiceregister as a root-level provider, meaning that the entire application has access to a single instance of the service.

// my.service.ts

import {
    
     Injectable } from '@angular/core';

@Injectable({
    
    
  providedIn: 'root'
})

export class MyService {
    
    
  private count: number = 0;

  incrementCount() {
    
    
    this.count++;
  }

  getCount(): number {
    
    
    return this.count;
  }
}

In components, this service can be used through dependency injection ;

// my.component.ts

import {
    
     Component } from '@angular/core';
import {
    
     MyService } from './my.service';

@Component({
    
    
  selector: 'my-component',
  template: `
    <h2>Count: {
     
     { myService.getCount() }}</h2>
    <button (click)="incrementCount()">Increment Count</button>
  `
})
export class MyComponent {
    
    
  constructor(private myService: MyService) {
    
    }

  incrementCount() {
    
    
    this.myService.incrementCount();
  }
}

expand

If not used in the configuration @Injectableof a decorator in Angular , but specified in another module or component, the service will become a singleton within the scope of that module or component .providedIn"root"

This means that the service will share the same instance within a specified module or component and its subcomponents, rather than across the entire application. This is useful for scenarios where data or functionality needs to be shared within a specific scope .

For example, suppose you have two components ComponentAand ComponentB, they both reference the same service SharedServiceand configure the service as a provider in their respective modules.

// shared.service.ts

import {
    
     Injectable } from '@angular/core';

@Injectable({
    
    
  providedIn: 'other-module' // 指定其他模块,而不是 'root'
})
export class SharedService {
    
    
  public sharedData: string = 'Shared data';
}

// component-a.component.ts

import {
    
     Component } from '@angular/core';
import {
    
     SharedService } from './shared.service';

@Component({
    
    
  selector: 'component-a',
  template: `
    <h2>{
     
     { sharedService.sharedData }}</h2>
  `
})
export class ComponentA {
    
    
  constructor(public sharedService: SharedService) {
    
    }
}

// component-b.component.ts

import {
    
     Component } from '@angular/core';
import {
    
     SharedService } from './shared.service';

@Component({
    
    
  selector: 'component-b',
  template: `
    <h2>{
     
     { sharedService.sharedData }}</h2>
  `
})
export class ComponentB {
    
    
  constructor(public sharedService: SharedService) {
    
    }
}

In this case, the same instance SharedServiceis shared between ComponentAand ComponentBbut scoped to both components and their subcomponents.

This usage allows developers to have more fine-grained control over the sharing scope of services, so that different modules or components can have independent service instances.

2 Observer mode

2.1 Vue.js

A common application for the observer pattern on the front end is Vue.jsa reactive system using a framework. Vue.jsUse the Observer pattern to track changes in data and update views.

<!-- index.html -->

<!DOCTYPE html>
<html>
<head>
    <title>Vue.js Observer Example</title>
    <script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <h2>{
   
   { message }}</h2>
        <input v-model="message" type="text" placeholder="Type something...">
    </div>

    <script>
        // Initialize Vue
        new Vue({
      
      
            el: '#app',
            data: {
      
      
                message: 'Hello, Vue!'
            }
        });
    </script>
</body>
</html>

The above code is used Vue.jsto create a simple application with two-way data binding. The value of the message attribute will be displayed in the h2 tag on the page and can be edited through the input box.

Vue.jsThe responsive system will messageautomatically update the corresponding content on the page when attributes change. This is achieved by Vue.js using the Observer pattern internally . When the value of the message attribute changes, the observer will be notified and perform corresponding updates.

The application of this observer pattern eliminates the need for developers to explicitly modify the DOM to update the view. Instead, they only need to pay attention to data changes, and Vue.js will automatically handle the update process. This greatly simplifies the task of handling view updates in front-end development.

2.2 Angular

In AngularPython, the Observer pattern is mainly implemented using RxJSthe (Reactive Extensions) library. RxJSIt is a powerful event processing library that provides a variety of operator and observer pattern implementations to handle asynchronous event streams.

In Angular, by using Observables(observable objects) and Subjects(topics), developers can implement the effects of the observer pattern and perform event communication or data sharing between components .

Here is a simple example showing how to Angularuse the Observer pattern to implement communication between components:

// message.service.ts

import {
    
     Injectable } from '@angular/core';
import {
    
     Subject } from 'rxjs';

@Injectable()
export class MessageService {
    
    
  private messageSubject = new Subject<string>();

  message$ = this.messageSubject.asObservable();

  sendMessage(message: string) {
    
    
    this.messageSubject.next(message);
  }
}

The above code creates a MessageServiceservice called , which Subjectcreates a message topic using . By calling next()methods on a topic, you can send messages to observers subscribed to the topic.

// component-a.component.ts

import {
    
     Component } from '@angular/core';
import {
    
     MessageService } from './message.service';

@Component({
    
    
  selector: 'component-a',
  template: `
    <button (click)="sendMessage()">Send Message</button>
  `
})
export class ComponentA {
    
    
  constructor(private messageService: MessageService) {
    
    }

  sendMessage() {
    
    
    this.messageService.sendMessage('Hello from Component A!');
  }
}

The above code creates a ComponentAcomponent named , which is referenced through dependency injection MessageService. In the button's click event, we call sendMessage()a method to send a message.

// component-b.component.ts

import {
    
     Component } from '@angular/core';
import {
    
     MessageService } from './message.service';

@Component({
    
    
  selector: 'component-b',
  template: `
    <h2>{
     
     { receivedMessage }}</h2>
  `
})
export class ComponentB {
    
    
  receivedMessage: string = '';

  constructor(private messageService: MessageService) {
    
    }

  ngOnInit() {
    
    
    this.messageService.message$.subscribe(message => {
    
    
      this.receivedMessage = message;
    });
  }
}

The above code creates a ComponentBcomponent named and subscribes to the observable ngOnInit()in the lifecycle hook . Once a new message is sent, the observer will receive the message and update the properties.MessageServicemessage$receivedMessage

Using the above code ComponentA, when the button in is clicked, it will ComponentBsend a message to and ComponentBupdate the displayed message in . This enables two-way communication between components via the Observer pattern .

2.3 React

In , the observer pattern can be implemented Reactusing the and hook functions. Context APIHere's a simple example:

First, create an observer context ( ObserverContext):

import React, {
    
     createContext, useContext, useState } from 'react';

const ObserverContext = createContext();

export const ObserverProvider = ({
     
      children }) => {
    
    
  const [observers, setObservers] = useState([]);

  const addObserver = (observer) => {
    
    
    setObservers((prevObservers) => [...prevObservers, observer]);
  };

  const removeObserver = (observer) => {
    
    
    setObservers((prevObservers) =>
      prevObservers.filter((o) => o !== observer)
    );
  };

  const notifyObservers = () => {
    
    
    observers.forEach((observer) => observer());
  };

  const contextValue = {
    
    
    addObserver,
    removeObserver,
    notifyObservers,
  };

  return (
    <ObserverContext.Provider value={
    
    contextValue}>
      {
    
    children}
    </ObserverContext.Provider>
  );
};

export const useObserver = (observer) => {
    
    
  const {
    
     addObserver, removeObserver } = useContext(ObserverContext);

  // 添加观察者
  useEffect(() => {
    
    
    addObserver(observer);

    // 组件卸载时移除观察者
    return () => removeObserver(observer);
  }, [observer, addObserver, removeObserver]);
};

Then, use hooks in the components that need to be observed useObserverto subscribe to and respond to changes.

import React, {
    
     useState } from 'react';
import {
    
     useObserver } from './ObserverContext';

const Counter = () => {
    
    
  const [count, setCount] = useState(0);

  const handleIncrement = () => {
    
    
    setCount((prevCount) => prevCount + 1);
  };

  // 添加观察者
  useObserver(() => {
    
    
    console.log('Count has changed:', count);
  });

  return (
    <div>
      <p>Count: {
    
    count}</p>
      <button onClick={
    
    handleIncrement}>Increment</button>
    </div>
  );
};

In the above example, ObserverProviderthe context of the observer is provided and methods for adding, removing, and notifying the observer are defined. Use useObserverhooks to subscribe to changes in the observer pattern. When the state (here count) changes, the observer will be notified and perform appropriate actions.

Using this approach, Reacta simple observer pattern can be implemented in so that components can subscribe to and respond to specific events or state changes.

3 Code Factory Pattern

The code factory pattern is a design pattern for creating objects . It encapsulates the object creation process by using factory functions or classes . In this mode, we do not directly call the object's constructor to create the object, but use a special factory method to uniformly manage the creation of the object .

The main purpose of the Code Factory pattern is to hide the details of specific object creation and provide an extensible and flexible way to create objects. It encapsulates the instantiation logic of an object in an independent component, so that the process of creating an object can be managed centrally instead of being scattered throughout the application.

3.1 Angular

Here is a simple example showing how to Angularuse the Code Factory pattern in :

import {
    
     Injectable } from '@angular/core';

@Injectable({
    
    
  providedIn: 'root',
})
export class UserService {
    
    
  private users: string[] = [];

  addUser(user: string): void {
    
    
    this.users.push(user);
  }

  getUsers(): string[] {
    
    
    return this.users;
  }
}

@Injectable({
    
    
  providedIn: 'root',
})
export class UserFactory {
    
    
  constructor(private userService: UserService) {
    
    }

  createUser(name: string): void {
    
    
    this.userService.addUser(name);
  }
}

In the above example, UserServiceit is a service class that manages user information. UserFactoryis a factory class responsible for creating users and adding them to UserService.

In Angular, by using @Injectable()the decorator and providedIn: 'root'options, we can register UserServiceand UserFactoryas injectable services and ensure that they are available in any component throughout the application.

These services can then be used in other components through dependency injection:

import {
    
     Component } from '@angular/core';
import {
    
     UserFactory } from './user.factory';

@Component({
    
    ...})
export class AppComponent {
    
    
  constructor(private userFactory: UserFactory) {
    
    }

  createUser() {
    
    
    this.userFactory.createUser('John');
  }
}

In the above example, AppComponentthe component uses the factory pattern to create the user through dependency injection UserFactory.

By Angularusing the code factory pattern in , we can encapsulate object creation and initialization logic into injectable services and leverage dependency injection to conveniently use these services when needed. This improves code maintainability, scalability, and testability.

3.2 React

Here is a simple example showing how to Reactuse the Code Factory pattern in :

import React from 'react';

// 工厂函数
function createButton(type) {
    
    
  const Button = (props) => {
    
    
    let button;

    if (type === 'primary') {
    
    
      button = (
        <button className="primary-button" onClick={
    
    props.onClick}>
          {
    
    props.children}
        </button>
      );
    } else if (type === 'secondary') {
    
    
      button = (
        <button className="secondary-button" onClick={
    
    props.onClick}>
          {
    
    props.children}
        </button>
      );
    } else {
    
    
      throw new Error('Invalid button type');
    }

    return button;
  };

  return Button;
}

// 使用工厂函数创建按钮组件
const PrimaryButton = createButton('primary');
const SecondaryButton = createButton('secondary');

// 使用按钮组件
const App = () => (
  <div>
    <PrimaryButton onClick={
    
    () => console.log("Primary button clicked")}>
      Primary Button
    </PrimaryButton>
    <SecondaryButton onClick={
    
    () => console.log("Secondary button clicked")}>
      Secondary Button
    </SecondaryButton>
  </div>
);

In the above example, createButtonit is a factory function that typereturns a button component of a specific type based on the parameters passed in. Create buttons with different styles, behaviors, or functions based on different types.

By calling createButtonfactory functions, you can easily create different types of button components and use them in your application. In this way, you avoid repeatedly writing similar code in multiple places, and instead centrally manage and create components through the factory pattern.

Using the factory pattern, multiple components can be quickly created and customized as needed, and easily modified or extended. This pattern provides a more flexible, maintainable, and reusable way to create and manage Reactcomponents.

4 Strategy Mode

Strategy pattern is used to define a series of algorithms and encapsulate them into independent objects so that they can be replaced with each other. In front-end development, the strategy pattern can be used to handle multiple algorithms or logic situations, such as validating according to different rules in form validation.

Here is a simple example for sorting an array based on different sorting strategies :

// 排序策略对象
const sortingStrategies = {
    
    
  quickSort: (arr) => arr.sort((a, b) => a - b),
  mergeSort: (arr) => arr.sort((a, b) => b - a),
};

// 排序上下文对象
class SortContext {
    
    
  constructor(strategy) {
    
    
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    
    
    this.strategy = strategy;
  }

  sort(arr) {
    
    
    return this.strategy(arr);
  }
}

// 使用策略模式进行排序
const arr = [5, 2, 8, 1, 4];
const context = new SortContext(sortingStrategies.quickSort);
console.log(context.sort(arr)); // 输出: [1, 2, 4, 5, 8]

context.setStrategy(sortingStrategies.mergeSort);
console.log(context.sort(arr)); // 输出: [8, 5, 4, 2, 1]

In the above example, sortingStrategiesis an object containing different sorting strategies, where quickSortand mergeSortare two different sorting algorithms.

SortContextIs a sorting context object that receives a sorting strategy as a parameter and provides setStrategymethods to dynamically change the current sorting strategy. sortMethod sorts the given array using the current sorting strategy.

By creating SortContextinstances and setting different sorting strategies, we can choose a specific sorting algorithm to sort the array according to our needs. In this way, we can flexibly switch algorithms according to needs at runtime without modifying the sorting logic everywhere.

The Strategy pattern makes applications more scalable and flexible because we can easily add new strategies or modify existing ones without modifying existing code . At the same time, it improves code readability and maintainability by decoupling algorithm selection from actual execution logic.

Guess you like

Origin blog.csdn.net/weixin_45678402/article/details/132810545