设计模式笔记

概述

《设计模式》这本书完全是从面向对象设计的角度出发的,通过对封装、继承、多态、组合等技术的反复使用,提炼出一些可重复使用的面向对象设计技巧.

所以有一种说法是设计模式仅仅是就面向对象的语言而言的, 其实际上是解决某些问题的一种思想.

❗❗❗❗❗❗ 所有设计模式的实现都遵循一条原则, 即"找出程序中变化的地方, 并将变化封装起来".

相关概念

多态

多态的实际含义是:同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。 换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈.

多态在设计模式中是重中之重,绝大部分设计模式的实现都离不开多态性的思想.

多态背后的思想是将"做什么"和"谁去做以及怎样去做"分离开来,也就是将"不变的事物"与 "可能改变的事物"分离开来.

举例: 分别调用一个猫对象和一个狗对象的叫方法, 猫则是喵~, 狗则旺~, 这就是多态

js 复制代码
// 动物例子
class Cat {
  call() {
    console.log('喵喵喵');
  }
}
class Dog {
  call() {
    console.log('汪汪汪');
  }
}
const makeSound = (animal) => animal.call()
makeSound(new Cat())
makeSound(new Dog())

// 地图例子
const renderMap = function (map) {
  if (map.show instanceof Function) {
    map.show();
  }
};

const googleMap = {
  show: function () {
    console.log('开始渲染谷歌地图');
  },
};
const baiduMap = {
  show: function () {
    console.log('开始渲染百度地图');
  },
};
const sosoMap = {
  show: function () {
    console.log('开始渲染搜搜地图');
  },
};

renderMap(googleMap); // 输出:开始渲染谷歌地图
renderMap(baiduMap); // 输出:开始渲染百度地图
renderMap(sosoMap); // 输出:开始渲染搜搜地图

Martin Fowler 在《重构:改善既有代码的设计》: 多态的最根本好处在于,你不必再向对象询问"你是什么类型"而后根据得到的答案调用对象的某个行为------你只管调用该行为就是了,其他的一切多态机制都会为你安排妥当。

再来个形象的场景例子:

在电影的拍摄现场,当导演喊出"action"时,主角开始背台词,照明师负责打灯光,后面的群众演员假装中枪倒地,道具师往镜头里撒上雪花。在得到同一个消息时,每个对象都知道自己应该做什么。

如果不利用对象的多态性,而是用面向过程的方式来编写这一段代码,那么相当于在电影开始拍摄之后,导演每次都要走到每个人的面前,确认它们的职业分工(类型),然后告诉他们要做什么。如果映射到程序中,那么程序中将充斥着条件分支语句。

封装

封装的目的是将信息隐藏, 包括封装数据, 封装实现, 封装变化.

AOP

AOP(面向切面编程)是把跟 核心业务逻辑无关且频繁使用的公共功能或操作抽离出来 ,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过"动态织入"的方式掺入业务逻辑模块中。

优点:

  • 将分散在不同地方的通用功能集中管理, 方便复用
  • 不影响原有业务逻辑代码, 保持业务逻辑模块的纯净和高内聚性

函数柯里化

currying 又称部分求值。一个 currying 的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。

简言之, 就是保存参数, 最后统一使用.

js 复制代码
var currying = function (fn) {
  var args = [];
  return function () {
    if (arguments.length === 0) {
      return fn.apply(this, args);
    } else {
      [].push.apply(args, arguments);
      return arguments.callee;
    }
  };
};

var cost = function () {
  var money = 0;
  for (var i = 0, l = arguments.length; i < l; i++) {
    money += arguments[i];
  }
  return money;
};

var cost = currying(cost); // 转化成 currying 函数
cost(100); // 未真正求值
cost(200); // 未真正求值
cost(300); // 未真正求值
alert(cost()); // 求值并输出:600

单例模式

保证一个类仅有一个实例,并提供一个访问它的全局访问点. 单例模式也适用于防止某些操作被重复执行.

