avatar

目录
设计模式

何为设计模式

软件工程中,设计模式(design pattem)是对软件设计中普遍存在 (反复出现) 的各种问题,所提出的解决方案

设计模式的目的

  1. 可重用性
  2. 可维护,可靠性:方便增加新功能,新增功能对原来功能没有影响
  3. 目的:高内聚(功能模块),低耦合的特性

内聚:一个模块内各个元素彼此结合的紧密程度
耦合:一个软件结构内不同模块之间互连程度的度量

最近编码的时候,总是在犹豫是把某个方法封装在一个类里,还是单独的封装成一个类。这让我突然想起内聚耦合这两个名词。我们一直追求着,高内聚,低耦合。

对于低耦合,我粗浅的理解是:
一个完整的系统,模块与模块之间,尽可能的使其独立存在。也就是说,让每个模块,尽可能的独立完成某个特定的子功能。模块与模块之间的接口,尽量的少而简单。如果某两个模块间的关系比较复杂的话,最好首先考虑进一步的模块划分。这样有利于修改和组合。

对于高内聚,我粗浅的理解是:
在一个模块内,让每个元素之间都尽可能的紧密相连。也就是充分利用每一个元素的功能,各施所能,以最终实现某个功能。如果某个元素与该模块的关系比较疏松的话,可能该模块的结构还不够完善,或者是该元素是多余的。

内聚和耦合,包含了横向和纵向的关系。功能内聚和数据耦合,是我们需要达成的目标。横向的内聚和耦合,通常体现在系统的各个模块、类之间的关系,而纵向的耦合,体现在系统的各个层次之间的关系。

对于我在编码中的困惑,我是这样想的,用面向对象的思想去考虑一个类的封装。
一个方法,如何封装,拿到现实生活中来看,看这种能力(方法)是否是属于这类事物(类)的本能。如果是,就封装在这个类里。如果不是,则考虑封装在其它类里。如果这种能力,很多事物都具有,则一定要封装在这类事物的总类里。如果这种能力,很多事物都会经常用到,则可以封装成一个总类的静态方法。

设计原则

设计描述

  • 即按照哪一种思路或者标准来实现功能
  • 功能相同,可以有不同设计方案来实现
  • 伴随着需求的增加,设计的作用才能体现出来

《UNIX/LINUX 设计哲学》到设计准则

  • 准则 1:小即是美
  • 准则 2:让每个程序只做好一件事
  • 准则 3:快速建立原型(先满足基本需求,再扩展)
  • 准则 4:舍弃高效率而取可移植性
  • 准则 5:采用纯文本来存取数据(效率于可读性之间取舍)
  • 准则 6:充分利用软件的杠杆效应(软件复用)
  • 准则 7:使用shell脚本来提高杠杆效应和可移植性
  • 准则 8:避免强制性的用户界面
  • 准则 9:让每个程序都称为过滤器

小准则:

  1. 允许用户定制环境
  2. 尽量使用操作系统内核小而轻量化
  3. 使用小写字母并尽量简短
  4. 沉默是金
  5. 各部分之和大于整体
  6. 寻求 90% 的解决方案

五大设计原则 SOLID

  1. 单一职责原则 (SRP)

    功能过于复杂就拆分,每个部分保持独立,职责过多,可能引起它变化的原因就越多,这样导致职责依赖,相互之间就会产生原因,大大损伤其内聚性和耦合度。

  2. 开放-封闭原则 (OCP)

    开放 - 封闭原则是面向对象设计的核心所在;对扩展开放,对修改封闭;遵循这个原则可以带来巨大好处,也就是可维护,可扩展,可复用,灵活性好。这是软件设计的终极目标。

  3. 里氏替换原则 (LSP)

    子类能覆盖父类,父类出现的地方子类就能出现。里氏代换原则是对“开 - 闭”原则的补充。实现“开 - 闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

  4. 接口隔离原则 (ISP)

    保持接口的单一独立,避免出现‘胖接口’,类似单一职责原则,这里跟关注接口

  5. 依赖倒转原则 (DIP)

    面向接口编程,依赖于抽象而不依赖于具体。简单说就是,我们要针对接口编程,使用接口只关注接口而不关注具体类的实现(面向抽象编程,非具象编程)。

