设计模式 | 青训营笔记

108 阅读6分钟

这是我参与「第四届青训营 」笔记创作活动的的第4天

(WHAT)什么是设计模式?

假设有一个空房间,我们要日复一日地往里面放一些东西。最简单的办法当然是把这些东西直接扔进去,但是时间久了,就会发现很难从这 个房子里找到自己想要的东西,要调整某几样东西的位置也不容易。所以在房间里做一些柜子也许是个更好的选择,虽然柜子会增加我们的成 本,但它可以在维护阶段为我们带来好处。使用这些柜子存放东西的规则,或许就是一种模式

因此,学习设计模式,可以帮助我们写出可维护、复用性高的代码。

从上述例子中也可以看出设计模式和数据结构、算法之间的区别

设计模式关注于”我们要存放东西”这个意图,数据结构关注于”存放东西需要什么(柜子还是书架?什么样的柜子?)”,算法关注于“怎么把书放进柜子”

软件设计中常见问题的解决方案模型

  • 历史经验的总结
  • 与特定语言无关

设计原则

单一职责原则(SRP)

一个对象或方法只做一件事情。如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

应该把对象或方法划分成较小的粒度

最少知识原则(LKP)

一个软件实体应当尽可能少地与其他实体发生相互作用

应当尽量减少对象之间的交互。如果两个对象之间不必彼此直接通信,那么这两个对象就不要发生直接的相互联系,可以转交给第三方进行处理

低耦合的表现,可以参考西欧等级森严的分封制度:

我附庸的附庸,不是我的附庸

开放-封闭原则(OCP)

软件实体(类、模块、函数)等应该是可以扩展的,但是不可修改

当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,尽量避免改动程序的源代码,防止影响原系统的稳定

面向对象的语言中可扩展一般采用继承,不可修改一般采用权限修饰符(private等)

设计模式分类

  • 创建型:如何创建一个对象
  • 结构型:如何灵活地将对象组装成一个较大的结构(参考数据结构)
  • 行为型:负责对象间的高校通信和职责划分

浏览器中的设计模式

  • 单例模式
  • 发布订阅模式(观察者模式)

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

全局唯一访问对象

e.g. 缓存,全局状态管理,window

function SetManager(name) {
    this.manager = name;
}

SetManager.prototype.getName = function() {
    console.log(this.manager);
};

/**
 * ES6写法
 * Typescript
class Manager {
	private manager: string;

	constructor(name: string) {
		this.manager = name;
	}

	public getName() {
		console.log(this.manager);
		return this.manager;
	}
}
*/

var SingletonSetManager = (function() {
    var manager = null;

    return function(name) {
        if (!manager) {
            manager = new SetManager(name);
        }

        return manager;
    } 
})();

SingletonSetManager('a').getName(); // a
SingletonSetManager('b').getName(); // a
SingletonSetManager('c').getName(); // a

发布订阅模式(观察者模式)

一种订阅机制。定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。

e.g. 邮件订阅,上线订阅等

与传统的发布-订阅模式实现方式(将订阅者自身当成引用传入发布者)不同,在JS中通常使用注册回调函数的形式来订阅。

// 订阅
document.body.addEventListener('click', function() {
    console.log('click1');
}, false);

document.body.addEventListener('click', function() {
    console.log('click2');
}, false);

// 发布
document.body.click(); 

JavaScript中的设计模式

  • 原型模式
  • 代理模式
  • 迭代器模式

原型模式

复制已有对象来创建新的对象。

e.g. 原型链,对象创建

Object.create()

代理模式

自定义控制原对象的访问方式,并且允许在更新前后做一些额外处理。

e.g. 监控,代理工具,前端框架实现等

当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象 来控制对这个对象的访问,客户实际上访问的是替身对象。 (Virtual DOM的思想来源)

替身对象对请求做出一些处理之后, 再把请求转交给本体对象。

代理和本体的接口具有一致性,本体定义了关键功能,而代理是提供或拒绝对它的访问,或者在访问本体之前做一些额外的事情。