重点: 有且仅有一个全局可访问的实例

js 复制代码
function Person(name) {
  this.name = name;
}
Person.prototype.getName = function () {
  return this.name;
};
// 使用闭包, 内部维护私有instance变量且不污染全局作用域
Person.getInstance = (function () {
  let instance = null;
  return function (name) {
    if (!instance) {
      instance = new Person(name);
    }
    return instance;
  };
})();

var a = Person.getInstance('sven1');
var b = Person.getInstance('sven2');
alert(a === b);

// 进一步简化封装, fn相当于初始化函数, 执行后提供初始单例对象
var getSingle = function (fn) {
  var result;
  return function () {
    return result || (result = fn.apply(this, arguments));
  };
};
Person.getInstance = getSingle(() => new Person('sven3'));

单例模式是一种简单但非常实用的模式,特别是惰性单例技术,在合适的时候才创建对象,并且只创建唯一的一个。更奇妙的是,创建对象和管理单例的职责被分布在两个不同的方法中,这两个方法组合起来才具有单例模式的威力

原型模式

原型模式是用于创建对象的一种模式, 一般而言有两种方法:

  1. 通过对象的构造函数创建
  2. 未知对象类型时, 可通过克隆复制一个一模一样的对象
js 复制代码
function Person(name, age) {
  this.name = name;
  this.age = age;
}

// 构造函数创建对象
const person = new Person('John', 30);
// 克隆创建对象
const copy1 = Object.create(person);

const copy2 = new Object();
Object.setPrototypeOf(copy2, person); // 等价于 create2.__proto__ = person;

策略模式

策略模式定义:定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换

"条条大路通罗马", 要实现某一个功能有多种方案可以选择。比如一个压缩文件的程序,既可以选择 zip 算法,也可以选择 gzip 算法。这些算法灵活多样,而且可以随意互相替换。这种解决方案就是本章将要介绍的策略模式。

一个基于策略模式的程序至少由两部分组成:

  • 第一个部分是一组策略类,策略类封装了具体的算法,并负责具体的计算过程。
  • 第二个部分是环境类 Context,Context 接受客户的请求,随后把请求委托给某一个策略类。
js 复制代码
// 策略类, 封装了绩效的具体计算方法
class PerformanceS {
  calculate(salary) {
    return salary * 4;
  }
}

class PerformanceA {
  calculate(salary) {
    return salary * 3;
  }
}

class PerformanceB {
  calculate(salary) {
    return salary * 2;
  }
}

// 环境类, 接收请求的原始数据与策略对象, 并将原始数据委托给策略对象去执行
class Bonus {
  constructor() {
    this.salary = null;   
    this.strategy = null; 
  }
  setSalary(salary) {
    this.salary = salary; 
  }
  setStrategy(strategy) {
    this.strategy = strategy;
  }
  getBonus() {
    return this.strategy.calculate(this.salary);
  }
}

// 使用示例:
// const bonus = new Bonus();
// bonus.setSalary(10000);
// bonus.setStrategy(new PerformanceS());
// console.log(bonus.getBonus());

再如: 表单检验, 不同的校验规则可以封装到一个策略类Strategy中, 再通过一个校验类Validator去管理多个不同校验规则以及触发校验, 最后返回校验结果.

js 复制代码
/***********************策略对象**************************/
const strategies = {
  isNonEmpty(value, errorMsg) {
    if (value === '') {
      return errorMsg;
    }
  },
  minLength(value, length, errorMsg) {
    if (value.length < Number(length)) {
      return errorMsg;
    }
  },
  isMobile(value, errorMsg) {
    if (!/(^1[3|5|8][0-9]{9}$)/.test(value)) {
      return errorMsg;
    }
  },
};

/***********************Validator 类**************************/
class Validator {
  constructor() {
    this.cache = []; // 保存校验规则
  }

