设计模式2 - KingQueenie/share GitHub Wiki

设计模式2

SOLID五大设计原则

1. S(Single responsibility principle)——单一职责原则

  • 每个对象/方法只做好一件事
  • 如果功能过于复杂就拆分开,每个部分保持独立

=> 降低了单个方法/对象的复杂性;职责之间相互独立,当一个职责发生变化时,不会影响到其他的职责

2. O(Open Closed Principle)——开放封闭原则

  • 对扩展开放,对修改封闭
  • 增加需求时,扩展新代码,而非修改已有代码

=> 避免修改源代码可能造成的副作用,降低维护源代码的成本

3. L(Liskov Substitution Principle, LSP)——李氏置换原则

  • 子类能覆盖父类
  • 父类能出现的地方子类就能出现(父类对象替换为子类对象,程序不会产生错误和异常)
  • js中使用功能较少(弱类型) => 子类可以扩展父类的功能,但是不能改变父类原有的功能。也就是说,在子类继承父类的时候,除了添加新的方法完成新增功能之外,尽量不要重写父类的方法。如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。如果程序违背了里氏替换原则,则继承的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计他们之间的关系。

4. I (Interface Segregation Principle)——接口独立原则

  • 保持接口的单一独立
  • js中没有接口概念(typescript例外)
  • 类似单一职责原则,这里更关注接口

5. D(Dependence Inversion Principle ,DIP)——依赖倒置原则

  • 面向接口编程,依赖于抽象而不依赖于具体
  • 使用方只关注接口而不关注具体类的实现
  • js中使用较少(没有接口概念,弱类型)

在js中,使用较多的是单一职责原则和开放封闭原则

回顾

  • 构造函数模式

在ECMAScript中,构造函数可以用来创建特定类型的对象,其可以定义自定义对象类型的属性和方法。

  • 单体模式

一个类只实例化唯一的对象,即一个类只有一个实例,系统中唯一被使用。如登录框、购物车等。

  • 模块模式

为单体模式添加私有变量和私有方法,能够减少全局变量的使用

  • 工厂模式

构造函数和创建者分离;书写简单,不用每次都new;构造函数名的更改,不会对使用者产生大的影响

// 这个函数相当于工厂,封装了返回实例的操作
class jQuery {
	constructor(selector) {
		// 获取数组的slice
		let slice = Array.prototype.slice
		// 获取节点,利用slice.call将其结果返回给一个数组,因为可能是多个dom节点
		let dom = slice.call(document.querySelectorAll(selector))
		// 获取dom的长度
		let len = dom ? dom.length : 0
		// 进行循环
		for (let i = 0; i < len; i++) {
			// 将dom的数组元素赋值给this也就是实例的元素,元素的k就是数组的k,0,1,2...
			this[i] = dom[i]
		}
		// 赋值数组的长度
		this.length = len
		this.selector = selector || ''
	}
	append(node) {
		//...
	}
	addClass(name) {
		//...
	}
	html(data) {
		//...
	}
	// 此处省略若干 API
}

// 定义全局$函数,函数里面返回一个jquery实例
window.$ = function(selector) {
	return new jQuery(selector)
}
console.log($(p))
  • 观察者模式

定义了一种一对多的依赖关系,当一个对象状态发生改变时,会触发所有观察者对象

// 主题(发布者),保存状态,接收状态变化,状态变化后触发所有观察者对象
class Subject {
	constructor() {
		// 状态
		this.state = 0
		// 所有观察者为一个数组
		this.observers = []
	}
	getState() {
		return this.state
	}
	setState(state) {
		this.state = state
		this.notifyAllObservers() // 通知所有的观察者
	}
	// 添加一个新的观察者
	attach(observer) {
		this.observers.push(observer)
	}
	// 循环所有的观察者
	notifyAllObservers() {
		this.observers.forEach(observer => {
			// 遍历的每个元素执行update方法
			observer.update()
		})
	}
}

// 观察者(订阅者),等待被触发
class Observer {
	constructor(name, subject) {
		this.name = name
		this.subject = subject
		// 将观察者添加到主题当中
		this.subject.attach(this)
	}
	update() {
		console.log(`${this.name} update, state: ${this.subject.getState()}`)
	}
}

// 测试代码
let s = new Subject()
let o1 = new Observer('o1', s)
let o2 = new Observer('o2', s)
let o3 = new Observer('o3', s)

s.setState(1) // 发布者发布一条消息,会通知所有的订阅者
// s.setState(2)
// s.setState(3)

详细介绍:设计模式1

适配器模式

  • 旧接口格式和使用者不兼容
  • 为避免修改旧接口,对旧接口做一层包装,中间加一个适配转换接口(如转换插头),来适配新需求