23 中设计模式

从设计到模式 → 设计原则为指导思想 → 模式

创建型

工厂模式(工厂方法模式,抽象工厂模式,建造者模式)

意图: 将实例化对象的代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦,从而提高项目的扩展和维护性。

  • 作为一种创建类模式,在任何需要生成复杂对象的地方,都可以使用工厂方法模式。有一点需要注意的地方就是复杂对象适合使用工厂模式,而简单对象,特别是只需要通过 new 就可以完成创建的对象,无需使用工厂模式。如果使用工厂模式,就需要引入一个工厂类,会增加系统的复杂度。符合 OCP,创建者于构造函数分离
JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class jQuery{
constructor(selector){
let dom = [].slice.call(document.querySelectorAll(selector));
let len = dom ? dom.length:0;
for(let i = 0; i< len; i++){
this[i] = dom[i];
}
this.len = len;
this.selector = selector;
}
append(node){}
html(data){}
}
// 创建一个 jQuery 对象时就使用了工厂模式,必须使用 new 操作符和 return 实例
window.$ = function(selector){
return new jQuery(selector);
}

单例模式 ⭐️

  • 意图: 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
  • 主要解决: 一个全局使用的类频繁地创建与销毁。
  • 何时使用: 当您想控制实例数目,节省系统资源的时候。
  • 如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
  • 关键代码: 构造函数是私有的。
  • 优点:
    1. 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)
    2. 避免对资源的多重占用(比如写文件操作)
    3. 符合了单一职责的原则,只实例化唯一的对象
  • 使用场景: 工具类,频繁访问的数据库或是文件对象(如数据源、session 工厂、计数器、vuex 中的 store
JavaScript
1
2
3
4
5
if(window.jQuery != null){
return window.jQuery;
}else{
// 初始化...
}

原型模式

创建重复的对象,创建对象的代价比较大时,如字段太多,就可以使用该模式。
原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 方法一: 使用 Object.create(prototype, optionalDescriptorObjects)
var vehiclePrototype = {
model:"保时捷",
getModel: function () {
console.log('车辆模具是:' + this.model);
}
};
var vehicle = Object.create(vehiclePrototype,{
"model":{
value:"法拉利"
}
});
vehicle.getModel();

// 方法二:使用 Prototype
var vehiclePrototype = {
init: function (carModel) {
this.model = carModel || "保时捷";
},
getModel: function () {
console.log('车辆模具是:' + this.model);
}
};
function vehicle(model) {
function F() { };
F.prototype = vehiclePrototype;
var f = new F();
f.init(model);
return f;
}
var car = vehicle('法拉利');
car.getModel();

组合型

设配器模式

意图:将一个类的接口转换成客户希望的另外一个接口(使旧接口兼容新接口)。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 符合 OCP
日常联想:在大陆使用港版的充电头,需的转换器,这个转换器就是类似设配器模式,在不拆改充电头的前提的下,正常使用它。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 封装旧接口
// 自己的封装的 ajax,使用方法如下
ajax({
url:'xxx',
type:'post',
data:{id:123}
})

// 但历史原因,以前代码全是:
$.ajax({....})

// 做一个设配器
var $ = {
ajax:function(options){
return ajax(options);
}
}

装饰器模式 (添加额外的功能模式)

意图:在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责。就增加功能来说,装饰器模式相比生成子类更为灵活。
扩展功能,原有功能不变且直接使用,两者独立存在,符合开放封闭原则,手机壳例子

我们可以通过装饰者模式来实现 AOP,但是两者并不是一个维度的概念。 AOP 是一种编程范式,而装饰者是一种设计模式。

  • 装饰者模式非常适合给业务代码附加非业务相关功能(如日志上报),就如同给照片加滤镜;
  • 装饰者模式非常适合无痛扩展别人的代码(你经常需要接手别人的项目吧)

有些同学可能会觉得装饰者模式和 vue 的 mixin 机制很像,其实他们都是“开放 - 封闭原则”和“单一职责原则”的体现。

《javascript 面向对象编程指南(第二版)》中关于装饰器模式的解释:

装饰器模式是一种结构型模式,它与对象的创建无关,主要考虑的是如何拓展对象的功能。也就是说,除了使用线性式(父-子-孙)继承方式之外,我们也可以为一个基础对象创建若干个装饰对象以拓展其功能。然后,由我们的程序自行选择不同的装饰器,并按不同的顺序使用它们。在不同的程序中我们可能会面临不同的需求,并从同样的装饰器集合中选择不同的子集。

ES7 Decorator

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//看一个简单的例子:
Function.prototype.fn = function(fn){
var self = this;
return function(){
self.apply(this,arguments);
fn.apply(this,arguments);
}
}

function a(){
console.log('我是函数a');
}

var copyA = a.fn(function(){
console.log('我是a函数额外的功能');
})
copyA();
// 我是函数a
// 我是a函数额外的功能

core-decorators 库

代理模式

显示原有功能,但是经过显示或者授权控制之后的(ES6 proxy)明星经纪人例子

代理模式 vs 设配器模式

  • 适:提供一个不同的接口(如不同版本的插头)(可以使用目标对象,但无法用)
  • 代:提供一模一样的接口(无权使用目标对象,但想用目标对象)

外观模式

意图:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

主要解决:降低访问复杂系统的内部子系统时的复杂度,可以理解:提供一个整合功能的‘胖’接口,简化客户端与之的接口;不符合单一职责和开闭原则,如果要改东西很麻烦,继承重写都不合适。不可滥用,但是好用。😂

JavaScript
1
2
3
4
5
6
7
8
9
function bindEvent(elem,type,selector,fn){
if(fn==null){
fn = selector;
selectot = null
}
}
// 调用 jQuyer源码用的较多
bindEvent(elem,'click','#div1',fn);
bindEvent(elem,'click',fn);

桥接模式

组员模式

享元模式

行为型

观察则模式 (变体为发布 - 订阅模式) ⭐⭐⭐

观察者模式是一种行为型模式,主要用于处理不同对象之间的交互通信问题。观察者模式中通常会包含两类对象:

  • 一个或多个发布者对象:当有重要的事情发生时,会通知订阅者
  • 一个或多个订阅者对象:它们追随一个或多个发布者,监听它们的通知,并作出相应的反应

优点:

  1. 观察者和被观察者是抽象耦合的。
  2. 建立一套触发机制。

缺点: 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。

观察者模式:我办了一个补习班,学生想来我这学习,必须先报名(注册)。收齐一帮学生,开始教学,学生们听了我的课及时更新了自己的认知。我和学生们紧密相连。每个人我都认识。

发布订阅模式:我在某视频站上开了一个专栏,把我的课上传上去,喜欢的同学订阅下。后续我只要把最新课程传到视频站上就好了,学生们听了我的课亦能及时新了自己的认知。我和学生们的联系不是那么大了。我也不需要认识他们。
后者比前者多了一个类似中转站的东西(姑且称为“中台”),省了我好多事。有学生不愿意学了 ,直接找中台退订就好了,不用找我说。我发布的新课程也由中台做广播,不用我自己再一个个通知,不会影响到我自己干其他工作。两种模式本质都是一样的,主要关键点都在于注册(添加到注册数组中)和触发(触发注册数组中的内容),只是订阅/发布模式对注册和触发进行了解耦。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// 观察者模式 🌰(低耦合)
// 被观察者
class Subject{
constructor(name){
this.name = name;
this.observers = [];
this.state ="宝宝心情不错!"
}
// 被观察得提供一个接受观察者的方法
addOb(ob){
this.observers.push(ob)
}
setState(newSate){
this.state =newState;
this.notify();
}
// 更改状态 通知观察者
notify(){
this.observers.forEach(n=>{
n.update(vanewStatelue)
})
}
}

// 观察者
class ObServer{
constructor(name){
this.name = name;
}
// 观察者必须提供被观察者通知的方法
update(state){
console.log(`${this.name}知道了${state}该干嘛干嘛了。`)
}
}

let baobao = new Subject("宝宝");
let o1 = new ObServer("妈妈");
let o2 = new ObServer("爸爸");

baobao.addOb(o1)
baobao.addOb(o2)
baobao.setState("宝宝心里好苦!")

// addEventListener 就相当于注册了一个观察者,当观察到‘click’事件的时候,作出一些处理。


// 发布订阅 🌰(完全解藕)

var observer = {
addSubscriber:function (callback){//添加订阅者
if(typeof callback === "function"){
this.subscribers[this.subscribers.length] = callback;
}
},
removeSubscriber:function (callback){//删除订阅者
for(var i=0;i<this.subscribers.length;i++){
if(this.subscribers[i] === callback){
delete this.subscribers[i];
}
}
},
publish:function (what) {//授受并传递数据给订阅者
for(var i=0;i<this.subscribers.length;i++){
if(typeof this.subscribers[i] === "function"){
this.subscribers[i](what);
}
}
},
make:function(o){//将任意对象转变为一个发布者并为其添加上述方法。
for(var i in this){//this->observer{addSubscriber: ƒ, removeSubscriber: ƒ, publish: ƒ, make:f}
if(this.hasOwnProperty(i)){//observer.hasOwnProperty('addSubscriber') -> true
o[i] = this[i];
o.subscribers = [];
}
}//o-> {addSubscriber: ƒ, removeSubscriber: ƒ, publish: ƒ, make:f,subscribers:[],o.XX}
}
};

//有个函数 blogger 和任意一个函数 jack
var blogger = {
writeBlogPost : function(){
var content = 'blogger';
this.publish(content);
}
};
var jack = {
read:function (what){
console.log('jack 订阅:'+what);
}
};

//blogger 变为发布者
observer.make(blogger);

//jack 订阅 blogger
blogger.addSubscriber(jack.read);

//blogger 发布信息
blogger.writeBlogPost();

//输出:jack 订阅: blogger

最后: 别的函数也可以成为发布者,
blogger也可以添加任意的函数为订阅者
jack也可以订阅别的发布者

策略模式

意图: 定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。一个问题匹配多个结局方案,且可以添加删除方案。
主要解决: 在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。
关键代码: 实现同一个接口。
应用实例:
1. 诸葛亮的锦囊妙计,每一个锦囊就是一个策略。
2. 旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。
3. 商品打折方式(见下例)
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const calePrice = (function () {
const sale = {
"100_10": (price) => (price -= 10),
"200_25": (price) => (price -= 25),
"80%": (price) => (price *= 0.8)
};
function calePrice(price, type) {
if (!sale[type]) return "没有这个折扣";
return sale[type](price);
}
calePrice.addSale = function (type, fn) {
sale[type] = fn;
};
calePrice.delSale = function (type, fn) {
delete sale[type];
};
return calePrice;
})();

console.log(calePrice(299, "200_25"));
console.log(calePrice(299, "80%"));
// 打折规则 可以方便扩展
calePrice.addSale("300_30", function (price) {
return (price -= 30);
});
console.log(calePrice(300, "300_30"));

模板方法模式

迭代器模式

顺序访问一个集合,使用者无需知道集合的内部结构(分装)

职责链模式

命令模式

状态模式

有限状态机 – “收藏”和“取消”

备忘录模式

访问者模式

中介者模式

解释器模式

待续。。。 学不动了 😭

文章作者: Tim
文章链接: http://w3ctim.com/post/364ea8cc.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Tim

评论