  add(dom, rule, errorMsg) {
    const parts = rule.split(':'); // 把 strategy 和参数分开
    const strategy = parts.shift(); // 用户挑选的 strategy
    this.cache.push(() => {
      // 把校验的步骤用函数包装起来,并且放入 cache
      const args = [dom.value, ...parts, errorMsg]; // 参数列表:value + 规则参数 + 错误信息
      return strategies[strategy].apply(dom, args);
    });
  }

  start() {
    for (const validatorFunc of this.cache) {
      const msg = validatorFunc(); // 开始校验,并取得校验后的返回信息
      if (msg) {
        // 如果有确切的返回值,说明校验没有通过
        return msg;
      }
    }
    return null;
  }
}

/***********************使用示例**************************/
const registerForm = document.getElementById('registerForm');

const validateFunc = () => {
  const validator = new Validator(); // 创建一个 validator 对象
  /***************添加一些校验规则****************/
  validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空');
  validator.add(registerForm.password, 'minLength:6', '密码长度不能少于 6 位');
  validator.add(registerForm.phoneNumber, 'isMobile', '手机号码格式不正确');
  const errorMsg = validator.start(); // 获得校验结果
  return errorMsg; // 返回校验结果
};

registerForm.onsubmit = function () {
  const errorMsg = validateFunc(); // 如果 errorMsg 有确切的返回值,说明未通过校验
  if (errorMsg) {
    alert(errorMsg);
    return false; // 阻止表单提交
  }
};

代理模式

代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问 ,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。 本质上代理就是一个中间协助的"助理"角色, 协助做一些事情.

基本示例:

  • 经纪人是明星的代理, 经纪人会把商业演出的细节和报酬都谈好之后,再把合同交给明星签.
  • 香港带货, 到香港进货的人是客户的代理
js 复制代码
var myImage = (function () {
	var imgNode = document.createElement('img');
	document.body.appendChild(imgNode);
	return {
		setSrc: function (src) {
			imgNode.src = src;
		},
	};
})();
var proxyImage = (function () {
	var img = new Image();
	img.onload = function () {
		myImage.setSrc(this.src);
	};
	return {
		setSrc: function (src) {
			myImage.setSrc('file:// /C:/Users/svenzeng/Desktop/loading.gif');
			img.src = src;
		},
	};
})();
proxyImage.setSrc('http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg');

代理和本体接口的一致性, 如上例中的本体和代理都对外提供了 setSrc 方法

  • 在用户看来, 代理和本体是一致的, 用户只关心能否得到想要的结果
  • 任何使用本体的地方都可以替换成使用代理

接口的一致性使得本体和代理可以随时灵活互相替换和切换.

一般会以代理实际做的事情来命名代理, 如: 缓存代理, 顾名思义该代理是专门用于为本体做一些缓存的.

缓存代理:

js 复制代码
var mult = function () {
	console.log('开始计算乘积');
	var a = 1;
	for (var i = 0, l = arguments.length; i < l; i++) {
		a = a * arguments[i];
	}
	return a;
};
mult(2, 3); // 输出:6
mult(2, 3, 4); // 输出:24

var proxyMult = (function(){ 
 var cache = {}; 
 return function(){ 
 var args = Array.prototype.join.call( arguments, ',' ); 
 if ( args in cache ){ 
 return cache[ args ]; 
 } 
 return cache[ args ] = mult.apply( this, arguments ); 
 } 
})(); 
 proxyMult( 1, 2, 3, 4 ); // 输出:24 
 proxyMult( 1, 2, 3, 4 ); // 输出:24

好处显而易见, 职责单一明确且高内聚低耦合, mult只关注乘积计算的实现即可, 代理只负责为mult本体添加缓存. 假设需要修改 mult 的实现方法, 不会影响缓存代理; 同理, 假设后续需要改变缓存策略或者删除缓存, 直接将代理替换回本体的 mult 即可, 非常方便灵活.

迭代器模式

迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。迭代器模式可以把迭代的过程从业务逻辑中分离出来,在使用迭代器模式之后,即使不关心对象的内部构造,也可以按顺序访问其中的每个元素。

发布-订阅模式

即观察者模式, 简言之, 预先订阅一个函数或事件, 在某个特定的异步时刻这个函数或事件将会被触发.

最典型的例子: 你关注(订阅)某个主播, 当主播发布新作品时(状态发生改变), 所有关注了这个主播的粉丝都会收到新作品内容的通知(发布通知).

通用实现:

js 复制代码
const event = (function () {
	const clientList = [];
	function listen(key, fn) {
		if (!this.clientList[key]) {
			this.clientList[key] = [];
		}
		this.clientList[key].push(fn);
	}
	function trigger() {
		let key = Array.prototype.shift.call(arguments),
			fns = this.clientList[key];
		if (!fns || fns.length === 0) {
			// 如果没有绑定对应的消息
			return false;
		}
		for (let i = 0, fn; (fn = fns[i++]); ) {
			fn.apply(this, arguments);
		}
	}
	function remove(key, fn) {
		var fns = this.clientList[key];
		if (!fns) {
			return false;
		}
		if (!fn) {
			fns && (fns.length = 0);
		} else {
			for (var l = fns.length - 1; l >= 0; l--) {
				var _fn = fns[l];
				if (_fn === fn) {
					fns.splice(l, 1);
				}
			}
		}
	}

	return {
		listen,
		remove,
		trigger,
	};
})();

Event.listen('alltoowell', function (...args) {
	console.log(args);
});
Event.trigger('alltoowell', 666);

命令模式

命令模式中的命令(command)指的是一个执行某些特定事情的指令

命令模式最常见的应用场景是:有时候需要向某些对象发送请求,但是并不知道请求的接收者是谁,也不知道被请求的操作是什么。此时希望用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系。

即发送者只关注这个指令的触发与执行的结果, 不关心这个指令由谁执行以及背后做了哪些事情.

示例:

拿订餐来说,客人 需要向厨师发送请求,但是完全不知道这些厨师的名字和联系方式,也不知道厨师炒菜的方式和步骤。命令模式把客人订餐的请求封装成 command 对象,也就是订餐中的订单对象。这个对象可以在程序中被四处传递,就像订单可以从服务员手中传到厨师的手中。这样一来,客人不需要知道厨师的名字,从而解开了请求调用者和请求接收者之间的耦合关系。

组合模式

不变之处 在于都是由若干部分组成, 变之处在于组成部分的数量与顺序等是不确定的.

组合模式就是用小的子对象来构建更大的对象,而这些小的子对象本身也许是由更小的"孙对象"构成的。组合模式将对象组合成树形结构,以表示"部分-整体"的层次结构。

基本对象可以被组合成更复杂的组合对象,组合对象又可以被组合,这样不断递归下去,这棵树的结构可以支持任意多的复杂度。

扫描文件夹

js 复制代码
/******************************* Folder ******************************/
var Folder = function (name) {
	this.name = name;
	this.files = [];
};
Folder.prototype.add = function (file) {
	this.files.push(file);
};
Folder.prototype.scan = function () {
	console.log('开始扫描文件夹: ' + this.name);
	for (var i = 0, file, files = this.files; (file = files[i++]); ) {
		file.scan();
	}
};

/******************************* File ******************************/ 
var File = function( name ){ 
 this.name = name; 
}; 
File.prototype.add = function(){ 
 throw new Error( '文件下面不能再添加文件' ); 
};
File.prototype.scan = function () {
	console.log('开始扫描文件: ' + this.name);
};


var folder = new Folder('学习资料');
var folder1 = new Folder('JavaScript');
var folder2 = new Folder('jQuery');
var file1 = new File('JavaScript 设计模式与开发实践');
var file2 = new File('精通 jQuery');
var file3 = new File('重构与模式');
folder1.add(file1);
folder2.add(file2);
folder.add(folder1);
folder.add(folder2);
folder.add(file3);