适配器模式

 // 旧接口
class Adaptee{
	specificRequest(){
		return '这是旧接口'
	}
}
// 新接口
class Target{
	constructor(){
		this.adaptee = new Adaptee()
	}
	request(){
		// 进行转换
		let info = this.adaptee.specificRequest()
		return `${info}--新接口`

	}
}
// 测试
let target = new Target()
console.log(target.request())

应用场景

  • 封装旧接口(旧接口和新接口起冲突时,使用适配器修改)
// 有可能之前使用的都是jquery的写法
$.ajax({...})
// 替换jquery的写法,使用自己封装的ajax
ajax({
	url: '/getData',
	type: 'post',
	dataType: 'json',
	data: {id: '123'}
}).done(function() {})
// 做一层适配器,封装旧接口
let $ = {
	ajax: function(options) {
		return ajax(options);
	}
}
// 使用者不用全局更改,仍可以按照原来的写法
$.ajax({...})
  • 两个地图接口之间的适配
var googleMap = {
	show() { // 方法是show
		console.log( '开始渲染谷歌地图' )
	}
};
var baiduMap = {
	display() { //方法是display
		console.log( '开始渲染百度地图' )
	}
};
var baiduMapAdapter = {
	show(){ // 适配器也改为show,返回的是display
		return baiduMap.display()
	}
};
//下面是渲染地图的方法,传入地图对象
var renderMap = function( map ){//传入地图对象
	if ( map.show instanceof Function ){ //判断
		map.show();  //地图对象的show方法
		//在传入baiduMapAdapter对象的时候,调用show方法,返回的
		//实际是baiduMap的display方法。
	}
};
renderMap( googleMap ) // 输出:开始渲染谷歌地图
renderMap( baiduMapAdapter ) // 输出:开始渲染百度地图
  • 两个API返回的数据结构不同,不需要直接改动API的输出代码,把输出的数据按照新数据结构的格式做适配,通过适配器渲染旧数据,输出新数据结构

装饰器模式

  • 为对象添加新功能
  • 不改变其原有的结构和功能

=> 既能使用原有的功能,又能使用装饰后的功能

装饰器模式

class Circle {
	draw() {
		console.log('我要画一个圆')
	}
}
// 装饰器
class Decorator {
	constructor(circle) {
		this.circle = circle
	}
	draw() {
		this.circle.draw()
		this.setBorder(circle)
	}
	setBorder(circle) {
		console.log('我还画了条线')
	}
}
// 测试
let circle = new Circle()
circle.draw()
let dec = new Decorator(circle)
dec.draw()

应用场景

  • ES7装饰器
// 1、首先定义一个函数,传个target参数
function testDemo(target){
    target.isDec = true
}

// 2、在Demo这个类上面,加个@testDemo(这个就是装饰器),通过@语法将这个类装饰一遍
// target这个参数其实就是Demo这个类
@testDemo
class Demo{

}

alert(Demo.isDec) // true

装饰器原理

@decorator
class A {}

// 等同于

class A {}
// 将A定义成decorator函数执行一遍的返回值(相当于A在decorator执行了一遍),没有的话返回A
A = decorator(A) || A

给装饰器传参数

@testDemo1(false)
class Demo1{

}

function testDemo1(isDec){
	// 这里面返回一个函数,装饰器返回的都是一个函数
	return function(target){
		target.isDec = isDec
	}
}
console.log(Demo1.isDec) // false

代理模式

  • 使用者无权访问目标对象
  • 中间加代理,通过代理做授权和控制

示例:公司内网无法直接访问,需要VPN;明星经纪人与主办方 代理模式

// 目标对象
class ReadImg{
	constructor(filename){
		this.filename = filename
		this.loadImg() // 初始化,加载图片
	}
	display(){
		console.log('display---'+ this.filename)
	}
	loadImg(){
		console.log('loading---'+this.filename)
	}

}
// 代理
class ProxyImg{
	constructor(filename){
		this.readImg = new ReadImg(filename)
	}
	display(){
		this.readImg.display()
	}
}
let proxyImg = new ProxyImg('1.png')
proxyImg.display()

应用场景

Proxy用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。对设置了拦截行为的对象obj,去读写它的属性,用自己的定义覆盖了对象的原始定义。

let proxy = new Proxy(target, handler);

Proxy对象的所有用法,都是上面的这种形式。不同的只是handle参数的写法。其中new Proxy用来生成Proxy实例,target是表示所要拦截的对象,handle是用来定制拦截行为的对象。如果handler没有设置任何拦截,那就等同于直接通向原对象.

明星和经纪人

// 明星
let star = {
	name: '张XX',
	age: 25,
	phone: '13910733521'
}

