前端8种常用设计模式
1. 发布 - 订阅模式
1. 定义与概念
- 发布 - 订阅模式(Publish - Subscribe Pattern)是一种软件设计模式。在前端开发中,它用于处理组件之间的通信,特别是当一个对象的状态变化需要通知其他多个对象时。这种模式包含了发布者(Publisher)和订阅者(Subscriber)两种角色。
- 发布者负责发布消息,它并不关心谁(哪些订阅者)会接收到消息。订阅者则是对某些消息感兴趣,它们会向发布者订阅自己感兴趣的消息类型,当发布者发布相应消息时,订阅者就会收到通知并执行相应的操作。
2. 简单示例
- 假设我们在一个网页中有多个部分需要根据用户登录状态进行更新。我们可以创建一个发布 - 订阅系统来处理这种情况。
- 首先,我们定义一个事件中心(Event Hub)来管理订阅和发布操作。以下是一个简单的 JavaScript 示例:
const eventHub = {
subscribers: {},
subscribe: function (eventType, callback) {
if (!this.subscribers[eventType]) {
this.subscribers[eventType] = [];
}
this.subscribers[eventType].push(callback);
},
publish: function (eventType, data) {
const subscribers = this.subscribers[eventType] || [];
subscribers.forEach((callback) => {
callback(data);
});
}
};
- 这里,
eventHub就是发布 - 订阅模式中的中心枢纽。subscribe方法用于订阅事件,它接收一个事件类型(如'userLoggedIn')和一个回调函数。回调函数是当事件发布时要执行的操作。publish方法用于发布事件,它会遍历所有订阅了指定事件类型的回调函数,并将数据传递给它们。
- 现在,假设我们有两个组件,一个是导航栏(
Navbar),一个是用户信息面板(UserInfoPanel),它们都需要在用户登录时更新。
- 对于导航栏组件:
function Navbar() {
eventHub.subscribe('userLoggedIn', function (userData) {
console.log(`导航栏更新:欢迎 ${userData.username}`);
});
}
function UserInfoPanel() {
eventHub.subscribe('userLoggedIn', function (userData) {
console.log(`用户信息面板更新:显示 ${userData.username} 的详细信息`);
});
}
function userLoginSuccess(userData) {
eventHub.publish('userLoggedIn', userData);
}
3. 在前端框架中的应用
- Vue.js:Vue.js 有自己的事件系统,它在组件之间的通信中也体现了发布 - 订阅的思想。例如,在父子组件通信中,父组件可以通过
$emit方法发布一个自定义事件,子组件可以通过v - on(缩写为@)来订阅这个事件。
- 假设我们有一个父组件ParentComponent和一个子组件ChildComponent。
- 父组件模板:
<template>
<div>
<button @click="sendMessage">向子组件发送消息</button>
<ChildComponent @parentMessage="handleChildResponse"/>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: {
ChildComponent
},
methods: {
sendMessage() {
this.$emit('parentMessage', '这是来自父组件的消息');
},
handleChildResponse(message) {
console.log('父组件收到子组件的响应:', message);
}
}
};
</script>
<template>
<div>
<p>等待父组件消息</p>
</div>
</template>
<script>
export default {
methods: {
handleParentMessage(message) {
console.log('子组件收到父组件消息:', message);
this.$emit('childResponse', '子组件已收到消息');
}
},
mounted() {
this.$on('parentMessage', this.handleParentMessage);
}
};
</script>
- 这里,父组件通过
$emit发布parentMessage事件,子组件通过$on订阅这个事件。当事件触发时,子组件执行handleParentMessage方法,并可以通过$emit发布自己的响应事件,父组件再通过@(v - on)来订阅这个响应事件。
- React.js:虽然 React 本身没有内置像 Vue 这样的事件系统,但可以使用第三方库如
pubsub - js来实现发布 - 订阅模式。
- 首先安装
pubsub - js:npm install pubsub - js。
- 以下是一个简单的示例:
- 定义一个消息发布者组件:
import PubSub from 'pubsub - js';
import React, { Component } from 'react';
class PublisherComponent extends Component {
handlePublishMessage = () => {
const message = '这是一条发布的消息';
PubSub.publish('messagePublished', message);
};
render() {
return (
<button onClick={this.handlePublishMessage}>发布消息</button>
);
}
}
import PubSub from 'pubsub - js';
import React, { Component } from 'react';
class SubscriberComponent extends Component {
constructor(props) {
super(props);
this.state = {
receivedMessage: null
};
}
componentDidMount() {
const token = PubSub.subscribe('messagePublished', (msg, data) => {
this.setState({
receivedMessage: data
});
});
this.token = token;
}
componentWillUnmount() {
PubSub.unsubscribe(this.token);
}
render() {
return (
<div>
{this.state.receivedMessage? (
<p>收到消息:{this.state.receivedMessage}</p>
) : (
<p>等待消息</p>
)}
</div>
);
}
}
4. 优点
- 解耦组件:发布 - 订阅模式使得组件之间的依赖关系更加松散。发布者和订阅者不需要直接知道对方的存在,它们只通过事件中心进行通信。这样在修改一个组件时,不会影响到其他组件,只要事件的接口(事件类型和传递的数据结构)不变。
- 可扩展性高:可以方便地添加新的订阅者。例如,在网页应用中,如果需要新增一个组件来根据用户登录状态更新,只需要在这个新组件中订阅
userLoggedIn事件即可,而不需要对发布者和其他订阅者进行大量的修改。
- 灵活性好:同一个事件可以有多个不同的订阅者,每个订阅者可以根据自己的需求对事件进行不同的处理。比如在用户登录事件中,导航栏可能只更新欢迎信息,而用户信息面板可以更新用户的详细信息和权限相关内容。
5. 缺点
- 增加系统复杂性:引入了事件中心,使得代码的逻辑流程可能会变得不那么直观。对于初学者来说,理解事件的发布、订阅和传递过程可能会有一定的难度。
- 可能导致内存泄漏:如果订阅者没有正确地取消订阅(特别是在单页应用中,组件的生命周期比较复杂),当组件被销毁后,仍然可能会收到事件通知,这可能会导致内存泄漏或者其他意想不到的错误。因此,在组件销毁时,一定要记得取消订阅相关的事件。
2. 单例模式(Singleton Pattern)
1. 定义与概念
- 单例模式(Singleton Pattern)是一种创建型设计模式。它确保一个类只有一个实例,并提供一个全局访问点来访问这个实例。在前端开发中,单例模式用于管理共享资源,例如全局配置对象、日志记录器、数据库连接池等,避免多个实例造成资源浪费或数据不一致的情况。
1. 实现方式
- 简单的 JavaScript 实现
- 以下是一个基本的单例模式的 JavaScript 示例。我们创建一个名为
Singleton的对象。
const Singleton = (function () {
let instance;
function createInstance() {
return {
property: '这是单例对象的属性',
method: function () {
console.log('这是单例对象的方法');
}
};
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
- 在这个示例中,
Singleton是一个立即执行函数表达式(IIFE)。它内部有一个instance变量,用于存储单例对象的实例。createInstance函数用于创建这个实例,它返回一个包含属性和方法的对象。getInstance函数是对外提供的访问点,当第一次调用getInstance时,instance为空,会创建一个新的实例,之后再调用getInstance就会返回已经创建好的实例。
- 使用 ES6 类实现单例模式(JavaScript)
class SingletonClass {
constructor() {
if (!SingletonClass.instance) {
SingletonClass.instance = this
this.property = '这是单例对象的属性'
this.method = function () {
console.log('这是单例对象的方法')
}
}
return SingletonClass.instance
}
}
const singletonInstance1 = new SingletonClass()
const singletonInstance2 = new SingletonClass()
console.log(singletonInstance1 === singletonInstance2)
- 在这个类实现中,
SingletonClass的构造函数检查是否已经存在instance属性。如果不存在,就将当前对象(this)赋值给instance,并定义属性和方法。如果instance已经存在,就直接返回这个已经存在的实例,这样就保证了只有一个实例被创建。
3. 在前端框架中的应用
- Vue.js 中的单例应用场景
- 在 Vue.js 应用中,全局事件总线(Event Bus)可以看作是一种单例模式的应用。它用于在不同组件之间进行通信,而不需要通过父子组件的 props 和事件传递的方式。
- 例如,我们可以创建一个全局事件总线来处理用户登录状态的通知。首先,在
main.js(Vue 应用的入口文件)中创建事件总线:
import Vue from 'vue';
const eventBus = new Vue();
export default eventBus;
- 然后,在组件中可以使用这个事件总线来发布和订阅事件。比如一个登录组件可以发布用户登录成功的事件:
import eventBus from './eventBus';
export default {
methods: {
userLoginSuccess() {
eventBus.$emit('userLoggedIn', {
username: '示例用户'
});
}
}
};
import eventBus from './eventBus';
export default {
created() {
eventBus.$on('userLoggedIn', (userData) => {
console.log(`导航栏更新:欢迎 ${userData.username}`);
});
}
};
- 这里的
eventBus就是一个单例对象,整个应用中只有一个这样的Vue实例用于事件的发布和订阅,避免了创建多个事件发布 - 订阅系统造成的混乱。
- React.js 中的单例应用场景
- 在 React.js 中,单例模式可以用于管理全局状态,比如使用 Redux 等状态管理库。Redux 中的`store`通常是单例的。
- 例如,我们有一个简单的 Redux 存储设置。首先,定义一个`reducer`来处理状态变化:
const initialState = {
count: 0
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return {
...state,
count: state.count + 1
};
default:
return state;
}
}
import { createStore } from 'redux';
const store = createStore(counterReducer);
export default store;
- 在 React 组件中,可以通过
store来获取和更新状态。多个组件可以共享这个store,因为它是单例的,这样就保证了整个应用的状态是统一管理的。例如,一个组件用于增加计数:
import React from 'react';
import store from './store';
function IncrementButton() {
const handleIncrement = () => {
store.dispatch({
type: 'INCREMENT'
});
};
return (
<button onClick={handleIncrement}>增加计数</button>
);
}
import React from 'react';
import store from './store';
function CounterDisplay() {
const count = store.getState().count;
return (
<p>计数:{count}</p>
);
}
4. 优点
- 资源共享:单例模式可以有效地共享资源。例如,对于一个应用中的配置对象,只需要一个实例就可以在各个部分进行访问和修改,避免了创建多个相同配置对象造成的内存浪费。
- 全局访问点:提供了一个统一的全局访问点,使得代码的各个部分可以方便地获取到单例对象。这对于管理全局状态或者共享功能非常有用,如全局的日志记录器,各个模块都可以通过相同的方式访问来记录日志。
- 控制实例数量:能够严格控制类的实例数量,确保在整个应用中只有一个实例,避免了因为多个实例可能导致的数据不一致或冲突问题。
5. 缺点
- 违反单一职责原则:单例类可能会承担过多的职责,因为它要负责自身的实例化、管理以及提供全局访问点。例如,一个单例的数据库连接池对象,除了管理连接池的连接,还要保证自身的单例性质,这使得类的职责不够单一。
- 不利于单元测试:由于单例模式的全局性质,在单元测试时可能会比较困难。如果一个单例对象在多个测试用例中被使用,可能会导致测试之间相互干扰,因为单例对象的状态可能会在不同测试用例之间产生混淆。
- 可能导致代码耦合:如果单例对象在多个模块中被广泛使用,可能会导致这些模块与单例对象之间的耦合度过高。当需要修改单例对象的内部实现或者接口时,可能会影响到大量使用该单例对象的模块。
3. 策略模式
1. 定义与概念
- 策略模式是一种行为设计模式。它定义了一系列算法,将每个算法封装起来,并使它们可以相互替换。在前端开发中,策略模式允许在运行时根据不同的条件选择不同的行为或算法,而不需要对使用这些行为的代码进行大量修改。
- 简单来说,策略模式把算法的选择和算法的实现分离开来,使得算法可以独立地变化。例如,在一个电商网站中,计算商品总价的方式可能会根据不同的促销活动(如满减、折扣、赠品等)而有所不同,这些不同的计算方式就可以看作是不同的策略。
2. 结构组成
- 策略(Strategy)接口或抽象类:定义了策略的方法签名,所有具体的策略类都要实现这个接口或继承这个抽象类。例如,在计算商品总价的例子中,这个接口可以是一个名为
calculateTotalPrice的方法。
- 具体策略(Concrete Strategy)类:实现了策略接口或抽象类,提供了具体的算法实现。比如针对满减策略、折扣策略等分别有对应的具体策略类,它们都实现了
calculateTotalPrice方法,但内部的计算逻辑不同。
- 上下文(Context)类:持有一个策略对象的引用,用于调用策略对象的方法。在前端应用场景中,上下文类可能是购物车对象,它通过调用策略对象的
calculateTotalPrice方法来计算总价。
3. 简单示例(JavaScript)
- 首先定义一个策略接口,这里以一个简单的函数形式表示:
function calculateTotalPrice(price, quantity) {
throw new Error('该方法需要在具体策略类中实现');
}
function DiscountStrategy(rate) {
this.rate = rate;
}
DiscountStrategy.prototype.calculateTotalPrice = function (price, quantity) {
return price * quantity * this.rate;
};
function FullReductionStrategy(full, reduction) {
this.full = full;
this.reduction = reduction;
}
FullReductionStrategy.prototype.calculateTotalPrice = function (price, quantity) {
const total = price * quantity;
if (total >= this.full) {
return total - this.reduction;
}
return total;
}
function ShoppingCart() {
this.strategy;
}
ShoppingCart.prototype.setStrategy = function (strategy) {
this.strategy = strategy;
};
ShoppingCart.prototype.calculateTotal = function (price, quantity) {
return this.strategy.calculateTotalPrice(price, quantity);
};
const cart = new ShoppingCart()
const discountStrategy = new DiscountStrategy(0.8)
cart.setStrategy(discountStrategy)
const totalPrice1 = cart.calculateTotal(100, 2)
console.log(totalPrice1)
const fullReductionStrategy = new FullReductionStrategy(200, 50)
cart.setStrategy(fullReductionStrategy)
const totalPrice2 = cart.calculateTotal(100, 3)
console.log(totalPrice2)
4. 在前端框架中的应用
- Vue.js 中的应用
- 在 Vue 组件中,策略模式可以用于处理不同的表单验证策略。例如,一个表单可能有多种字段,如文本字段、邮箱字段、密码字段等,每个字段的验证策略不同。
- 首先定义验证策略接口:
const validateStrategy = {
validate: function (value) {
throw new Error('该方法需要在具体验证策略类中实现');
}
};
const emailValidateStrategy = {
validate: function (value) {
const emailRegex = /^[a - zA - Z0 - 9_.+-]+@[a - zA - Z0 - 9 -]+.[a - zA - Z0 - 9-.]+$/
return emailRegex.test(value)
}
}
const textValidateStrategy = {
validate: function (value) {
return value.length > 0
}
}
Vue.component('my - form', {
template: `
<div>
<input v - model="email" placeholder="请输入邮箱">
<input v - model="text" placeholder="请输入文本">
<button @click="validateForm">验证表单</button>
</div>
`,
data() {
return {
email: '',
text: ''
}
},
methods: {
validateForm() {
const emailValid = emailValidateStrategy.validate(this.email)
const textValid = textValidateStrategy.validate(this.text)
if (emailValid && textValid) {
console.log('表单验证通过')
} else {
console.log('表单验证未通过')
}
}
}
})
- React.js 中的应用
- 在 React 中,策略模式可以用于处理不同的组件渲染策略。例如,一个列表组件可能根据不同的数据类型(如数组、对象等)有不同的渲染策略。
- 首先定义渲染策略接口:
const renderStrategy = {
render: function (data) {
throw new Error('该方法需要在具体渲染策略类中实现');
}
};
const arrayRenderStrategy = {
render: function (data) {
return data.map((item, index) => <li key={index}>{item}</li>);
}
};
const objectRenderStrategy = {
render: function (data) {
return Object.keys(data).map((key) => <li key={key}>{key}: {data[key]}</li>);
}
};
class MyList extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
strategy: arrayRenderStrategy
};
}
componentDidMount() {
const data = [1, 2, 3];
this.setState({
data
});
}
changeStrategy() {
this.setState({
strategy: objectRenderStrategy
});
}
render() {
const { data, strategy } = this.state;
return (
<div>
<ul>
{strategy.render(data)}
</ul>
<button onClick={this.changeStrategy.bind(this)}>切换渲染策略</button>
</div>
);
}
}
- 优点
- 可维护性高:每个策略都是一个独立的类或函数,当需要修改某个策略的实现时,只需要在对应的策略类或函数中进行修改,不会影响到其他策略和使用这些策略的上下文代码。
- 可扩展性强:可以方便地添加新的策略,只要新策略实现了策略接口或抽象类。例如,在电商网站的总价计算中,如果新增一种促销策略(如买一送一),只需要创建一个新的具体策略类并实现
calculateTotalPrice方法即可。
- 代码复用性好:不同的上下文可以复用相同的策略。例如,多个购物车对象可以复用折扣策略、满减策略等,提高了代码的复用程度。
- 缺点
- 策略类增多:随着策略的增加,策略类或函数的数量会增多,可能会导致代码结构变得复杂,特别是当策略之间有一些相似性时,可能会出现代码重复的情况。
- 客户端需要了解策略:客户端(使用策略的代码)需要知道有哪些策略可供选择,并且需要选择合适的策略。在一些复杂的系统中,这可能会增加客户端代码的复杂性。
4. 适配器模式
1. 定义与概念
- 适配器模式是一种结构型设计模式。它的主要作用是将一个类的接口转换成客户期望的另一个接口,使得原本由于接口不兼容而不能一起工作的类可以协同工作。就像是一个转接头,把一种形状的插头(接口)转换为另一种形状的插座(另一个接口)能够接受的形式。
- 例如,在前端开发中,可能会有一个新的第三方库提供了一些功能,但其接口和我们现有代码所期望的接口不同,这时就可以使用适配器模式来使它们兼容。
2. 结构组成
- 目标(Target)接口:这是客户端所期望的接口。客户端代码通过这个接口来调用方法,它定义了客户端想要使用的方法签名。
- 被适配者(Adaptee)类:这是需要被适配的类,它有自己的接口和方法实现,但这个接口与目标接口不兼容。
- 适配器(Adapter)类:它实现了目标接口,并且在内部包含一个被适配者的实例。适配器类的方法会调用被适配者的相应方法来完成功能,从而将被适配者的接口转换为目标接口。
3. 简单示例(JavaScript)
- 假设我们有一个旧的函数,它的接口不符合我们新的需求。
- 被适配者(旧函数):
function oldFunction(data) {
console.log('旧函数执行,数据为: ', data);
}
function targetFunction(arg1, arg2) {
throw new Error('该方法需要在适配器中实现');
}
function adapterFunction(arg1, arg2) {
const data = {
arg1: arg1,
arg2: arg2
};
oldFunction(data);
}
adapterFunction('参数1', '参数2');
4. 在前端框架中的应用
- Vue.js 中的应用
- 在 Vue 组件中,当整合不同的数据源或者外部库时可能会用到适配器模式。例如,有一个外部的图表库,它的数据格式要求是一个特定的数组结构,但我们从后端获取的数据格式与之不同。
- 假设外部图表库期望的数据格式是
[{name: '类别1', value: 10}, {name: '类别2', value: 20}],而后端返回的数据格式是{category: ['类别1', '类别2'], data: [10, 20]}。
- 首先定义目标接口,即图表库所期望的数据获取函数:
function getDataForChart() {
throw new Error('该方法需要在适配器中实现');
}
function getBackendData() {
return {
category: ['类别1', '类别2'],
data: [10, 20]
};
}
function adapterForChartData() {
const backendData = getBackendData()
const adaptedData = backendData.category.map((name, index) => ({
name: name,
value: backendData.data[index]
}))
return adaptedData
}
Vue.component('chart - component', {
template: '<div>图表组件</div>',
mounted() {
const data = adapterForChartData();
console.log('传递给图表库的数据: ', data);
}
});
- React.js 中的应用
- 在 React 中,假设我们有一个旧的 API 返回的数据格式与 React 组件期望的数据格式不同。例如,旧 API 返回的用户信息是
{user_id: 1, user_name: '张三', user_email: 'zhangsan@example.com'},而 React 组件期望的用户信息格式是{id: 1, name: '张三', email: 'zhangsan@example.com'}。
- 定义目标接口(React 组件期望的数据获取函数):
function getUserData() {
throw new Error('该方法需要在适配器中实现');
}
function getOldUserData() {
return {
user_id: 1,
user_name: '张三',
user_email: 'zhangsan@example.com'
};
}
function adapterForUserData() {
const oldData = getOldUserData()
const adaptedData = {
id: oldData.user_id,
name: oldData.user_name,
email: oldData.user_email
}
return adaptedData
}
class UserInfoComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
userData: null
};
}
componentDidMount() {
const data = adapterForUserData();
this.setState({
userData: data
});
}
render() {
const { userData } = this.state;
if (!userData) {
return <div>加载中...</div>;
}
return (
<div>
<p>用户ID: {userData.id}</p>
<p>用户姓名: {userData.name}</p>
<p>用户邮箱: {userData.email}</p>
</div>
);
}
}
5. 优点
- 提高代码兼容性:可以使原本不兼容的接口能够协同工作,有助于整合旧系统和新系统、不同的库或者不同格式的数据,避免了因为接口差异而重写大量代码。
- 符合开闭原则:在不修改原有代码(被适配者)的基础上,通过创建适配器来实现新的接口需求,使得系统更容易维护和扩展。当需要接入新的不兼容的接口时,只需要创建新的适配器即可。
6. 缺点
- 增加代码复杂度:每添加一个适配器,就会增加新的类或函数,可能会使代码结构变得复杂,特别是当有多个适配器时,需要仔细管理和维护这些适配器。
- 性能开销可能增加:适配器在转换接口的过程中可能会涉及到数据的重新组织、格式转换等操作,这些额外的操作可能会带来一定的性能开销,不过在大多数前端应用场景中,这种开销通常是可以接受的。
5. 责任链模式
1. 定义与概念
- 责任链模式是一种行为设计模式。它将请求的发送者和接收者解耦,让多个对象都有机会处理这个请求。这些对象连成一条链,当有请求到来时,会沿着这条链传递,直到有一个对象处理这个请求为止。就像是一个工作流程审批,一个申请可能会经过多个审批人,每个审批人都有自己的职责范围来决定是否处理这个申请。
1. 结构组成
- 抽象处理者(Handler) :它定义了一个处理请求的接口,并且通常包含一个指向下一个处理者的引用。这个抽象类或者接口的主要方法是处理请求的方法,在方法中会判断是否自己处理请求,如果不能处理就将请求传递给下一个处理者。
- 具体处理者(Concrete Handler) :实现了抽象处理者的接口,它们是责任链中的实际节点。每个具体处理者都有自己的处理逻辑,用于判断是否处理请求。如果自己能够处理就处理,不能处理就将请求传递给下一个处理者。
1. 简单示例(JavaScript)
class AbstractHandler {
constructor() {
this.nextHandler;
}
setNext(handler) {
this.nextHandler = handler;
}
handle(request) {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
- 然后创建具体处理者,例如处理小于 10 的数字的请求:
class SmallNumberHandler extends AbstractHandler {
handle(request) {
if (request < 10) {
console.log(`SmallNumberHandler处理了请求: ${request}`);
return;
}
return super.handle(request);
}
}
- 再创建处理大于等于 10 且小于 20 的数字的请求的处理者:
class MediumNumberHandler extends AbstractHandler {
handle(request) {
if (request >= 10 && request < 20) {
console.log(`MediumNumberHandler处理了请求: ${request}`);
return;
}
return super.handle(request);
}
}
- 最后创建处理大于等于 20 的数字的请求的处理者:
class LargeNumberHandler extends AbstractHandler {
handle(request) {
if (request >= 20) {
console.log(`LargeNumberHandler处理了请求: ${request}`);
return;
}
return super.handle(request);
}
}
const smallHandler = new SmallNumberHandler()
const mediumHandler = new MediumNumberHandler()
const largeHandler = new LargeNumberHandler()
smallHandler.setNext(mediumHandler)
mediumHandler.setNext(largeHandler)
smallHandler.handle(5)
smallHandler.handle(15)
smallHandler.handle(25)
4. 在前端框架中的应用
- Vue.js 中的应用
- 在表单验证中可以使用责任链模式。例如,一个表单有多个验证规则,如非空验证、格式验证(如邮箱格式、手机号码格式)等。
- 首先定义抽象验证处理者:
class AbstractValidator {
constructor() {
this.nextValidator;
}
setNext(validator) {
this.nextValidator = validator;
}
validate(value) {
if (this.nextValidator) {
return this.nextValidator.validate(value);
}
return null;
}
}
class NonEmptyValidator extends AbstractValidator {
validate(value) {
if (value) {
return super.validate(value);
}
console.log('值不能为空');
return false;
}
}
class EmailValidator extends AbstractValidator {
validate(value) {
const emailRegex = /^[a - zA - Z0 - 9_.+-]+@[a - zA - Z0 - 9 -]+.[a - zA - Z0 - 9-.]+$/;
if (emailRegex.test(value)) {
return super.validate(value);
}
console.log('请输入正确的邮箱格式');
return false;
}
}
Vue.component('my - form', {
template: `
<div>
<input v - model="email">
<button @click="validateForm">验证表单</button>
</div>
`,
data() {
return {
email: ''
}
},
methods: {
validateForm() {
const nonEmptyValidator = new NonEmptyValidator()
const emailValidator = new EmailValidator()
nonEmptyValidator.setNext(emailValidator)
const result = nonEmptyValidator.validate(this.email)
console.log('表单验证结果:', result)
}
}
})
- React.js 中的应用
- 在事件处理中可以应用责任链模式。例如,在一个复杂的用户操作场景中,一个点击事件可能需要经过多个处理步骤,如权限验证、数据验证、操作执行等。
- 首先定义抽象事件处理者:
class AbstractEventHandler {
constructor() {
this.nextHandler;
}
setNext(handler) {
this.nextHandler = handler;
}
handleEvent(event) {
if (this.nextHandler) {
return this.nextHandler.handleEvent(event);
}
return null;
}
}
class PermissionValidatorHandler extends AbstractEventHandler {
handleEvent(event) {
const hasPermission = true;
if (hasPermission) {
return super.handleEvent(event);
}
console.log('你没有权限执行此操作');
return false;
}
}
class DataValidatorHandler extends AbstractEventHandler {
handleEvent(event) {
const validData = true;
if (validData) {
return super.handleEvent(event);
}
console.log('数据无效');
return false;
}
}
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
};
}
handleClick = (event) => {
const permissionHandler = new PermissionValidatorHandler();
const dataHandler = new DataValidatorHandler();
permissionHandler.setNext(dataHandler);
const result = permissionHandler.handleEvent(event);
console.log('事件处理结果:', result);
};
render() {
return (
<div>
<button onClick={this.handleClick}>执行操作</button>
</div>
);
}
}
5. 优点
- 解耦请求发送者和接收者:请求的发送者不需要知道具体是哪个对象来处理请求,只需要将请求发送到责任链的开头即可。这样降低了系统的耦合度,使得系统更加灵活。
- 动态组合处理者:可以根据实际情况动态地组合处理者,增加或减少处理者的数量,改变处理者的顺序等,而不需要修改请求发送者的代码。
- 增强系统的可扩展性:当需要添加新的处理规则或者功能时,只需要创建新的具体处理者并将其加入到责任链中即可,方便系统的扩展。
6. 缺点
- 性能问题:如果责任链过长,请求可能需要经过多个对象的传递和判断才能得到处理,这可能会导致性能下降,特别是在处理性能敏感的场景中需要注意。
- 调试困难:由于请求在多个对象之间传递,当出现问题时,定位具体是哪个处理者出现问题可能会比较复杂,需要仔细跟踪请求在责任链中的传递过程。
6. 策略模式
1. 定义与概念
- 享元模式是一种结构型设计模式。它主要用于减少创建对象的数量,以减少内存占用和提高性能。其核心思想是共享对象,通过共享尽可能多的对象状态,使得可以用较少的对象来表示更多的情况。
- 例如,在一个文本编辑器中,如果要显示大量的字母 “a”,这些 “a” 的外观(字体、颜色等)相同,那么可以使用享元模式,让这些相同外观的 “a” 字符共享同一个对象表示,而不是为每个 “a” 都创建一个新的对象。
2. 结构组成
- 享元(Flyweight)接口或抽象类:定义了对象的共享方法和属性。所有具体的享元类都要实现这个接口或者继承这个抽象类,这些方法用于操作对象的内部状态。
- 具体享元(Concrete Flyweight)类:实现了享元接口或抽象类,具体实现了共享方法和属性。这些类包含了可以被共享的对象状态。
- 享元工厂(Flyweight Factory)类:负责创建和管理享元对象。它维护一个享元对象的池,当需要一个享元对象时,先在池中查找是否已经存在,如果存在就直接返回,不存在则创建一个新的享元对象并放入池中。
1. 简单示例(JavaScript)
class CharacterFlyweight {
constructor() {
this.color = 'black';
}
draw() {
throw new Error('该方法需要在具体享元类中实现');
}
}
class CharacterA extends CharacterFlyweight {
draw() {
console.log(`绘制黑色的字母A`);
}
}
class CharacterFlyweightFactory {
constructor() {
this.flyweights = {};
}
getFlyweight(key) {
if (!this.flyweights[key]) {
if (key === 'A') {
this.flyweights[key] = new CharacterA();
}
}
return this.flyweights[key];
}
}
const factory = new CharacterFlyweightFactory()
const characterA1 = factory.getFlyweight('A')
const characterA2 = factory.getFlyweight('A')
characterA1.draw()
characterA2.draw()
console.log(characterA1 === characterA2)
4. 在前端框架中的应用
- Vue.js 中的应用
- 在 Vue 组件中,当需要大量相同样式的组件时可以使用享元模式。例如,一个列表中有许多相同样式的图标组件。
- 首先定义享元接口(以组件选项对象形式):
const IconFlyweight = {
props: ['iconName'],
template: '<i :class="iconClass"></i>',
computed: {
iconClass() {
return `icon-${this.iconName}`;
}
},
methods: {
draw() {
throw new Error('该方法需要在具体享元类中实现');
}
}
};
const StarIcon = {
...IconFlyweight,
draw() {
console.log('绘制星星图标');
}
};
const IconFlyweightFactory = {
icons: {},
getIcon(iconName) {
if (!this.icons[iconName]) {
if (iconName === 'star') {
this.icons[iconName] = StarIcon
}
}
return this.icons[iconName]
}
}
Vue.component('icon - list', {
template: `
<div>
<component :is="iconFactory.getIcon('star')" v - for="i in 5"></component>
</div>
`,
data() {
return {
iconFactory: IconFlyweightFactory
};
}
});
- React.js 中的应用
- 在 React 中,当渲染大量相同样式的元素时可以考虑享元模式。例如,一个页面中有很多相同颜色和大小的圆形。
- 首先定义享元接口(以函数组件形式):
const CircleFlyweight = (props) => {
const { color, size } = props;
const style = {
backgroundColor: color,
width: size,
height: size,
borderRadius: '50%'
};
return <div style={style}></div>;
};
const CircleFlyweightFactory = {
circles: {},
getCircle(color, size) {
const key = `${color}-${size}`
if (!this.circles[key]) {
this.circles[key] = CircleFlyweight({ color, size })
}
return this.circles[key]
}
}
const CircleList = () => {
const factory = CircleFlyweightFactory;
return (
<div>
{[1, 2, 3].map((i) => (
<div key={i}>
{factory.getCircle('red', '20px')}
{factory.getCircle('red', '20px')}
</div>
))}
</div>
);
};
5. 优点
- 节省内存:通过共享对象,可以大大减少对象的数量,尤其是在处理大量相似对象的场景下,能够显著节省内存空间。
- 提高性能:由于对象的创建和销毁是比较消耗资源的操作,享元模式减少了对象的创建次数,从而在一定程度上提高了性能。
- 易于维护和管理:享元对象的共享状态集中在享元类和享元工厂中管理,当需要修改共享状态时,只需要在这些地方进行修改,而不需要在大量的对象实例中逐个修改。
6. 缺点
- 增加复杂性:享元模式引入了享元工厂和对象池等概念,使得代码结构相对复杂。需要额外的代码来管理享元对象的创建、查找和共享。
- 共享状态的限制:对象的共享状态需要仔细设计和规划。如果共享状态设计不合理,可能会导致一些对象无法正确共享,或者在共享过程中出现状态冲突等问题。
- 不适合所有场景:对于那些对象状态变化频繁、对象之间差异较大的场景,享元模式可能并不适用,因为很难找到合适的共享状态来实现对象的共享。
7. 状态模式
1. 定义与概念
- 状态模式是一种行为设计模式。它允许一个对象在其内部状态改变时改变它的行为。对象看起来好像修改了它的类,实际上是通过切换不同的状态对象来实现不同的行为。这种模式将状态相关的行为封装到不同的状态类中,使得状态的转换和行为的执行更加清晰和易于维护。
- 例如,一个简单的音乐播放器,它有播放、暂停、停止等状态。在不同的状态下,用户操作(如点击按钮)会引发不同的行为,比如在播放状态下点击暂停按钮会暂停播放,在暂停状态下点击播放按钮会恢复播放。
1. 结构组成
- 上下文(Context)类:它维护一个对当前状态对象的引用,并且提供一些方法用于客户端调用。这些方法会委托给当前状态对象来执行实际的操作。上下文类通常还包含用于切换状态的方法。
- 状态(State)接口或抽象类:定义了状态相关行为的接口,所有具体的状态类都要实现这个接口或者继承这个抽象类。这些接口方法代表了在该状态下对象可以执行的操作。
- 具体状态(Concrete State)类:实现了状态接口或抽象类,每个具体状态类封装了在特定状态下的行为逻辑。当上下文对象的状态改变时,具体状态类中的方法会被调用以执行相应的行为。
1. 简单示例(JavaScript)
class PlayerState {
constructor(player) {
this.player = player;
}
play() {
throw new Error('该方法需要在具体状态类中实现');
}
pause() {
throw new Error('该方法需要在具体状态类中实现');
}
stop() {
throw新 Error('该方法需要在具体状态类中实现');
}
}
class PlayingState extends PlayerState {
play() {
console.log('已经在播放状态,无需操作');
}
pause() {
console.log('暂停播放');
this.player.setState(this.player.pausedState);
}
stop() {
console.log('停止播放');
this.player.setState(this.player.stoppedState);
}
}
class PausedState extends PlayerState {
play() {
console.log('恢复播放');
this.player.setState(this.player.playingState);
}
pause() {
console.log('已经在暂停状态,无需操作');
}
stop() {
console.log('停止播放');
this.player.setState(this.player.stoppedState);
}
}
class StoppedState extends PlayerState {
play() {
console.log('开始播放');
this.player.setState(this.player.playingState);
}
pause() {
console.log('已经停止,无法暂停');
}
stop() {
console.log('已经停止,无需操作');
}
}
class MusicPlayer {
constructor() {
this.playingState = new PlayingState(this);
this.pausedState = new PausedState(this);
this.stoppedState = new StoppedState(this);
this.currentState = this.stoppedState;
}
setState(state) {
this.currentState = state;
}
play() {
this.currentState.play();
}
pause() {
this.currentState.pause();
}
stop() {
this.currentState.stop();
}
}
const player = new MusicPlayer()
player.play()
player.pause()
player.stop()
4. 在前端框架中的应用
- Vue.js 中的应用
- 在 Vue 组件中,状态模式可以用于实现具有多种状态的交互组件。例如,一个模态框组件有显示和隐藏两种状态。
- 首先定义状态接口:
const ModalState {
constructor(modal) {
this.modal = modal;
}
show() {
throw new Error('该方法需要在具体状态类中实现');
}
hide() {
throw new Error('该方法需要在具体状态类中实现');
}
}
const ModalShownState extends ModalState {
show() {
console.log('模态框已经显示,无需操作');
}
hide() {
console.log('隐藏模态框');
this.modal.setState(this.modal.hiddenState);
}
}
const ModalHiddenState extends ModalState {
show() {
console.log('显示模态框');
this.modal.setState(this.modal.shownState);
}
hide() {
console.log('模态框已经隐藏,无需操作');
}
}
Vue.component('my - modal', {
template: `
<div v - if="isShown">模态框内容</div>
`,
data() {
return {
shownState: new ModalShownState(this),
hiddenState: new ModalHiddenState(this),
currentState: this.hiddenState,
isShown: false
};
},
methods: {
setState(state) {
this.currentState = state;
this.isShown = (state instanceof ModalShownState);
},
show() {
this.currentState.show();
},
hide() {
this.currentState.hide();
}
}
});
- React.js 中的应用
- 在 React 中,状态模式可以用于实现具有复杂状态转换的组件,如一个加载数据的组件,有加载中、加载成功、加载失败等状态。
- 首先定义状态接口(以函数形式):
const DataLoadingState = (component) => {
return {
load() {
console.log('正在加载,无需操作');
},
success() {
console.log('加载成功');
component.setState(component.successState);
},
failure() {
console.log('加载失败');
component.setState(component.failureState);
}
};
};
const DataSuccessState = (component) => {
return {
load() {
console.log('已经加载成功,无需操作');
},
success() {
console.log('已经加载成功,无需操作');
},
failure() {
console.log('数据变为失败状态');
component.setState(component.failureState);
}
};
};
const DataFailureState = (component) => {
return {
load() {
console.log('重新加载');
component.setState(component.loadingState);
},
success() {
console.log('数据变为成功状态');
component.setState(component.successState);
},
failure() {
console.log('已经失败,无需操作');
}
};
};
class DataLoadingComponent extends React.Component {
constructor(props) {
super(props);
this.loadingState = DataLoadingState(this);
this.successState = DataSuccessState(this);
this.failureState = DataFailureState(this);
this.state = {
currentState: this.loadingState
};
}
load() {
this.state.currentState.load();
}
success() {
this.state.currentState.success();
}
failure() {
this.state.currentState.failure();
}
render() {
return (
<div>
{this.state.currentState.load()}
</div>
);
}
}
5. 优点
- 状态和行为的清晰分离:将不同状态下的行为封装到不同的状态类中,使得代码结构清晰,易于理解和维护。当需要修改某个状态下的行为时,只需要在对应的状态类中进行修改,不会影响其他状态。
- 状态转换的逻辑集中管理:状态转换的逻辑在状态类和上下文类中进行集中管理,而不是分散在多个条件判断语句中。这样可以更容易地跟踪和理解状态的转换过程,减少错误。
- 符合开闭原则:当需要添加新的状态或者修改现有状态的行为时,只需要创建新的状态类或者修改现有状态类,而不需要修改使用状态的上下文类的核心逻辑,易于扩展。
5. 缺点
- 增加类的数量:每一个状态都需要一个对应的状态类,当状态数量较多时,会导致类的数量增加,使得代码结构变得复杂,增加了维护成本。
- 状态之间的通信可能复杂:在一些情况下,不同状态类之间可能需要进行通信或者共享数据,这可能会使代码变得复杂,需要仔细设计状态类之间的接口和交互方式。
8. 命令模式
1. 定义与概念
- 命令模式是一种行为设计模式。它将一个请求封装为一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作。简单来说,它把发出命令的责任和执行命令的责任分割开,将命令的请求者和执行者解耦。
- 例如,在一个图形编辑软件中,用户可以执行各种操作,如绘制图形、移动图形、删除图形等。这些操作可以被看作是命令,通过命令模式,我们可以将每个操作封装成一个命令对象,方便管理和执行。
1. 结构组成
- 命令(Command)接口或抽象类:定义了执行命令的方法,通常是一个
execute方法。这个接口或抽象类是所有具体命令类的基础,用于规范命令的执行行为。
- 具体命令(Concrete Command)类:实现了命令接口或抽象类,具体实现了
execute方法。这些类包含了执行命令所需的具体操作,并且通常会持有接收者(Receiver)对象的引用,通过接收者来执行实际的操作。
- 接收者(Receiver)类:执行具体的操作,是真正完成命令任务的对象。例如,在图形编辑软件中,接收者可能是图形绘制引擎,它负责根据命令绘制、移动或删除图形。
- 调用者(Invoker)类:负责创建和发送命令,它持有一个命令对象,并在需要的时候调用命令对象的
execute方法来执行命令。
1. 简单示例(JavaScript)
class Command {
constructor(receiver) {
this.receiver = receiver;
}
execute() {
throw new Error('该方法需要在具体命令类中实现');
}
}
class DrawRectangleCommand extends Command {
constructor(receiver, width, height) {
super(receiver);
this.width = width;
this.height = height;
}
execute() {
this.receiver.drawRectangle(this.width, this.height);
}
}
class GraphicsEngine {
drawRectangle(width, height) {
console.log(`绘制一个宽度为${width},高度为${height}的矩形`);
}
}
class CommandInvoker {
constructor(command) {
this.command = command;
}
invoke() {
this.command.execute();
}
}
const graphicsEngine = new GraphicsEngine()
const drawCommand = new DrawRectangleCommand(graphicsEngine, 100, 200)
const invoker = new CommandInvoker(drawCommand)
invoker.invoke()
4. 在前端框架中的应用
- Vue.js 中的应用
- 在 Vue 组件中,命令模式可以用于处理用户操作和业务逻辑的分离。例如,在一个表单组件中,用户提交表单的操作可以封装为一个命令。
- 首先定义命令接口:
const FormCommand {
constructor(form) {
this.form = form;
}
execute() {
throw new Error('该方法需要在具体命令类中实现');
}
}
const SubmitFormCommand extends FormCommand {
execute() {
const formData = this.form.serialize();
console.log('提交表单数据:', formData);
}
}
Vue.component('my - form', {
template: `
<form ref="myForm">
<input type="text" v - model="inputValue">
<button @click="submitForm">提交表单</button>
</form>
`,
data() {
return {
inputValue: ''
}
},
methods: {
serialize() {
return {
inputValue: this.inputValue
}
},
submitForm() {
const command = new SubmitFormCommand(this)
const invoker = new CommandInvoker(command)
invoker.invoke()
}
}
})
const CommandInvoker = {
constructor(command) {
this.command = command
}
invoke() {
this.command.execute()
}
}
- React.js 中的应用
- 在 React 中,命令模式可以用于处理用户交互事件。例如,在一个按钮组件中,点击按钮的操作可以封装为一个命令。
- 首先定义命令接口(以函数形式):
const ButtonCommand = (receiver) => {
return {
execute() {
throw new Error('该方法需要在具体命令类中实现');
}
};
};
const ShowMessageCommand = (receiver, message) => {
return {
execute() {
console.log(message);
}
};
};
const MessageDisplay = () => {
return (
<div>
<button onClick={() => {
const command = ShowMessageCommand(this, '按钮被点击')
const invoker = new CommandInvoker(command)
invoker.invoke()
}}>显示消息</button>
</div>
)
}
const CommandInvoker = {
constructor(command) {
this.command = command
}
invoke() {
this.command.execute()
}
}
5. 优点
- 解耦请求者和执行者:命令模式将请求的发送者(调用者)和请求的执行者(接收者)分离,使得两者可以独立变化。这样可以更容易地修改、扩展或替换请求的执行逻辑,而不会影响到请求的发送部分。
- 可扩展性强:可以方便地添加新的命令,只需要创建新的具体命令类并实现
execute方法即可。这对于系统的功能扩展非常有利,例如在图形编辑软件中添加新的图形绘制命令。
- 支持命令的排队、记录和撤销:由于命令被封装为对象,可以方便地对命令进行排队执行,记录命令的执行历史,以及实现撤销和重做操作。这在一些需要复杂操作管理的系统中非常有用。
6. 缺点
- 增加代码复杂度:命令模式引入了较多的类和对象,如命令类、接收者类、调用者类等,这可能会使代码结构变得复杂,尤其是对于简单的应用场景,可能会增加不必要的复杂性。
- 可能导致性能问题:在一些情况下,频繁地创建和销毁命令对象可能会导致性能下降。例如,在一个高性能要求的实时系统中,如果大量使用命令模式且没有合理的对象管理,可能会影响系统性能。