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 MyService
register 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
@Injectable
of 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 ComponentA
and ComponentB
, they both reference the same service SharedService
and 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 SharedService
is shared between ComponentA
and ComponentB
but 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.js
a reactive system using a framework. Vue.js
Use 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.js
to 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.js
The responsive system will message
automatically 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 Angular
Python, the Observer pattern is mainly implemented using RxJS
the (Reactive Extensions) library. RxJS
It 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 Angular
use 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 MessageService
service called , which Subject
creates 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 ComponentA
component 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 ComponentB
component 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.MessageService
message$
receivedMessage
Using the above code ComponentA
, when the button in is clicked, it will ComponentB
send a message to and ComponentB
update 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 React
using the and hook functions. Context API
Here'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 useObserver
to 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, ObserverProvider
the context of the observer is provided and methods for adding, removing, and notifying the observer are defined. Use useObserver
hooks 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, React
a 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 Angular
use 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, UserService
it is a service class that manages user information. UserFactory
is 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 UserService
and UserFactory
as 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, AppComponent
the component uses the factory pattern to create the user through dependency injection UserFactory
.
By Angular
using 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 React
use 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, createButton
it is a factory function that type
returns 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 createButton
factory 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 React
components.
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, sortingStrategies
is an object containing different sorting strategies, where quickSort
and mergeSort
are two different sorting algorithms.
SortContext
Is a sorting context object that receives a sorting strategy as a parameter and provides setStrategy
methods to dynamically change the current sorting strategy. sort
Method sorts the given array using the current sorting strategy.
By creating SortContext
instances 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.