模板方法模式

模板方法模式是一种使用继承就可以实现的简单模式。

模板方法模式由两部分结构组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

享元模式

享元(flyweight)模式是一种用于性能优化 的模式,"fly"在这里是苍蝇的意思,意为蝇量级。享元模式的核心是运用共享技术来有效支持大量细粒度的对象。

通用对象池:

js 复制代码
var objectPoolFactory = function (createObjFn) {
	var objectPool = [];
	return {
		create: function () {
			var obj = objectPool.length === 0 ? createObjFn.apply(this, arguments) : objectPool.shift();
			return obj;
		},
		recover: function (obj) {
			objectPool.push(obj);
		},
	};
};

职责链模式

定义: 使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系 ,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止

职责链模式的名字非常形象,一系列可能会处理请求的对象被连接成一条链,请求在这些对象之间依次传递,直到遇到一个可以处理它的对象.

示例:

  • 如果早高峰能顺利挤上公交车的话,那么估计这一天都会过得很开心。因为公交车上人实在太多了,经常上车后却找不到售票员在哪,所以只好把两块钱硬币往前面递。除非你运气够好,站在你前面的第一个人就是售票员,否则,你的硬币通常要在 N 个人手上传递,才能最终到达售票员的手里。

职责链模式的最大优点:请求发送者只需要知道链中的第一个节点,从而弱化了发送者和一组接收者之间的强联系。如果不使用职责链模式,那么在公交车上,我就得先搞清楚谁是售票员,才能把硬币递给他。

js 复制代码
function Chain(fn) {
	this.fn = fn;
	this.nextProcessor = null; // 下一个处理节点
}

Chain.prototype.setNextProcessor = function (nextProcessor) {
	return this.nextProcessor = nextProcessor;
};

Chain.prototype.process = function () {
	const result = this.fn.apply(this, arguments);
	if (result === 'nextProcessor') {
		return this.nextProcessor && this.nextProcessor.process.apply(this.nextProcessor, arguments);
	}
	return result;
};

var order500 = function (orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log('500 元定金预购,得到 100 优惠券');
	} else {
		return 'nextProcessor'; // 我不负责处理本次请求,把请求往后面传递
	}
};
var order200 = function (orderType, pay, stock) {
	if (orderType === 2 && pay === true) {
		console.log('200 元定金预购,得到 50 优惠券');
	} else {
		return 'nextProcessor'; // 我不负责处理本次请求,把请求往后面传递
	}
};
var orderNormal = function (orderType, pay, stock) {
	if (stock > 0) {
		console.log('普通购买,无优惠券');
	} else {
		console.log('手机库存不足');
	}
};

var chainOrder500 = new Chain(order500);
var chainOrder200 = new Chain(order200);
var chainOrderNormal = new Chain(orderNormal);

chainOrder500.setNextProcessor(chainOrder200);
chainOrder200.setNextProcessor(chainOrderNormal);

chainOrder500.process(1, true, 500); // 输出:500 元定金预购,得到 100 优惠券
chainOrder500.process(2, true, 500); // 输出:200 元定金预购,得到 50 优惠券
chainOrder500.process(3, true, 500); // 输出:普通购买,无优惠券
chainOrder500.process(1, false, 0); // 输出:手机库存不足

// 利用js函数式特性, 更简便实现职责链模式; 
Function.prototype.after = function (fn) {
	const self = this;
	return function () {
		const ret = self.apply(this, arguments);
		if (ret === 'nextProcessor') {
			return fn.apply(this, arguments);
		}
		return ret;
	}
}
const order = chainOrder500.after(chainOrder200).after(chainOrderNormal)
order(1, true, 500)

中介者模式

