设计原则
- 开闭原则:对扩展开放,对修改关闭。
- 依赖倒置原则:高层不应该依赖底层,要面向接口编程。
- 单一职责原则:一个类只做一件事,实现类要单一。
- 接口隔离原则:一个接口只做一件事,接口要精简单一。
- 迪米特法则:不该知道的不要知道,一个类应该保持对其它对象最少的了解,降低耦合度。
- 里氏替换原则:不要破坏继承体系,子类重写方法功能发生改变,不应该影响父类方法的含义。
- 合成复用原则:尽量使用组合或者聚合关系实现代码复用,少使用继承。
创建型模式
创建型模式包含单例模式、简单工厂模式、工厂方法模式、抽象工厂模式、原型模式、建造者模式。 创建型模式就是创建对象的模式,抽象了实例化的过程。它帮助一个系统独立于如何创建、组合和表示它的那些对象。关注的是对象的创建,创建型模式将创建对象的过程进行了抽象,也可以理解为将创建对象的过程进行了封装,作为客户程序仅仅需要去使用对象,而不再关心创建对象过程中的逻辑。
工厂模式
工厂模式的定义:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。 按实际业务场景划分,工厂模式有 3 种不同的实现方式,分别是简单工厂模式、工厂方法模式和抽象工厂模式。
简单工厂
我们把被创建的对象称为“产品”,把创建产品的对象称为“工厂”。如果要创建的产品不多,只要一个工厂类就可以完成,这种模式叫“简单工厂模式”。
简单工厂模式包含如下角色:
- Factory:工厂角色: 工厂角色负责实现创建所有实例的内部逻辑
- Product:抽象产品角色:抽象产品角色是所创建的所有对象的父类,负责描述所有实例所共有的公共接口
- ConcreteProduct:具体产品角色:具体产品角色是创建目标,所有创建的对象都充当这个角色的某个具体类的实例 应用场景:
- 工厂类负责创建的对象较少,客户端对如何创建对象不关心
abstract class Coffee {
constructor(public name: string) {}
}
class AmericanCoffee extends Coffee {}
class LatteCoffee extends Coffee {}
class CappuccinoCoffee extends Coffee {}
// 简单工厂
class CoffeeFactory {
static order(name: string) {
switch (name) {
case 'AmericanCoffee':
return new AmericanCoffee('AmericanCoffee')
case "LatteCoffee":
return new LatteCoffee("LatteCoffee")
case "CappuccinoCoffee":
return new CappuccinoCoffee("CappuccinoCoffee")
default:
throw new Error("no coffee");
}
}
}
console.log(CoffeeFactory.order('AmericanCoffee'))
console.log(CoffeeFactory.order('LatteCoffee'))
console.log(CoffeeFactory.order('CappuccinoCoffee'))
优点:
- 客户类与具体子类解耦
- 客户类不需要知道所有子类的细节 缺点:
- 工厂类职责过重
- 增加工厂类增加了系统复杂度
- 系统扩展困难(会修改工厂逻辑)
案例1
interface Jquery {
[index: number]: any
}
class Jquery {
public length: number
constructor(selector: string) {
let elements = Array.from(document.querySelectorAll(selector))
let length = elements ? elements.length : 0
this.length = length
for (let i = 0; i < elements.length; i++) {
this[i] = elements[i]
}
}
html(htmlText: string | undefined) {
if (htmlText) {
for (let i = 0; i < this.length; i++) {
this[i].innerHtml = htmlText
}
} else {
return this[0]
}
}
}
interface Window {
$: any
}
// 简单工厂就是函数里返回实例
window.$ = function(selector: string) {
return new Jquery(selector)
}
工厂方法
在工厂方法模式中,核心的工厂类不再负责所有产品的创建,而是将具体创建工作交给子类去做。这个核心类仅仅负责给出具体工厂必须实现的接口,而不负责哪一个产品类被实例化这种细节,这使得工厂方法模式可以允许系统在不修改工厂角色的情况下引进新产品。
工厂方法模式的主要角色如下。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法 newProduct() 来创建产品。
- 具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一一对应。
应用场景:
- 客户只知道创建产品的工厂名,而不知道具体的产品名。如 TCL 电视工厂、海信电视工厂等。
- 创建对象的任务由多个具体子工厂中的某一个完成,而抽象工厂只提供创建产品的接口。
- 客户不关心创建产品的细节,只关心产品的
export {}
abstract class Coffee {
constructor(public name: string) {}
}
class AmericanCoffee extends Coffee {}
class LatteCoffee extends Coffee {}
class CappuccinoCoffee extends Coffee {}
abstract class CoffeeFactory {
abstract createCoffee(): Coffee
}
class AmericanCoffeeFactory extends CoffeeFactory {
createCoffee() {
return new AmericanCoffee('AmericanCoffee')
}
}
class LatteCoffeeFactory extends CoffeeFactory {
createCoffee() {
return new LatteCoffee('LatteCoffee')
}
}
class CappuccinoCoffeeFactory extends CoffeeFactory {
createCoffee() {
return new CappuccinoCoffee('CappuccinoCoffee')
}
}
let americanCoffeeFactory = new AmericanCoffeeFactory
console.log(americanCoffeeFactory.createCoffee())
function createElement (type: any, config: any) {
return { type, props: config }
}
function createFactory (type: string) {
const factory = createElement.bind(null, type)
return factory
}
let factory = createFactory('h1')
let element = factory({ type: "h2", className: "title" })
console.log(element)
function createElement2 (type: string) {
return (config: any) => {
return {
type,
props: config
}
}
}
console.log(createElement2('h1')({ type: "h2", className: "title"}))
优点:
- 用户只需关心产品对应的工厂。
- 添加新产品是只要添加一个具体的工厂和具体产品(符合开闭原则) 缺点:
- 类个数成倍增加(增加一个产品会增加具体类和实现工厂)
抽象工厂
抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,属于对象创建型模式。
抽象工厂模式的主要角色如下。
- 抽象工厂(Abstract Factory):提供了创建产品的接口,它包含多个创建产品的方法 newProduct(),可以创建多个不同等级的产品。
- 具体工厂(Concrete Factory):主要是实现抽象工厂中的多个抽象方法,完成具体产品的创建。
- 抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能,抽象工厂模式有多个抽象产品。
- 具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间是多对一的关系。
应用场景: 无需关心对象创建过程,系统有多于一个的产品族且每次只需某一个产品族、产品等级结构稳定。
abstract class AmericanCoffee {}
abstract class LatteCoffee {}
abstract class CappuccinoCoffee {}
class StarbucksAmericanCoffee extends AmericanCoffee {}
class LinkinAmericanCoffee extends AmericanCoffee {}
class StarbucksLatteCoffee extends LatteCoffee {}
class LinkinLatteCoffee extends LatteCoffee {}
class StarbucksCappuccinoCoffee extends CappuccinoCoffee {}
class LinkinCappuccinoCoffee extends CappuccinoCoffee {}
abstract class CoffeeFactory {
abstract createAmericanCoffee(): AmericanCoffee
abstract createLatteCoffee(): LatteCoffee
abstract createCappuccinoCoffee(): CappuccinoCoffee
}
class StarbucksCoffeeFactory extends CoffeeFactory {
createAmericanCoffee(): AmericanCoffee {
return new StarbucksAmericanCoffee()
}
createLatteCoffee(): LatteCoffee {
return new StarbucksLatteCoffee()
}
createCappuccinoCoffee(): CappuccinoCoffee {
return new StarbucksCappuccinoCoffee()
}
}
class LinkinCoffeeFactory extends CoffeeFactory {
createAmericanCoffee(): AmericanCoffee {
return new LinkinAmericanCoffee()
}
createLatteCoffee(): LatteCoffee {
return new LinkinLatteCoffee()
}
createCappuccinoCoffee(): CappuccinoCoffee {
return new LinkinCappuccinoCoffee()
}
}
let linkinFactory = new LinkinCoffeeFactory()
console.log(linkinFactory.createAmericanCoffee())
优点:
隔离了具体类的生成,使得客户并不需要知道什么被创建,而且每次可以通过具体工厂类创建一个产品族中的多个对象,增加或者替换产品族比较方便,增加新的具体工厂和产品族很方便;
缺点:
在于增加新的产品等级结构很复杂,需要修改抽象工厂和所有的具体工厂类,对“开闭原则”的支持呈现倾斜性。
单例模式
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
单例模式的主要角色如下。
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。 应用场景:
- 需要频繁进行创建和销毁对象,创建对象耗时或消耗资源过多,但又经常用到。
普通单例
// ES6 实现单例
class Window {
private static instance: Window
private constructor() {
}
public static getInstance() {
if(!Window.instance) {
Window.instance = new Window()
}
return Window.instance
}
}
let w1 = Window.getInstance()
let w2 = Window.getInstance()
// ES5 实现单例
export {}
// 通过 es5 实现单例
function Window() {}
Window.prototype.hello = function () {
console.log('hello')
}
Window.getInstance = (function () {
let window: Window
return function () {
if (!window) {
window = new (Window as any)()
}
return window
}
})()
let w1 = Window.getInstance()
let w2 = Window.getInstance()
console.log(w1 === w2)
这个单例实现获取对象的方式经常见于新手的写法,这种方式获取对象虽然简单,但是这种实现方式不透明。知道的人可以通过 Window.getInstance() 获取对象,不知道的需要研究代码的实现,这样不好。这与我们常见的用 new 关键字来获取对象有出入,实际意义不大。
透明的单例模式
let Window = (function() {
let window:Window
let Window = function(this: Window) { // 构造函数: 构造函数中的this指向构造函数的实例
if (window) {
return window
} else {
return window = this
}
}
return Window
})()
let w1 = new (Window as any)()
let w2 = new (Window as any)()
console.log(w1 === w2)
透明单例模式可以像平常一样通过 new 关键字来创建实例。
代理单例模式
自己不去做,委托 createInstance 去创建单例
export {}
// 通过 es5 实现单例
function Window() {
}
// 希望通过 createInstance 方法创建任何实例
let createInstance = function(constructor: any) {
let instance: any
return function(this: any) {
if (!instance) {
constructor.apply(this, arguments);
// this.__proto__ = constructor.protoType;
Object.setPrototypeOf(this, constructor.prototype);
instance = this;
}
return instance;
}
}
let createWindow = createInstance(Window)
let w1 = new (createWindow as any)()
let w2 = new (createWindow as any)()
console.log(w1 === w2)
优点:
- 单例模式可以保证内存里只有一个实例,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
原型模式
用原型实例指定创建对象的种类,并且通过拷贝这些原型,创建新的对象。创建基类的时候,简单差异化的属性放在构造函数中,消耗资源相同的功能放在基类原型中。
原型模式包含以下主要角色。
- 抽象原型类:规定了具体原型对象必须实现的接口。
- 具体原型类:实现抽象原型类的 clone() 方法,它是可被复制的对象。
- 访问类:使用具体原型类中的 clone() 方法来复制新的对象。 应用场景:
- 创建成本比较大的场景
- 需要动态获取当前对象运行状态的场景
function Person(name) {
this.name = name
}
Person.prototype.getName = function() {
console.log(this.name)
}
let p1 = new Person('aa')
let p2 = new Person('bb')
console.log(p1.getName === p2.getName)
ts实现原型模式
interface Prototype {
clone() : Prototype
}
class Dog implements Prototype {
public name: string;
public birthYear: number;
public sex: string;
public presentYear: number
constructor() {
this.name = 'lili';
this.birthYear = 2021
this.sex = '男'
this.presentYear = 2022
}
public getDiscription(): string{
return `姓名:${this.name},性别:${this.sex}`
}
// 实现复制
public clone(): Prototype {
return Object.create(this)
}
}
// 使用
const dog = new Dog()
console.log(dog.getDiscription())
dog.presentYear = 2019
const dog1 = Object.create(dog)
console.log(dog1.getDiscription())
优点
-
简化创建对象的过程并提高效率。
-
可动态获取对象运行时的状态。
-
原始对象变化(增减属性)相应的克隆对象也会有变化。 缺点
-
对已有类修改时,需要修改i源码,违背了ocp原则。
结构型模式
结构型模式包含适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。结构型模式为解决怎样组装现有的类,设计他们的交互方式,从而达到实现一定的功能。
适配器模式
适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。 适配器模式包含如下角色:
适配器模式(Adapter)包含以下主要角色。
- 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象类或接口。
- 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
- 适配器(Adapter)类:它是一个转换器,通过继承或引用适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。
- Client:客户类
应用场景:
- 类适配器和对象适配器:接口不符合规范,通过适配后变成符合规范的接口进行使用。
- 接口适配器:适用于一个接口不想使用其所有的方法的情况。
// 适配器模式又称包装器模式,将一个类的接口转化为 用户需要的另一个接口,解决类(对象)之间接口的不兼容问题
// 旧的接口和使用者不兼容
// 中间加一个适配器转换接口
// 需要被适配的类
class Socket {
output() {
return '220v'
}
}
abstract class Power{
abstract charge(): string
}
class PowerAdapter extends Power{
constructor(public socket: Socket) {
super()
}
charge() {
return this.socket.output() + '转换为 24v'
}
}
let adapter = new PowerAdapter(new Socket())
console.log(adapter.charge())
优点
-
让两个没有关系的类一起使用。
-
提高类的复用性(源角色在原有系统还可以使用) 缺点
-
类适配器:不支持多重继承语言一次只能适配一个被适配的类而且目标抽象类只能为接口,有一定的局限性;被适配者类的方法在 Adapter 中都会暴露出来。
-
对象适配器:与类适配器模式相比,要想置换被适配类的方法就不容易。
案例1
项目中的 jQuery 发送请求全部转换成 axios 请求
export {}
let $ = require('jquery')
let axios = require('axios')
// 适配 $.ajax
function toAxiosAdapter(options) {
return axios({
url: options.url,
method: options.type
}).then(options.success, options.error)
}
$.ajax = function(options) {
return toAxiosAdapter(options)
}
// 适配者类
$.ajax({
url: "http://localhost:8080/api/user",
type: "get",
success() {
},
error() {
}
})
案例2
使用 promise 封装读取文件方法
export{}
let fs = require('fs')
// 读取文件
fs.readFile('./4-adapterObj/1.txt', 'utf8', function(err, data){
console.log(data)
})
// 使用 promise 适配读取文件
function readFilePromiseAdapter(...args) {
return new Promise(function(resolve){
fs.readFile(...args, function(err, data){
resolve(data)
})
})
}
(async function () {
let content = await readFilePromiseAdapter('./4-adapterObj/1.txt', 'utf8')
console.log(content)
})()
// 封装适配器
function promiseAdapter(callableFn) {
return function(...args) {
return new Promise(function(resolve, reject) {
callableFn(...args, function(err, data){
err ? reject(err) : resolve(data)
})
})
}
}
let readFile = promiseAdapter(fs.readFile)
(async function () {
let content = await readFile('./4-adapterObj/1.txt', 'utf8')
console.log(content)
})()
桥接模式
桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。
桥接(Bridge)模式包含以下主要角色。
- 抽象化(Abstraction)角色:定义抽象类,并包含一个对实现化对象的引用。
- 扩展抽象化(Refined Abstraction)角色:是抽象化角色的子类,实现父类中的业务方法,并通过组合关系调用实现化角色中的业务方法。
- 实现化(Implementor)角色:定义实现化角色的接口,供扩展抽象化角色调用。
- 具体实现化(Concrete Implementor)角色:给出实现化角色接口的具体实现。
应用场景: 当一个系统不希望使用继承或因为多层次继承导致系统类的个数急剧增加时。如JDBC驱动程序、银行转账系统(转账分类和转账用户类型)、消息管理(消息类型、消息分类)
class A {
constructor(bridge) {
this.position = 'A地点'
this.bridge = bridge
}
go() {
console.log(`从${this.from()}到达${this.bridge.to()}`)
}
from() {
throw new Error('子类必须实现此方法')
}
}
class A1 extends A {
from() {
return 'A1'
}
}
class A2 extends A {
from() {
return 'A2'
}
}
class B {
to() {
throw new Error('子类必须实现此方法')
}
}
class B1 extends B {
to() {
return 'B1'
}
}
class B2 extends B {
to() {
return 'B2'
}
}
let b2 = new B2()
let a1 = new A1(b2)
a1.go()
优点:
- 实现了抽象和实现部分分离,提高了系统的灵活性。
- 替代了多层继承方案,减少了子类的个数。
缺点: 增加了系统的理解和设计模式;要求正确识别出系统中两个独立变化的维度,使用具有一定的局限性。
案例1
点击 li 标签获取数据,并展示到页面
// 服务端
const express = require('express')
const path = require('path')
const app = express()
app.get('/', function (req, res) {
res.sendFile(path.join(__dirname, '3.html'))
})
app.get('/user/:id', function (req, res) {
const id = req.params.id
console.log('id', id)
res.json({
id,
name: `user${id}`
})
})
app.listen(8080)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li data-id='1'>用户1</li>
<li data-id='2'>用户2</li>
</ul>
<p id="content"></p>
<script>
const p = document.getElementById('content')
let lis = document.querySelectorAll('li')
for (let i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', getUserById)
}
function getUserById(event) {
let id = event.target.dataset.id
let xhr = new XMLHttpRequest
xhr.open('GET', `/user/${id}`,true)
xhr.responseType = 'json'
xhr.onreadystatechange = function(a, b) {
let user = xhr.response
if (xhr.readyState == 4 && xhr.status === 200) {
p.innerHTML = user.name
}
}
xhr.send()
}
</script>
</body>
</html>
使用 桥接模式 优化
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li data-id='1'>用户1</li>
<li data-id='2'>用户2</li>
</ul>
<p id="content"></p>
<script>
// 改造 2.html
const p = document.getElementById('content')
let lis = document.querySelectorAll('li')
for (let i = 0; i < lis.length; i++) {
lis[i].addEventListener('click', getUserByIdBridge)
}
function getUserByIdBridge() {
let id = this.dataset.id
getUserById(id, function(user) {
p.innerHTML = user.name
})
}
function getUserById(id, callback) {
// let id = event.target.dataset.id
let xhr = new XMLHttpRequest
xhr.open('GET', `/user/${id}`,true)
xhr.responseType = 'json'
xhr.onreadystatechange = function(a, b) {
let user = xhr.response
if (xhr.readyState == 4 && xhr.status === 200) {
// p.innerHTML = user.name
callback(user)
}
}
xhr.send()
}
</script>
</body>
</html>
案例2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
*{margin: 0;padding: 0;}
#canvas {
border: 1px solid #ccc;
}
</style>
</head>
<body>
<canvas id="canvas" width="1000" height="600"></canvas>
<script>
function Position(x, y) {
this.x = x
this.y = y
}
function CircleColor(color) {
this.color = color
}
function Circle(x, y, circleColor) {
this.position = new Position(x, y)
this.circleColor = new CircleColor(circleColor)
}
Circle.prototype.render = function () {
let canvas = document.getElementById('canvas')
let cvsCtx = canvas.getContext('2d')
cvsCtx.beginPath();
cvsCtx.arc(this.position.x, this.position.y, 100, 0, 2*Math.PI, false);
cvsCtx.fillStyle = this.circleColor.color;
cvsCtx.fill();
}
let c = new Circle(100, 100, 'red')
c.render()
</script>
</body>
</html>
装饰模式
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。
装饰器模式主要包含以下角色。
- 抽象构件(Component)角色:定义一个抽象接口以规范准备接收附加责任的对象。
- 具体构件(ConcreteComponent)角色:实现抽象构件,通过装饰角色为其添加一些职责。
- 抽象装饰(Decorator)角色:继承抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
- 具体装饰(ConcreteDecorator)角色:实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。
应用场景:
- 需要扩展一个类的功能,或给一个类增减附加功能。
- 需要动态地给一个对象增加功,这些功能可以再动态地撤销。
- 需要增加由一些基本功能的配列组合而产生的非常大量的功能。
// 在不改变原有的结构和功能为对象添加新功能的模式其实就是装饰器模式
// 装饰比继承更加的灵活,可以实现装饰者和被装饰者之间的松耦合
// 被装饰者可以使用装饰者动态地增加和 撤销功能
export {}
// 抽象构件
abstract class Shape {
abstract draw() :void
}
// 具体构件
class Circle extends Shape {
draw(): void {
console.log('绘制圆形')
}
}
class Rectangle extends Shape {
draw(): void {
console.log('绘制矩形')
}
}
// 抽象 装饰角色
abstract class ColorfulShape extends Shape {
constructor(public shape: Shape){
super()
}
}
// 具体装饰角色
class RedColorfulShape extends ColorfulShape {
draw(): void {
this.shape.draw()
console.log('绘制边框为red')
}
}
class GreenColorfulShape extends ColorfulShape {
draw(): void {
this.shape.draw()
console.log('绘制边框为green')
}
}
let redColorfulShape = new RedColorfulShape(new Circle())
redColorfulShape.draw()
优点
- 装饰类和被装饰类可以独立发展,而不会耦合。
- 装饰模式是继承关系的一个替代方案,但是装饰者模式比继承更加灵活。
- 装饰模式可以动态地扩展一个实现类地功能。 缺点
多层地装饰是比较复杂地,由于装饰者模式会导致设计中出现许多小对象,过度使用会让 程序变地更复杂。
案例1
在表单提交之前添加表单字段校验
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
用户名:<input type="text" id="username">
密码:<input type="text" id="password">
<button id="register">注册</button>
<script>
Function.prototype.before = function (beforeFn) {
let thisFn = this
console.log(this)
return () => {
let pass = beforeFn()
if (pass) {
thisFn.apply(this, arguments)
}
}
}
function registerFn(event) {
console.log('提交表单')
}
registerFn = registerFn.before(function () {
let username = document.getElementById('username').value
if (!username) {
return alert('用户名不能为空')
}
return true
})
let registerBtn = document.getElementById('register')
registerBtn.addEventListener('click', registerFn)
</script>
</body>
</html>
组合模式
组合(Composite Pattern)模式的定义:有时又叫作整体-部分(Part-Whole)模式,它是一种将对象组合成树状的层次结构的模式,用来表示“整体-部分”的关系,使用户对单个对象和组合对象具有一致的访问性,属于结构型设计模式。
组合模式包含以下主要角色。
- 抽象构件(Component)角色:它的主要作用是为树叶构件和树枝构件声明公共接口,并实现它们的默认行为。在透明式的组合模式中抽象构件还声明访问和管理子类的接口;在安全式的组合模式中不声明访问和管理子类的接口,管理工作由树枝构件完成。(总的抽象类或接口,定义一些通用的方法,比如新增、删除)
- 树叶构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
- 树枝构件(Composite)角色 / 中间构件:是组合中的分支节点对象,它有子节点,用于继承和实现抽象构件。它的主要作用是存储和管理子部件,通常包含 Add()、Remove()、GetChild() 等方法。
应用场景:
- 需要遍历组织结构或者处理的对象具有树形结构时,非常适合使用组合模式。
- 维护和展示部分-整体关系的模式,如树形菜单、文件和文件夹管理。
- 一句话就是组合模式是应树形结构而生,所以组合模式的使用场景就是出现树形结构的地方。
abstract class Component {
protected name: string
constructor(name: string) {
this.name = name
}
public abstract doOperation(): void
public add(component: Component): void{}
public remove(component: Component): void{}
public getChildren(): Array<Component> {
return []
}
}
class Composite extends Component{
//
private componentList: any
constructor(name: string) {
super(name)
this.componentList = []
}
public doOperation():void{
console.log(`这是容器${this.name}, 处理一些业务逻辑`)
}
public add(component: Component): void {
this.componentList.push(component)
}
public remove(component: Component): void {
const componentIndex = this.componentList.findIndex((value: Component) => {
return value === component
})
this.componentList.splice(componentIndex, 1)
}
public getChildren(): Array<Component> {
return this.componentList
}
}
class Leaf extends Component{
constructor(name: string) {
super(name)
}
public doOperation(): void{
console.log(`这是叶子节点${this.name}, 处理一些逻辑业务!`)
}
}
const root: Component = new Composite('root')
const node1: Component = new Leaf('1')
const node2: Component = new Composite('2')
const node3: Component = new Leaf('3')
root.add(node1)
root.add(node2)
root.add(node3)
const children1 = root.getChildren()
console.log(children1)
/*
[
Leaf { name: '1' },
Composite { name: '2', componentList: [] },
Leaf { name: '3' }
]
*/
const node2_1: Component = new Leaf('2_1')
node2.add(node2_1)
const children2 = root.getChildren()
console.log(children2)
/*
[
Leaf { name: '1' },
Composite { name: '2', componentList: [ [Leaf] ] },
Leaf { name: '3' }
]
*/
root.remove(node2)
const children3 = root.getChildren()
console.log(children3)
/*
[ Leaf { name: '1' }, Leaf { name: '3' } ]
*/
优点:
- 组合模式使得客户端代码可以一致地处理单个对象和组合对象,无须关心自己处理的是单个对象,还是组合对象,这简化了客户端代码;
- 更容易在组合体内加入新的对象,客户端不会因为加入了新的对象而更改源代码,满足“开闭原则”;
缺点:
- 设计较复杂,客户端需要花更多时间理清类之间的层次关系;
- 不容易限制容器中的构件;
- 不容易用继承的方法来增加构件的新功能;
案例1
react jsx 代码展示
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">
</div>
<script>
class ReactElement{
constructor(type, props) {
this.type = type
this.props = props
}
}
function createElement(type, props = {}, ...children) {
return new ReactElement(type, { ...props, children: children.length == 1 ? children[0] : children })
}
let rootElement = createElement('span', null, 'hello', createElement('span', null, ' world', createElement('span', null, ' !')))
function render(element, container) {
console.log(element)
if(typeof element === 'string') return container.appendChild(document.createTextNode(element))
let { type, props } = element
let domElement = document.createElement(type) // 创建一个真实的dom节点
for (const attr in props) {
if (attr === 'children') {
// children 可能为 对象 string 数组
if (typeof props[attr] === 'object') {
props[attr].forEach(item => {
if (typeof item === 'object') {
render(item, domElement)
}else {
domElement.appendChild(document.createTextNode(item))
}
})
}else {
domElement.appendChild(document.createTextNode(props[attr]))
}
}else if(attr === 'className') {
domElement.setAttribute('class', props[attr])
}else {
domElement.setAttribute(attr, props[attr])
}
}
container.appendChild(domElement)
}
// ReactDOM.render(<span>hello<span>world</span></span>, document.getElementById('root'))
render(rootElement, document.getElementById('root'))
</script>
</body>
</html>
案例2
文件夹案例
function Folder(name) {
this.name = name
this.children = []
this.parent = null
}
Folder.prototype.add = function(child) {
child.parent = this
this.children.push(child)
}
Folder.prototype.show = function () {
console.log('文件夹: ', this.name)
for (let i = 0; i < this.children.length; i++) {
this.children[i].show()
}
}
Folder.prototype.remove = function() {
if (!this.parent) {
return
}else {
let children = this.parent.children
for (let i = 0; i < children.length; i++) {
if (children[i] === this) {
children.splice(i, 1)
return
}
}
}
}
function File(name) {
this.name = name
}
File.prototype.add = function(child) {
throw new Error('文件下不能新增文件或文件夹')
}
File.prototype.show = function () {
console.log('文件:', this.name)
}
File.prototype.remove = function() {
if (!this.parent) {
return
}else {
let children = this.parent.children
for (let i = 0; i < children.length; i++) {
if (children[i] === this) {
children.splice(i, 1)
return
}
}
}
}
let video = new Folder('video')
let vue = new Folder('vue')
let react = new Folder('react')
let vuejs = new File('vue.js')
let reactjs = new File('react.js')
vue.add(vuejs)
react.add(reactjs)
video.add(vue)
video.add(react)
video.show()
console.log('---------------------------')
react.remove()
video.show()
代理模式
代理模式(Proxy Pattern) :给某一个对象提供一个代理,并由代理对象控制对原对象的引用。代理模式的英 文叫做Proxy或Surrogate,它是一种对象结构型模式。
代理模式的主要角色如下。
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。
应用场景:
- 远程代理(Remote Proxy):控制对远程对象地访问,它负责将请求及其参数进行编码,并向不同地址空间中地对象发送已经编码地请求。
- 虚拟代理(Virtual Proxy):根据需要创建开销很大地对象,它可以缓存实体地附件信息,以 便延迟对它地访问,例如图片懒加载、预加载。
- 智能代理(Smart Reference):取代了简单地指针,它在访问对象时执行一些附加操作,记录对象地引用次数,当第一次引用一个对象时,将它装入内存;在访问一个实际对象前,检查是否已经锁定了它,以确保其它对象不能改变它。
// 代理模式
// 由于一个对象不能直接引用另一个对象,所以需要通过代理对象在这两个对象之间起到中介的作用
// 代理模式就是为目标对象创造一个代理对象,以实现对目标对象大的访问
// 这样就可以在代理对象里添加一些逻辑,调用前和调用后执行一些操作,从而实现了扩展目标的功能
abstract class Star {
abstract answerPhone() : void
}
class Andy extends Star{
available: boolean = true
answerPhone() :void{
console.log('你好,我是 andy!')
}
}
class AndyAgent extends Star{
constructor(private andy: Andy) {
super()
}
answerPhone(): void {
console.log('你好,我是xxx的经纪人')
if (this.andy.available) {
this.andy.answerPhone()
}
}
}
let andyAgent = new AndyAgent(new Andy())
andyAgent.answerPhone()
优点
在不修改目标对象功能地前提下,能通过代理对象对目标功能扩展。
缺点
- 因为代理对象 需要与目标对象实现一样地接口,所以会有很多代理类。
- 一旦接口增加方法目标对象与代理对象都要维护。
- 增加代理类之后明显会增加处理时间,拖慢处理时间。
案例1
图片预加载
<!-- 页面 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.bg-container{
width: 200px;
height: 600px;
margin: 100px auto;
}
.bg-image{
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="background">
<button data-src="/image/bg1.webp">bg1</button>
<button data-src="/image/bg2.webp">bg2</button>
<button data-src="/image/bg3.webp">bg3</button>
</div>
<div class="bg-container">
<img src="/image/bg1.webp" alt="" id="bg-image">
</div>
<script>
let background = document.getElementById('background')
class BackgroundImage {
constructor() {
this.bgImg = document.getElementById('bg-image')
}
setSrc(src) {
this.bgImg.src = src
}
}
class LoadingBackgroundImage {
static LOADING_URL = '/image/loading.gif'
constructor() {
this.backgroundImage = new BackgroundImage()
}
setSrc(src) {
this.backgroundImage.setSrc(LoadingBackgroundImage.LOADING_URL)
let img = new Image()
img.onload = () => {
this.backgroundImage.setSrc(src)
}
img.src = src
}
}
let loadingBackgroundImage = new LoadingBackgroundImage()
background.addEventListener('click', (event) => {
let src = event.target.dataset.src
loadingBackgroundImage.setSrc(src)
})
</script>
</body>
</html>
// 服务器代码
let path = require('path')
console.log(path.join(__dirname, '../image'));
let express = require('express')
let app = express()
app.get('/image/loading.gif', (req, res) => {
res.sendFile(path.join(__dirname, '../image', 'loading.gif'))
})
app.get('/image/:name', (req, res) => {
setTimeout(() => {
res.sendFile(path.join(__dirname, '../', req.path))
}, 3000);
})
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '2.preload.html'))
})
app.listen(8080)
案例2
图片懒加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.image{
width: 300px;
height: 600px;
background-color: #eee;
}
.image img{
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div class="image-container">
<div class="image">
<img data-src="/image/bg1.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg2.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg3.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg1.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg2.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg3.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg1.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg2.webp" alt="">
</div>
<div class="image">
<img data-src="/image/bg3.webp" alt="">
</div>
</div>
<script>
const images = document.getElementsByTagName('img')
let clientHeight = window.innerHeight || document.documentElement.clientHeight
function lazyLoad() {
for (let i = 0; i < images.length; i++) {
console.log(images[i].getBoundingClientRect().top , clientHeight)
if (images[i].getBoundingClientRect().top < clientHeight) {
images[i].src = images[i].dataset.src
}
}
}
lazyLoad()
window.addEventListener('scroll', lazyLoad, false)
</script>
</body>
</html>
// 服务端
let path = require('path')
console.log(path.join(__dirname, '../image'));
let express = require('express')
let app = express()
app.get('/image/loading.gif', (req, res) => {
res.sendFile(path.join(__dirname, '../image', 'loading.gif'))
})
app.get('/image/:name', (req, res) => {
setTimeout(() => {
res.sendFile(path.join(__dirname, '../', req.path))
}, 3000);
})
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '3.imageLazyLoading.html'))
})
app.listen(8080)
案例3
缓存代理
// 阶乘
const factorial = function(num) {
console.log('正在进行计算阶乘')
if(num === 1)
return 1
else
return num * factorial(num - 1)
}
const proxy = function(fn) {
const cache = {}
return function(num) {
if (num in cache) {
return cache[num]
}
return cache[num] = fn(num)
}
}
let proxyFactorial = proxy(factorial)
console.log(proxyFactorial(5))
console.log(proxyFactorial(5))
console.log(proxyFactorial(5))
console.log(proxyFactorial(5))
案例4
节流
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#container{
width: 200px;
height: 400px;
border: 1px solid red;
overflow: auto;
}
#container .content{
height: 4000px;
}
</style>
</head>
<body>
<!--
节流:是在某段时间内不管触发了多少次回调都只认第一个,并在第一次结束后执行回调
-->
<div id="container">
<div class="content"></div>
</div>
<script>
let container = document.getElementById('container')
let lastTime = Date.now()
function throttle(callback, interval) {
let lastExecuteTime;
return function() {
let context = this
let args = Array.from(arguments)
let now = Date.now()
if (lastExecuteTime) {
if (now - lastExecuteTime > interval) {
callback.apply(context, args)
lastExecuteTime = now
}
}else {
callback.apply(context, args)
lastExecuteTime = now
}
}
}
const scrollEvent = (event) => {
let nowDate = Date.now()
console.log('触发了滚动事件', (nowDate - lastTime) / 1000)
lastTime = nowDate
}
container.addEventListener('scroll', throttle(scrollEvent, 1000))
</script>
</body>
</html>
案例5
防抖
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
#container{
width: 200px;
height: 400px;
border: 1px solid red;
overflow: auto;
}
#container .content{
height: 4000px;
}
</style>
</head>
<body>
<!--
防抖:
-->
<div id="container">
<div class="content"></div>
</div>
<script>
let container = document.getElementById('container')
let lastTime = Date.now()
function debounce(callback, delay) {
let timer
return function() {
let context = this
let args = arguments
if (timer) {
clearInterval(timer)
timer = setTimeout(() => {
callback.apply(context, args)
}, delay)
}
}
}
const scrollEvent = (event) => {
let nowDate = Date.now()
console.log('触发了滚动事件', (nowDate - lastTime) / 1000)
lastTime = nowDate
}
container.addEventListener('scroll', debounce(scrollEvent, 1000))
</script>
</body>
</html>
案例6
代理服务器
// 源服务器
let http = require('http')
let server = http.createServer((req, res) => {
res.end('9999')
})
server.listen(9999, () => console.log('9999'))
// 代理服务器
let http = require('http')
const httpProxy = require('http-proxy')
const proxy = httpProxy.createProxyServer()
let server = http.createServer((req, res) => {
proxy.web(req, res, {
target: 'http://127.0.0.1:9999'
})
})
server.listen(8888, () => console.log('8888'))
外观模式
外观模式(Facade Pattern):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。
外观(Facade)模式包含以下主要角色。
- 外观(Facade)角色:为多个子系统对外提供一个共同的接口。
- 子系统(Sub System)角色:实现系统的部分功能,客户可以通过外观角色访问它。
- 客户(Client)角色:通过一个外观角色访问各个子系统的功能。
应用场景:
- 为一个复杂地模块或子系统提供一个供外界访问的接口。
- 子系统相对独立,外界对子系统的访问只要黑箱操作即可
- 维护一个大型遗留系统的时候可能这个系统已经非常难以维护和扩展,此时可以考虑为新系统开发一个 Facede 类,来提供遗留系统的比较清晰简单的接口,让新系统与 Facede 类交互,提高复用性。
- 当系统需要进行分层设计时,可以考虑 Facede 模式。
// 外观模式
// 门面角色:外观模式的核心。它被客户角色调用,它熟悉子系统的功能。内部根据客户角色的需求哪几种功能组合
// 子系统角色: 实现子系统的功能。它对客户角色和facade时是未知的
// 客户角色:通过调用 facade 来完成要实现的功能
class Sum{
sum(a, b) { return a + b }
}
class Minus {
minus(a, b) { return a - b }
}
class Multiply{
multiply(a, b) { return a * b }
}
class Divide {
divide(a, b) { return a / b }
}
class Calculator {
constructor() {
this.sumObj = new Sum()
this.minusObj = new Minus()
this.multiplyObj = new Multiply()
this.divideObj = new Divide()
}
sum(a, b) {
return this.sumObj.sum(a, b)
}
minus(a, b) {
return this.minusObj.minus(a, b)
}
multiply(a, b) {
return this.multiplyObj.multiply(a, b)
}
divide(a, b) {
return this.divideObj.divide(a, b)
}
}
let calculator = new Calculator()
console.log(calculator.sum(22, 889))
优点
- 外观模式最大的优点就是使复杂的接口变的简单,减少了客户端对子系统的依赖,达到解耦的效果。
- 让子系统内部的模块更容易维护和扩展。
- 遵循迪米特法则,对内封装具体细节,对外只暴露必要的接口。 缺点 不符合开闭原则,如果修改某一子系统的功能,通常外观类也要一起修改。
享元模式
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元(Concrete Flyweight)角色:实现抽象享元角色中所规定的接口。
- 非享元(Unsharable Flyweight)角色:是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂(Flyweight Factory)角色:负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合要求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
应用场景:
- 需要缓冲池的场景,比如String常量池、数据库连接池。
- 系统中存在大量的相似对象。
- 细粒度的对象 都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
绘制正方形(不使用享元模式)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<input type="radio" name="color" value="red" id="" checked>红色
<input type="radio" name="color" value="yellow" id="" checked>黄色
<input type="radio" name="color" value="blue" id="" checked>蓝色
<button onclick="draw()">绘制</button>
<div id="container">
</div>
<script>
function draw() {
let btns = Array.from(document.getElementsByName('color'))
let btn = btns.find(item => item.checked)
let color = btn ? btn.value : 'red'
let div = document.createElement('div')
div.style = `width: 100px;height: 100px;background-color: ${color}`
document.getElementById('container').appendChild(div)
}
</script>
</body>
</html>
使用享元模式改进
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
使用享元模式改进
<input type="radio" name="color" value="red" id="" checked>红色
<input type="radio" name="color" value="yellow" id="" checked>黄色
<input type="radio" name="color" value="blue" id="" checked>蓝色
<button onclick="draw()">绘制</button>
<div id="container">
</div>
<script>
class MyDiv{
constructor() {
this.element = document.createElement('div')
}
setColor(color) {
this.element.style = `width: 100px;height: 100px;background-color: ${color}`
}
}
let myDiv = new MyDiv()
function draw() {
let btns = Array.from(document.getElementsByName('color'))
let btn = btns.find(item => item.checked)
let color = btn ? btn.value : 'red'
myDiv.setColor(color)
document.getElementById('container').appendChild(myDiv.element)
}
</script>
</body>
</html>
优点:
相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力。
缺点:
- 为了使对象可以共享,需要将一些不能共享的状态外部化,这将增加程序的复杂性。
- 读取享元模式的外部状态会使得运行时间稍微变长。 案例1
分页(不使用享元模式)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="news">
</ul>
<button onclick="goNext()">下一页</button>
<script>
let list = []
for (let i = 0; i < 20; i++) {
list.push(`新闻${i}`)
}
let pageNum = 0
let pageSize = 5
let news = document.getElementById('news')
for (let i = 0; i < list.length; i++) {
let li = document.createElement('li')
li.style.display = 'none'
li.innerHTML = list[i]
news.appendChild(li)
}
function goNext() {
pageNum++
let start = (pageNum - 1)*pageSize
let lis = document.getElementsByTagName('li')
for (let i = 0; i < lis.length; i++) {
let li = lis[i]
if (i > start && i <= start + pageSize) {
li.style.display = 'block'
}else {
li.style.display = 'none'
}
}
}
goNext()
</script>
</body>
</html>
分页(使用享元模式)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="news">
</ul>
<button onclick="goNext()">下一页</button>
<script>
let list = []
for (let i = 0; i < 20; i++) {
list.push(`新闻${i}`)
}
let getLi = (function (params) {
let lis = [] // 只需创建5个li
return function () {
let element;
if (lis.length < 5) {
element = document.createElement('li')
lis.push(element)
}else {
element = lis.shift() // 从左端删除放到右端
lis.push(element)
}
return element
}
})()
let pageNum = 0
let pageSize = 5
let news = document.getElementById('news')
function goNext() {
pageNum++
let start = (pageNum - 1) * pageSize
for (let i = 0; i < 5; i++) {
let element = getLi()
element.innerHTML = list[start + i]
news.appendChild(element)
}
}
goNext()
</script>
</body>
</html>
行为型模式
行为型模式包含 模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式(Interpreter模式)、状态模式、策略模式、职责链模式(责任链模式)。行为模式对不同的对象之间的划分职责和算法的抽象化,行为模式不仅仅关注类和对象之间的结构,而且重点关注他们之间的相互作用,通过行为型模式,可以更加清晰地划分类与对象地职责,并研究系统在运行时实例对象之间地交互。
观察者模式
观察者模式(Observer Pattern):定义对象间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
观察者模式的主要角色如下。
- 抽象主题(Subject)角色:也叫抽象目标类,它提供了一个用于保存观察者对象的聚集类和增加、删除观察者对象的方法,以及通知所有观察者的抽象方法。
- 具体主题(Concrete Subject)角色:也叫具体目标类,它实现抽象目标中的通知方法,当具体主题的内部状态发生改变时,通知所有注册过的观察者对象。
- 抽象观察者(Observer)角色:它是一个抽象类或接口,它包含了一个更新自己的抽象方法,当接到具体主题的更改通知时被调用。
- 具体观察者(Concrete Observer)角色:实现抽象观察者中定义的抽象方法,以便在得到目标的更改通知时更新自身的状态。
应用场景:
- 对一个对象状态地更新,需要其他对象同步更新,而且其他对象地数量动态可变。
- 对象仅需要将自己地更新通知给其他对象而不需要知道其他对象的细节。
class Star{
constructor(name) {
this.name = name
this.state = ''
this.observers = []
}
getState() {
return this.state
}
setState(state) {
this.state = state
this.notifyAllObservers()
}
attach(observer) { // 增加一个新的观察者
this.observers.push(observer)
}
notifyAllObservers() {
if (this.observers.length > 0) {
this.observers.forEach(observer => observer.update())
}
}
}
class Fun {
constructor(name, star) {
this.name = name
this.star = star
this.star.attach(this)
}
update() {
console.log(this.star.getState())
}
}
let star = new Star('aaa')
let f1 = new Fun('bbb', star)
star.setState('绿色')
优点
- 在观察者和被观察者之间建立一个抽象的耦合。
- 观察者模式支持广播通讯,被观察者会向所有的登记过的观察者发出通知。 缺点
- 观察者模式需要考虑一下开发效率和运行效率问题,一个被观察者,多个观察者,开发和调试就会比较复杂。
案例1
promise 中使用观察者模式
class Promise{
constructor(fn) {
this.successes = []
const resolve = (data) => {
this.successes.forEach(item => item(data))
}
fn(resolve)
}
then(success) {
this.successes.push(success)
}
}
let p = new Promise(function(resolve) {
setTimeout(() => {
resolve('ok')
}, 2000);
})
p.then((res) => {
console.log('res', res)
})
案例2
EventEmitter 函数实现观察者模式
// let EventEmitter = require('events')
class EventEmitter {
constructor() {
this._events = {}
}
on(type, listener) {
let listeners = this._events[type]
if (listeners) {
listeners.push(listener)
} else {
this._events[type] = [listener]
}
}
emit(type) {
let listeners = this._events[type]
let args = Array.prototype.slice.call(arguments, 1)
if (listeners) {
listeners.forEach(l => l(...args))
}
}
}
let eve = new EventEmitter
eve.on('click', function(name) {
console.log('监听', name)
})
eve.emit('click', '触发 监听')
发布订阅模式
观察者模式存在的问题: 目标无法选择自己想要的消息发布,观察者会接收所有消息。
发布订阅模式: 发布订阅模式在观察者模式的基础上,在目标和观察者 之间增加一个调度中心。订阅者(观察者)把自己想要订阅的事件注册到调度中心,当该事件触发的时候,发布者(目标)发布该事件到调度中心,由调度中心统一调度订阅者注册到调度中心的处理代码。
区别: 观察者模式是由具体目标调度的,而发布/订阅模式是统一由调度中心调度的,所以观察者模式订阅者与发布者之间存在依赖的,而发布/订阅模式则不会,所以发布订阅模式的组件是松散耦合的。
// 发布订阅模式
class Agent {
constructor() {
this._events = {}
}
// on
subscribe(type, listener) {
let listeners = this._events[type]
if (listeners) {
listeners.push(listener)
} else {
this._events[type] = [listener]
}
}
// emit
publish(type) {
let listeners = this._events[type]
let args = Array.prototype.slice.call(arguments, 1)
if (listeners) {
listeners.forEach(l => l(...args))
}
}
}
// 房东
class LandLord {
constructor(name) {
this.name = name
}
lend(agent, area, money) { // 向外出租
agent.publish('house', area, money)
}
}
class Tenant{
constructor(name) {
this.name = name
}
rent(agent) {
agent.subscribe('house', (area, money) => {
console.log(`${this.name} 看到新房源:面积 ${area} 房租 ${money}`)
})
}
}
let agent = new Agent()
let t1 = new Tenant('t1')
let t2 = new Tenant('t2')
t1.rent(agent)
t2.rent(agent)
let l1 = new LandLord()
l1.lend(agent, 40, 4000)
l1.lend(agent, 60, 7000)
状态模式
状态模式(State Pattern) :允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象(Objects for States),状态模式是一种对象行为型模式。
状态模式包含以下主要角色。
- 环境类(Context)角色:也称为上下文,它定义了客户端需要的接口,内部维护一个当前状态,并负责具体状态的切换。
- 抽象状态(State)角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为,可以有一个或多个行为。
- 具体状态(Concrete State)角色:实现抽象状态所对应的行为,并且在需要的情况下进行状态切换。
应用场景:
- 行为随状态改变而改变的场景。当一个事件或者对象有很多种状态,状态之间会相互转换,对不同的状态要求有不同的行为的时候,可以考虑使用状态模式。
- 条件、分支判断语句的替代者。在程序中大量使用 switch 语句或者 if 判断语句会导致程序结构不清晰,使用状态模式,通过扩展子类实现了条件的判断处理。
class Battery{
constructor() {
this.amount = 'height'
this.state = new SuccessState() // 绿色状态
}
show() {
this.state.show() // 把显示的状态委托给状态对象
// 内部还要维护状态的变化
if (this.amount === 'height') {
this.amount = 'middle'
this.state = new WarningState()
}else if (this.amount === 'middle') {
this.amount = 'low'
this.state = new ErrorState()
}
}
}
class SuccessState {
show(){
console.log('绿色')
}
}
class WarningState {
show(){
console.log('黄色')
}
}
class ErrorState {
show(){
console.log('红色')
}
}
let battery = new Battery()
battery.show()
battery.show()
battery.show()
battery.show()
优点
- 代码有很强的可读性。状态模式将每个状态的行为封装到对应的一个类中。
- 方便维护。将容易产生问题的 if-else 语句删除了,如果把每个状态的行为都放到一个类中,每次调用方法时都要判断当前是什么状态,不但会产生很多 if-else 语句,而且容易出错。
- 符合开闭原则,容易增删状态。 缺点 会产生很多类。每个状态都要一个对应的类,当状态多多时会产生很多类,加大维护难度。
案例1
点赞取消点赞
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
点赞
<div id="root"></div>
<script>
let likeState = {
render(element) {
element.innerHTML = '赞'
}
}
let likedState = {
render(element) {
element.innerHTML = '取消'
}
}
class Button {
constructor(container) {
this.liked = false // 按钮的状态默认是未点赞状态
this.state = likeState
this.element = document.createElement('button')
container.appendChild(this.element)
this.render() // 渲染按钮本身
}
setState(state) {
this.state = state // 修改状态
this.render() // 状态修改完成之后重新渲染
}
render() {
this.state.render(this.element)
}
}
let btn = new Button(document.getElementById('root'))
btn.element.addEventListener('click', () => {
btn.liked = !btn.liked
btn.setState(btn.liked ? likedState : likeState)
})
</script>
</body>
</html>
案例2
promise 使用状态模式
class Promise {
constructor(fn) {
this.state = 'initial' // 初始状态
this.successes = []
this.errors = []
let resolve = (data) => {
this.state = 'fulfilled'
this.successes.forEach(item => item(data))
}
let reject = (error) => {
this.state = 'failed'
this.errors.forEach(item => item(error))
}
fn(resolve, reject)
}
then(success, error) {
this.successes.push(success)
this.errors.push(error)
}
}
let p = new Promise(function(resolve, reject) {
setTimeout(() => {
let num = Math.random()
if (num > 0.5) {
resolve(num)
} else {
reject(num)
}
}, 500);
})
p.then((data) => {
console.log('成功', data)
},(error) => {
console.log('失败', error)
})
策略模式
策略模式(Strategy Pattern):定义一系列算法,将每一个算法封装起来,并让它们可以相互替换。策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。
策略模式的主要角色如下。
- 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
应用场景:
- 多个类只有在算法或行为上稍有不同的场景。
- 算法需要自由切换的场景。
- 需要屏蔽算法规则的场景。
不使用策略模式
class Customer{
constructor(type) {
this.type = type
}
pay(amount) {
if (this.type == 'member') {
return amount * 0.9
}else if (this.type == 'vip') {
return amount * 0.8
}else {
return amount
}
}
}
let c = new Customer('normal')
console.log(c.pay(100))
使用策略模式(方式1)
class Customer{
constructor(kind) {
this.kind = kind
}
pay(amount) {
return this.kind.pay(amount)
}
}
class Normal {
pay(amount) { return amount }
}
class Member {
pay(amount) { return amount * 0.9 }
}
class VIP {
pay(amount) { return amount * .8 }
}
let c = new Customer(new Member())
console.log(c.pay(100))
使用策略模式(方式2)
class Customer{
constructor() {
this.kinds = {
normal: function(amount) {
return amount
},
member: function(amount) {
return amount * .9
},
vip: function(amount) {
return amount * .8
}
}
}
pay(kind, amount) {
return this.kinds[kind](amount)
}
}
let c = new Customer()
console.log(c.pay('vip',100))
优点:
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
缺点:
- 策略类数量增多,会导致类数目庞大。
- 所有的策略类都需要对外暴露,对于这个缺陷可通过工厂方法模式、代理模式或者享元模式修正。
命令模式
命令(Command)模式的定义如下:将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
命令模式包含以下主要角色。
- 抽象命令类(Command)角色:声明执行命令的接口,拥有执行命令的抽象方法 execute()。
- 具体命令类(Concrete Command)角色:是抽象命令类的具体实现类,它拥有接收者对象,并通过调用接收者的功能来完成命令要执行的操作。
- 实现者/接收者(Receiver)角色:执行命令功能的相关操作,是具体命令对象业务的真正实现者。
- 调用者/请求者(Invoker)角色:是请求的发送者,它通常拥有很多的命令对象,并通过访问命令对象来执行相关请求,它不直接访问接收者。
应用场景: 命令模式适合用在将调用者和请求的接收者进行解耦的场景:
- 界面的按钮都是一个命令,可以采用命令模式。
- 模拟CMD(DOS命令)。
- 订单撤销、恢复。
- 触发-反馈机制。
class Cooker{
cook() {
console.log('做饭')
}
}
class Clean{
clean() {
console.log('保洁')
}
}
class CookCommand {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.cook()
}
}
class CleanCommand {
constructor(receiver) {
this.receiver = receiver
}
execute() {
this.receiver.clean()
}
}
class Customer{
constructor(command) {
this.command = command
}
setCommand(command) {
this.command = command
}
clean() {
this.command.execute()
}
cook() {
this.command.execute()
}
}
let cooker = new Cooker()
let clean = new Clean()
let cookCommand = new CookCommand(cooker)
let cleanCommand = new CleanCommand(clean)
let customer = new Customer(cookCommand)
customer.cook()
customer.setCommand(cleanCommand)
customer.clean()
优点:
- 通过引入中间件(抽象接口)降低系统的耦合度。
- 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
- 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
缺点:
- 可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
案例1
计数器
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="number">0</p>
<button id="addBtn">+</button>
<button id="undoBtn">undo</button>
<button id="redoBtn">redo</button>
<script>
let number = document.getElementById('number')
let worker = {
history: [],
index: -1,
add() {
let oldVal = isNaN(number.innerHTML) ? 0 : parseInt(number.innerHTML)
let newVal = oldVal + 1
worker.history.push(newVal)
worker.index = worker.history.length - 1
number.innerHTML = newVal
},
undo() {
if (worker.index > 0) {
worker.index--
number.innerHTML = worker.history[worker.index]
}
},
redo() {
if (worker.index < worker.history.length - 1) {
worker.index++
number.innerHTML = worker.history[worker.index]
}
},
}
class AddCommand {
constructor(receiver) {
this.receiver = receiver
}
execute(){
this.receiver.add()
}
}
class UndoCommand {
constructor(receiver) {
this.receiver = receiver
}
execute(){
this.receiver.undo()
}
}
class RedoCommand {
constructor(receiver) {
this.receiver = receiver
}
execute(){
this.receiver.redo()
}
}
let addCommand = new AddCommand(worker)
let undoCommand = new UndoCommand(worker)
let redoCommand = new RedoCommand(worker)
let addBtn = document.getElementById('addBtn')
addBtn.addEventListener('click', () => addCommand.execute())
document.getElementById('undoBtn').addEventListener('click', () => undoCommand.execute())
document.getElementById('redoBtn').addEventListener('click', () => redoCommand.execute())
</script>
</body>
</html>
模板方法模式
模板方法(Template Method)模式的定义如下:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。 模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中某些步骤的具体实现。 一般有两部分组成第一部分是抽象父类,第二部分是具体实现的子类
命令模式包含以下主要角色:
- AbstractClass(抽象模板):它的方法分为两类:基本方法和模板 方法。基本方法是由 子类实现的方法并且在模板方法中调用;模板方法算法的骨架。
- ConcreateClass(具体模板):实现父类抽象方法或按需重写方法。
应用场景:
- 当要完成某个过程,该过程要执行一系列的步骤,这一系列的步骤基本相同,但个别步骤在实现时可能不同,通常考虑使用模板方法模式。
- 重构时。把相同的代码抽取到父类中,然后通过钩子函数约束其行为。
class Person {
dinner() {
this.buy()
this.cook()
this.eat()
}
buy() {
throw new Error('必须由子类实现!')
}
cook() {
throw new Error('必须由子类实现!')
}
eat() {
throw new Error('必须由子类实现!')
}
}
class User1 extends Person {
buy() {
console.log('买黄瓜')
}
cook() {
console.log('拍黄瓜')
}
eat() {
console.log('吃拍黄瓜')
}
}
let user1 = new User1()
user1.buy()
user1.cook()
user1.eat()
优点:
- 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
- 它在父类中提取了公共的部分代码,便于代码复用。
- 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
- 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。