设计模式学习

120 阅读3分钟

@TOC

一、设计模式

设计模式包含两部分:1、设计原则(要遵循定义好的一些方法);2、设计模式(主要就是经验的复用)

1.1设计原则有哪些?

  • 单一原则: 一个类应该只有一个应该发生的原因,简而言之就是每个类只负责自己的那部分,类的复杂度就会降低。
  • 开闭原则: 一个软件实体,如类,模块和函数应该对扩展开放,对修改关闭;
  • 里氏替换原则 所有引用基类的地方必须能透明的使用期子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。
  • 迪米特法则: 迪米特法则(Law of Demeter) 又叫做 最少知识原则,一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。
  • 接口隔离原则: 多个特定的客户端接口要好于一个通用性的接口。
  • 依赖倒置原则: 1、上层模块不应该依赖低层模块,他们都应该依赖于抽象。 2、抽象不应该依赖于细节,细节应该依赖于抽象。

二、设计模式有哪些?

1.单例模式

前端具体会使用到单例模式的需求:比如我想要这个页面只有一个对话框,或者jqery中的$符号等。

下面我们使用单例模式,来实现页面中只有一个对话框的单例模式:

//通用单例
function createInstance(fn){
	//闭包模式
	let instance;
	return function(...args){
		if(!instance){
			instance = new fn(...args);
		}
			return instance;
	}
}
//单例应用:
class Dialog{
	constructor(){
		let dialog = document.createElement("div");
		this.dialog = dialog;
		this.dialog.style.display = "none";
		this.isShow = false;
	}
	showDialog(){
		//可以实现只显示一个对话框
		if(!this.isShow){
			this.dialog.innerHTML = '对话框';
			document.body.appendChild(this.dialog);
			this.dialog.style.display = 'block';
			this.isShow = true;
		}else{
			console.log('已经显示一个了');
		}
	}
}
let dialog1 = new Dialog();
let dialog2 = new Dialog();
//上面这种方式同样可以实例化两个对话框,所以没有实现单例模式
document.querySelector("button").onclick = function(){
	dialog1.showDialog();
	dialog2.showDialog();
}
//要实现单例模式,使用下面的代码:
let createInstanceFn = createInstanceFn(Dialog);
let dialog1 = new createInstanceFn();
let dialog2 = new createInstanceFn();
//只会创建一个实例
document.querySelector("button").onclick = function(){
	dialog1.showDialog();
	dialog2.showDialog();
}

2.工厂模式

工厂模式就是需要的东西封装起来,方便我们调用。但是这样有时候反而看起来更复杂了,但是使用的场景很多,上面的通用单例也是一种工厂模式。

看一下代码:

class Luban{
	constructor(){
		this.name = "鲁班";
	}
}
class Yase{
	constructor(){
		this.name = '亚瑟';
	}
}

//工厂模式
function Factory(heroName){
	switch(heroName){
		case 'luban':
		return new Luban();
		break;
		case 'yase':
		return new Yase();
		break;
		default:
		console.log('没有英雄');
		break;
	}
}
let yase = Factory('yase');
let luban = Factory('luban');

3.装饰者模式

装饰者模式就是在原有方法的基础上进行扩展。看下面这个例子

class Yase(){
	constructor(name){
		this.name = '亚瑟';
	}
	release(){
		console.log("释放技能");
	}
}

let yase = new Yase();

function hurt(){
	console.log("造成100点伤害");
}
//这个时候我们想给这个英雄释放技能造成伤害,这个时候如何修改呢?
//首先如果我们使用extends 继承的话,里面再写一个release,然后添加造成伤害。但是这种方式,违背了里氏替换原则原则。因为是父组件的功能发生了变化。
//另外一中就是直接在Yase里面修改代码,但是这违背了开闭原则。原因是:不能直接修改父组件代码。
//那我们该如何实现呢???
Function.prototype.Decorator = function(fn){
	this(); //这个this 就是谁调用Decorator这个方式就是谁
	fn();
}

yase.release.Decorator(hurt);

如果有多个方法需要扩展,我们可以修改上面的代码变成装饰者链的形式:

class Yase(){
	constructor(name){
		this.name = '亚瑟';
	}
	release(){
		console.log("释放技能");
	}
}

let yase = new Yase();

function hurt(){
	console.log("造成100点伤害");
}
function work(){
	console.log("走了100步");
}
Function.prototype.Decorator = function(fn){
	let _this = this;
	//因为返回的是个函数,所以他还有Decorator方法
	return function(){
		_this();
		fn();
	}
}
//装饰者链
yase.release.Decorator(hurt).Decorator(work)();

4.观察者模式

观察者模式:就是对要观察的对象,进行监听,然后发生变化的话就执行对应的操作;

一个典型的例子就是:

document.querySelector(".box").addEventListener("click",function(){
	console.log("click1");
})

下面我们自己手动实现一个时间类,来管理两个对象,当有一个对象发生改变的时候,另外一个对象也会发生改变。