代理模式主要有三种:保护代理、虚拟代理、缓存代理。

保护代理:实现了访问主体的限制行为,如HTTP请求过滤器。

虚拟代理:虚拟代理在控制对主体的访问时,加入了一些额外的操作。如上节提到的HOF实现函数装饰器的效果,对一些行为在保持核心功能不变的情况下做一些装饰。

缓存代理:为一些开销大的运算结果提供暂时的缓存,提升效率。

// 主体
function add() {
    var arg = [].slice.call(arguments);

    return arg.reduce(function(a, b) {
        return a + b;
    });
}

// 代理
var proxyAdd = (function() {
    var cache = [];

    return function() {
        var arg = [].slice.call(arguments).join(',');
        
        // 如果有,则直接从缓存返回
        if (cache[arg]) {
            return cache[arg];
        } else {
            var ret = add.apply(this, arguments);
            return ret;
        }
    };
})();

console.log(
    add(1, 2, 3, 4),
    add(1, 2, 3, 4),

    proxyAdd(10, 20, 30, 40),
    proxyAdd(10, 20, 30, 40)
); // 10 10 100 100

迭代器模式

在不暴露数据类型的情况下访问集合中的数据。

不用关心对象的数据结构就可顺序访问其中每个元素

e.g. 为不同数据结构类型提供通用的操作接口

class MyDomElement {
	tag: string;
	children: MyDomElement[];
	constructor(tag: string) {
		this.tag = tag;
		this.children = [];
	}

	addChildren(component: MyDomElement) {
		this.children.push(component);
	}

	[Symbol.iterator]() {
		const list: MyDomElement[] = [...this.children];
		let node: MyDomElement;
		return {
			next: () => {
				while((node = list.shift())) {
					// 先序遍历,如果有子节点就放进数组尾部
					node.children.length > 0 && list.push(...node.children);

					// 并打印出当前节点
					return {value: node, done: false};
				}
				return {value: null, done: true};
			}
		};
	}
}

前端框架中的设计模式

  • 代理模式
  • 组合模式

代理模式

移步至Virtual DOM实现及Vue3 Ref()实现。

组合模式

可多个对象组合使用,也可单个对象独立使用。(组件化思想)

e.g. 前端组件,文件目录,DOM

扫描文件夹中的文件:

// 文件夹 组合对象
function Folder(name) {
    this.name = name;
    this.parent = null;
    this.files = [];
}

Folder.prototype = {
    constructor: Folder,

    add: function(file) {
        file.parent = this;
        this.files.push(file);

        return this;
    },

    scan: function() {
        // 委托给叶对象处理
        for (var i = 0; i < this.files.length; ++i) {
            this.files[i].scan();
        }
    },

    remove: function(file) {
        if (typeof file === 'undefined') {
            this.files = [];
            return;
        }

        for (var i = 0; i < this.files.length; ++i) {
            if (this.files[i] === file) {
                this.files.splice(i, 1);
            }
        }
    }
};

// 文件 叶对象
function File(name) {
    this.name = name;
    this.parent = null;
}

File.prototype = {
    constructor: File,

    add: function() {
        console.log('文件里面不能添加文件');
    },

    scan: function() {
        var name = [this.name];
        var parent = this.parent;

        while (parent) {
            name.unshift(parent.name);
            parent = parent.parent;
        }

        console.log(name.join(' / '));
    }
};

// 测试
var web = new Folder('Web');
var fe = new Folder('前端');
var css = new Folder('CSS');
var js = new Folder('js');
var rd = new Folder('后端');

web.add(fe).add(rd);

var file1 = new File('HTML权威指南.pdf');
var file2 = new File('CSS权威指南.pdf');
var file3 = new File('JavaScript权威指南.pdf');
var file4 = new File('MySQL基础.pdf');
var file5 = new File('Web安全.pdf');
var file6 = new File('Linux菜鸟.pdf');

css.add(file2);
fe.add(file1).add(file3).add(css).add(js);
rd.add(file4).add(file5);
web.add(file6);

rd.remove(file4);

// 扫描
web.scan();