场景描述: 在程序里,也许一个对象会和其他 10 个对象打交道,所以它会保持 10 个对象的引用。当程序的规模增大,对象会越来越多,它们之间的关系也越来越复杂,难免会形成网状的交叉引用(多对多关系)。当我们改变或删除其中一个对象的时候,很可能需要通知所有引用到它的对象.

中介者模式: 解除对象与对象之间的紧耦合关系。增加一个中介者对象后,所有的相关对象都通过中介者对象来通信,而不是互相引用,所以当一个对象发生改变时,只需要通知中介者对象即可。中介者使各对象之间耦合松散,而且可以独立地改变它们之间的交互。中介者模式使网状的多对多关系变成了相对简单的一对多关系.

经典应用场景:

  • 单点登录, 用户在多个相互信任的系统中,只需要进行一次身份验证,就能访问所有授权的系统
  • 统一接口层
  • 团队射击游戏中, 两个队伍的玩家的存活与死亡等信息的通知
    • 每个玩家加入队伍或死亡移出队伍等等的信息通知中介者对象, 由中介者对象去通知所有其他玩家, 从而解耦了玩家与玩家之间直接的多对多关系

装饰者模式

不改变对象自身的基础 上, 动态地给某个对象添加额外的职责,而不会影响从这个类中派生的其他对象.

用AOP装饰函数示例1

js 复制代码
// 装饰 fn 动态添加记录日志的功能, 以及try-catch错误捕获, 更新loading状态
const wrapperFnWithLog = (fn: Function, config: { action: string } | any = {}) => {
  return async (...args: any[]) => {
    const { action, hideSuccess = false } = config;
    try {
      cameraStore.loading = true;
      cameraStore.addLog({
        content: action,
      });
      await fn(...args);
      !hideSuccess &&
        cameraStore.addLog({
          type: 'success',
          content: `${action}成功`,
        });
    } catch (error: any) {
      cameraStore.addLog({
        content: `${action}失败: ${error.message}`,
        type: 'error',
      });
      ElMessage.error(action);
      console.error(`${action}失败: `, error);
    } finally {
      cameraStore.loading = false;
    }
  };
};

用AOP装饰函数示例2

js 复制代码
Function.prototype.before = function (beforefn) {
	var __self = this; // 保存原函数的引用
	return function () {
		beforefn.apply(this, arguments); // 执行新函数,且保证 this 不被劫持,新函数接受的参数
		// 也会被原封不动地传入原函数,新函数在原函数之前执行
		return __self.apply(this, arguments); // 执行原函数并返回原函数的执行结果,
	};
};
Function.prototype.after = function (afterfn) {
	var __self = this;
	return function () {
		var ret = __self.apply(this, arguments);
		afterfn.apply(this, arguments);
		return ret;
	};
};

状态模式

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类.

一个对象有多个不同的状态, 每种状态下执行相同的操作会产生不同的行为, 即类似多态的概念. 其实就是一个状态机, 适用于多种状态间切换去执行不同的操作.

场景例子:

  • 文件上传过程中存在扫描, 上传中, 暂停, 上传成功或失败几种状态
  • 音乐播放器存在加载中, 播放中, 暂停, 播放完毕几种状态

状态模式的通用结构:

  • 存在一个上下文对象context, 其将持有每种状态类的实例的引用
  • 实现各种状态类, 状态类的实例对象将持有context的引用
    • 状态类封装并实现了对应状态下的行为
js 复制代码
var Light = function () {
	this.offLightState = new OffLightState(this); // 持有状态对象的引用
	this.weakLightState = new WeakLightState(this);
	this.strongLightState = new StrongLightState(this);
	this.button = null;
};
Light.prototype.init = function () {
	var button = document.createElement('button'),
		self = this;
	this.button = document.body.appendChild(button);
	this.button.innerHTML = '开关';
	this.currState = this.offLightState; // 设置默认初始状态
	this.button.onclick = function () {
		// 定义用户的请求动作
		self.currState.buttonWasPressed();
	};
};