let obj1 = {
	fn1(){
		console.log("fn1更新");
	}
}
let obj2 = {
	fn2(){
		console.log("fn2更新");
	}
}

//管理事件类
class MyEvent{
	 constructor(){
		this.handles = {};
	}
	addEvent(eventName,fn){
		//一个事件可以对应多个函数:{myevent:[fn1,fn2,fn3....]}
		if(typeof this.handles[eventName] === "undefined"){
			this.handles[evnetName] = [];
		}
		this.handles[eventName].push(fn);
	}
	trigger(eventName){
		if(!eventName in this.handles){
			return;
		}
		this.handles[eventName].forEach(fn=>fn())
	}
}

//使用自定义事件(观察者模式)
let eventObj = new MyEvent();

//让eventObj这个对象和obj1和obj2产生关联
evnetObj.addEvent("myevent",obj1.fn1);
evnetObj.addEvent("myevent",obj2.fn2);

//同时触发fn1和fn2
setTime(()=>{
	evnetObj.trigger("myevnet");
},1000)

这里做的最大的事情就是对两个事件进行结藕,就是让两个对象产生关联。更新一个对象的事件,另外一个对象的事件也会发生更新。

5.代理模式

通过对对象的代理,来实现对对象的控制,通过代理实现的方式肯定是跟原来没有使用代理是有区别的,否则这种方式没有意义。

使用代码来体会其中的区别:

let zhangsan = {
	sellHouse(num){
		console.log("卖了" + num +"万元");
	}
}
//代理方式
let proxySeller = {

	sellHouse(hasSold,num){
		if(hasSold){
			zhangsan.sellHouse(num - 2);
		}else{
			zhangsan.sellHouse(num);
		}
	}
}
proxySeller.sellHouse(true,100);

proxy在前端使用最多的就是vue 和react中的代理。例如:代理服务器,转发请求;es6中的代理对象,对对象进行控制,和vue3的响应式对象相似;

下一个例子:当加载一个图片的时候,给图片一个占位图,图片没有展示出来的时候显示:

class CreateImage{
	constructor(){
		this.img = document.createElement("img");
		document.body.appendChild(this.img);
	}
	setSrc(src){
		this.img.src = src;
	}
}

//let img = new CreateImg();
let src = `图片地址`;
//img.setSrc(src);
//代理模式实现
function proxyImg(){
	let myImg = new CreateImg();
	let loadImg = new Image();
	//设置占位图片
	myImg.setSrc("./loading.gif");
	loadImg.src = src;
	//当加载完成之后设置图片地址
	loadImg.onload = function(){
		myImg.setSrc(src);
	}
}

6.适配器模式

适配器就是做转换用的,例如你买的台湾省的充电器,不适配河南的插板,所以可以通过适配器来进行适配,让他可以使用河南的插板。

通过代码实现一下,适配我们想要的数据结构

function getUsers(){
	return [{
		name:"zhangsan",
		age:20
		},{
			name:"lisi",
			age:30
		}
		]
}

function Adaptor(users){
	let arr = [];
	for(let i = 0;i<users.length;i++){
		arr[users[i].name] = users[i].age;
	}
	return arr;
}
let res = Adaptor(getUsers());//[zhangsan:20,lisi:30]

axios 就是使用了适配器模式实现的,因为他支持node端和浏览器端

下面介绍一种经典的混入模式来介绍适配器:

class Yase{
	constructor(name){
		this.name = name;
	}
}
class Skills{
	hurt(){
		console.log("造成伤害");
	}
	walk(){
		console.log("走路");
	}
	release(){
		console.log("释放技能");
	}
}

首先我们分析一下上面的代码,亚瑟肯定也有下面的技能但是我们不能把它们写到一起,因为这样违背了单一原则,大部分时候我们肯定会使用继承去实现但是这种方式可以实现我们的需求,但是这些技能不仅仅只是亚瑟的,而且用技能去继承亚瑟,这种逻辑还是有问题的。因为js没有多继承的概念,所以我们使用混入来解决这个问题。

//混入模式
function mixin(receivingClass,givingClass){
	if(typeof arguments[2] !== "undefined"){
		for(let i=2;i<arguements.length;i++){
		//给被混入的对象原型链上添加方法;
			receivingClass.prototype[arguement[i]] = givingClass.prototype[arguement[i]];
		}
	}
}
//混入的方法,所以可以控制混入哪些方法;
mixin(Yase,Skills,"hurt","walk","release");

let yase = new Yase();
console.log(yase);//就具备了这些混入的方法;

7.享元模式

运用共享技术来有效的支持对象的复用,以减少创建的对象的数量,通过共享对象解决内存资源,提高性能和效率。

通过下面骑马的例子,让我们更清楚的了解享元模式:

let userNumber = 10;// 单数是大人,双数是小孩;
let houseNum = 0;
class CreateHorse{
	constructor(){
		horseNum++;
		this.horseNum = horseNum;
		this.type = type? "大马":"小马";
	}
	ride(uid){
		console.log(`人${uid}在骑${this.$horseNum}`);
	}
}

