设计模式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()
应用场景
- ES6 Proxy
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);
}
}