var OffLightState = function (light) {
	this.light = light;
};
OffLightState.prototype.buttonWasPressed = function () {
	console.log('弱光');
	this.light.setState(this.light.weakLightState);
};

触发当前状态下的操作, 然后再切换到另一个状态.

适配器模式

适配器模式的作用是解决两个软件实体间的接口不兼容的问题。使用适配器模式之后,原本由于接口不兼容而不能工作的两个软件实体可以一起工作。

场景例子:

  • 一拖三数据线, type-a / type-c / usb 等接口互转
  • 扩展坞
  • 电源适配器

设计原则和编程技巧

每种设计模式都是为了让代码迎合其中一个或多个原则而出现的,它们本身已经融入了设计模式之中,给面向对象编程指明了方向。

前辈总结的这些设计原则通常指的是单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、合成复用原则和最少知识原则。

代码优劣的评判标准

  • 主功能入口函数是否足够的精简? 过于冗长会导致业务逻辑的脉络模糊, 可读性差
  • 增删改角度出发, 函数是否具有弹性, 增删改时是否需要深入函数内部实现去操作(开闭原则)
  • 复用性角度考虑, 是否便于类似的应用场景的直接复用
  • 单一职责原则, 面向对象设计提倡将行为分布到细粒度 的对象之中, 即高内聚, 低耦合

单一职责原则

单一职责原则(SRP)的职责被定义为"引起变化的原因"。如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责。 每个职责都是变化的一个轴线,如果一个方法承担了过多的职责,那么在需求的变迁过程中,需要改写这个方法的可能性就越大。

SRP 原则体现为:一个对象(方法)只做一件事情。

然而并不是所有的职责都需要一一分离, 分离的颗粒度以及是否需要分离取决于这些职责互相的关联性, 强关联或者时常一起改变的则不需要分离; 相对固定不变的或者无需考虑复用性的职责也不需要分离.

最少知识原则

又称迪米特法则, 一个软件实体应当尽可能少地与其他实体发生相互依赖。这里的软件实体是一个广义的概念,不仅包括对象,还包括系统、类、模块、函数、变量等。

开闭原则

在项目需求变迁的过程中,我们经常会找到相关代码,然后改写它们。这个过程很容易因为修改了源码引入了更多的bug.

开闭原则: 当需要改变一个程序的功能或者给这个程序增加新功能的时候,可以使用增加代码的方式,但是不允许改动程序的源代码。

js 复制代码
Function.prototype.after = function (afterfn) {
	var __self = this;
	return function () {
		var ret = __self.apply(this, arguments);
		afterfn.apply(this, arguments);
		return ret;
	};
};
window.onload = (window.onload || function () {}).after(function () {
	console.log(document.getElementsByTagName('*').length);
});

其实就是告诫我们不要轻易改动他人的代码, 以防产生某些意料之外的副作用而引入bug.

相关推荐
Focus_2 小时前
如何借助AI在UE5中将图片批量生成3D模型
前端·aigc·游戏开发
@PHARAOH2 小时前
WHAT - Vercel react-best-practices 系列(二)
前端·javascript·react.js
qq_406176142 小时前
深入理解 JavaScript 闭包:从原理到实战避坑
开发语言·前端·javascript
float_六七2 小时前
JavaScript变量声明:var的奥秘
开发语言·前端·javascript
zhengxianyi5152 小时前
ruoyi-vue-pro本地环境搭建(超级详细,带异常处理)
前端·vue.js·前后端分离·ruoyi-vue-pro
桃子叔叔2 小时前
react-wavesurfer录音组件1:从需求到组件一次说清楚
前端·react.js·前端框架
陈随易2 小时前
聊一聊2025年用AI的思考与总结
前端·后端·程序员
@PHARAOH2 小时前
WHAT - React startTransition vs setTimeout vs debounce
前端·react.js·前端框架
绝美焦栖3 小时前
低版本pdfjs升级
前端·javascript·vue.js