for(let i= 1;i<userNumber;i++){
	let horse;
	if(i%2 === 0){
		horse = new CreateHorse(false);
	}else{
		horse = new CreateHorse(true);
	}
	horse.ride(i);
}
console.log(horseNum);//10  通过上面的方式要实例化10次

我们通过享元模式改变一下上面的方式来实现:

let smallHorse = new CreateHorse(false);
let bigHorse = new CreateHorse(true);
for(let i= 1;i<userNumber;i++){
	let horse;
	if(i%2 === 0){
		smllHorse.ride(i)
	}else{
		bigHorse.ride(i)
	}
	horse.ride(i);
}
console.log(horseNum);//2 通过享元模式改进,这次只实例化2次

改进享元模式:异步加载的形式:

//改进享元模式
let horseNum = 0;
class CreateHorse(){
	constructor(type){
		horseNum ++;
		this.horseNum = horseNum;
		this.type = type?'大马':'小马';
		this.finish = true;
	}
	ride(uid){
		return new Promise(resolve=>{
			console.log(`人${uid}在骑${this.horseNum}${this.type}`);
			this.finish = false;
			setTimeout(()=>{
				resovle(`人${uid}在骑${this.horseNum}${this.type}骑行完毕`);
				this.finish = true;
			},2000);
		})
	}
}

class HorsePool{
	constructor(){
		//共享三匹马,轮流来骑  (共享资源,享元模式的一种体现)
		 this.horse = [new CreateHorse(true),new CreateHorse(true),new CreateHorse(true)];
		 this.peopele = [1,2,3,4,5,6,7,8,9,10];
	}
	rideHorse(){
		this.peopele.forEach(uid=>{
			let horse = this.getHorse();
			if(horse){
				horse.ride(uid).then(res=>{
					console.log(res);
					//如果还有人没骑马那就继续执行,直到都骑完;
					this.peopele.shift() && this.rideHorse() && this.peopele.length;
				})
			}
		})
	}
	getHorse(){
		return this.horse.find(item=>item.finish);
	}
}
let horsePool = new HorsePool();
horsePool.rideHorse();

前端方面的应用就是,线程池和连接池等;

8.相关思考

1.(观察者模式)实现一个removeEvent

class MyEvent{
	 constructor(){
		this.handles = {};
	}
	addEvent(eventName,fn){
		//一个事件可以对应多个函数:{myevent:[fn1,fn2,fn3....]}
		if(typeof this.handles[eventName] === "undefined"){
			this.handles[evnetName] = [];
		}
		this.handles[eventName].push(fn);
	}
	//实现移除函数方法
	removeEvent(eventName,fn){
	if(!(evnetName in this.handles)){
		return;
	}
	for(let i=0;i<this.handles[evnetName].length;i++){
		if(this.handles[eventName][i] === fn){
			this.handles[eventName].splice(i,1);
			break;
		}
	}
}
	trigger(eventName){
		if(!eventName in this.handles){
			return;
		}
		this.handles[eventName].forEach(fn=>fn())
	}
}


2.自定义事件

直接来看代码:

// gameEvent.js
export defalut class GameEvent{
	 constructor(){
		this.handles = {};
	}
	addEvent(eventName,fn){
		//一个事件可以对应多个函数:{myevent:[fn1,fn2,fn3....]}
		if(typeof this.handles[eventName] === "undefined"){
			this.handles[evnetName] = [];
		}
		this.handles[eventName].push(fn);
	}
	//实现移除函数方法
	removeEvent(eventName,fn){
	if(!(evnetName in this.handles)){
		return;
	}
	for(let i=0;i<this.handles[evnetName].length;i++){
		if(this.handles[eventName][i] === fn){
			this.handles[eventName].splice(i,1);
			break;
		}
	}
}
	trigger(eventName){
		if(!eventName in this.handles){
			return;
		}
		this.handles[eventName].forEach(fn=>fn())
	}
	
}
//hero.js
import GameEvent from '.gameEvent.js'
//基类
export default class AbstractHero extends GameEvent{
	constructor({name,ico,skills}){
		//继承必须添加的super()
		super();
		if(new.target === AbstractHero){
			throw new Eroor("AbstractHero不能被实例化");
		}
		this.name = name;
		this.ico = ico;
		this.skills = skills;
		//绑定init 自定义事件 this.addEvnet("init",this.init);
		this.addEvent("init",this.init);
	}
	init(){
		console.log("初始化");
	}
}
//game.js
export default class Game{
	constractor(){
		this.player = null;
	}
	login(name){
		//触发init执行
		this.player = new Player(name);
		//player 玩家创建的时候,英雄肯定也创建了,英雄继承了gameEvent事件,所以每个实例化的英雄都具备了init这个方法;
		this.player.heros.forEach(hero=>{
			hero.trigger("init");
		})
	}
}