// 经纪人agent
// star的代理对象,监听代理的获取 get 和设置 set 属性
// 注意代理的接口要和原生的一样 比如要知道name,就写name
let agent = new Proxy(star, {
	// target代理对象, key就是代理对象的值
	get: function (target, key) { // 拦截对象属性的读取
		if (key === 'phone') {
			// 返回经纪人自己的手机号
			return '18611112222'
		}

		if (key === 'price') {
			// 明星不报价,经纪人报价
			return 120000
		}
		// 如果不是在这个两种情况,直接返回target[key]
		return target[key]
	},

	set: function (target, key, val) { // 拦截对象属性的设置
		// 这是我们自己定义的价格
		if (key === 'customPrice') {
			if (val < 100000) {
				// 最低 10w,小于10万,报错
				throw new Error('价格太低')
			} else {
				target[key] = val
				// 这里写renturn true 要不然不会赋值成功
				return true
			}
		}
	}
})

// 测试+++++++++++
// 主办方
console.log(agent.name) // 张XX
console.log(agent.age) // 25
console.log(agent.phone) // 18611112222 
console.log(agent.price) // 120000

// 想自己提供报价(砍价,或者高价争抢)
agent.customPrice = 150000
// agent.customPrice = 90000  // 报错:价格太低
console.log('customPrice', agent.customPrice)

代理模式、适配模式、装饰器模式三者的区别

  • 适配器模式:提供一个不同的接口(进行格式的转换),比如不同版本的插头
  • 装饰器模式:扩展原有的功能,原有功能不变且可直接使用
  • 代理模式:提供一模一样的接口(目标类不让使用者使用,使用又必须使用);显示原有功能,但是经过限制或阉割之后的

迭代器模式

  • 提供一种方法可以顺序访问一个集合(必须是有序列表,如数组,对象是个无序列表),为遍历不同的集合结构提供一个统一的接口
  • 使用者无需知道集合的内部结构(封装,目的在于生成一个访问机制,不需要外界知道内部结构)

比如:

let arr = [1, 2, 3];
let nodeList = document.getElementsByTagName('p');
let $p = $('p');

// 要对这三个变量进行遍历,需要写三个遍历的方法
// 第一
arr.forEach((item) => {
    console.log(item);
})
// 第二
for(let i = 0; i < nodeList.length; i++) {
	console.log(nodeList[i]);
}
// 第三
$p.each((key, p) => {
    console.log(key, p);
})

需要定义一个统一的迭代器来遍历不同的集合结构

迭代器模式

// 迭代器
class Iterator {
	constructor(conatiner) {
		this.list = conatiner.list
		this.index = 0
	}
	next() {
		if (this.hasNext()) {
			// 如果还有下一项,直接返回当前这一项的index++
			return this.list[this.index++]
		}
		// 如果没有,则返回null
		return null
	}
	hasNext() {
		// 判断有没有下一项
		// this.index >= this.list.length 这句话的意思是有没有到头
		if (this.index >= this.list.length) {
			return false
		}
		// 如果没有就是还有下一项
		return true
	}
}
// Container容器
class Container {
	constructor(list) {
		this.list = list
	}
	// 生成遍历器
	getIterator() {
		// 遍历器是有依据的,所以要传递一个参数
		return new Iterator(this)
	}
}
// 测试代码
let container = new Container([1, 2, 3, 4, 5])
// 生成一个遍历器,通过这个遍历器可以兼容所有的有序结集合的数据结构
let iterator = container.getIterator()
while(iterator.hasNext()) {
	console.log(iterator.next())
}

应用场景

  • jQuery的each
// 通过jQuery的each来遍历三种对象
function each(data) {
	var $data = $(data); // 生成一个迭代器
	$data.each(function() {
		console.log(key, p);
	});
}
// 测试代码
each(arr);
each(nodeList);
each($p);
  • ES6 Iterator

=> 通过一个统一的接口来遍历所有的有序集合数据类型,比如Array,Map,arguments,nodeList等

// 传入的data可以是任意的
function each(data) {
	// 生成遍历器,类似jquery生成的遍历器
	let iterator = data[Symbol.iterator]() // 迭代器对象方法
	let item = { done: false }
	while (!item.done) {
		// 每次获取next
		item = iterator.next() // 有数据时返回 {value: 1, done: false}
		// 判断done是否结束,done 等于 true 就结束了
		if (!item.done) {
			console.log(item.value)
		}
	}
}
// 测试
let arr = [1, 2, 3, 4]
let nodeList = document.getElementsByTagName('p')
// 如果是对象,可以利用Map.set
let m = new Map()
m.set('a', 100) // ['a', 100]
m.set('b', 200)

简单的写法

function each(data) {
	for (let item of data) {
		console.log(item);
	}
}