二十多种设计模式的遵循的原则,简称为SOLID原则,通常是的是以下五大原则:
有的资料中描述为六大原则,较上面相比新加入了:
又有的资料描述为七大原则,较六大原则而言新加入了:
一个类(Class)、函数(Function)甚至是模块(Module)的功能尽可能精简,甚至是只能有一个
减少复杂度
一个200行代码实现的复杂功能,可以使用几个功能精简而且清晰的简单函数去组合实现
在日后修改和维护这个功能时,往往更容易读懂,更容易修改(只需修改一两个简单函数)
现实现一个计算函数,会根据运算提示词进行四则运算
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
default:
return 'Invalid operator';
}
}
// Examples
calculate(1, 2, '+'); // 3
calculate(5, 2, '-'); // 3
calculate(10, 5, '*'); // 50
calculate(20, 5, '/'); // 4
上述函数虽然一个函数就解决了四则运算,但是这个一个函数略显复杂,现拆分为多个个精简而清晰的函数
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return add(num1, num2);
case '-':
return subtract(num1, num2);
case '*':
return multiply(num1, num2);
case '/':
return divide(num1, num2);
default:
return 'Invalid operator';
}
}
function add(num1, num2) {
return num1 + num2;
}
function subtract(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
function divide(num1, num2) {
if (num2 !== 0) {
return num1 / num2;
} else {
return 'Invalid division';
}
}
这里将四则运算的实现方法抽离出来了
优点是:
缺点是:
一个类(Class)、函数(Function)甚至是模块(Module)的功能对于扩展“开放”,对于修改则应是“封闭”
在实现新功能时能保持已有代码不变
在团队开发协作中,需要在已有功能基础上添加新功能,通常我们希望不直接修改原来的代码,而是能够将新增的代码合并进去
再口语化一点,我们希望能保留之前写的代码(即封闭修改),同时又能较为容易的加入后来写的代码(即开放扩展)
现实现一个计算函数,会根据运算提示词进行四则运算
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
default:
return 'Invalid operator';
}
}
// Examples
calculate(1, 2, '+'); // 3
calculate(5, 2, '-'); // 3
calculate(10, 5, '*'); // 50
calculate(20, 5, '/'); // 4
上述函数虽然一个函数就解决了四则运算,但是这个函数接收的运算只能是加减乘除,现改为可以接受其他运算符的函数
const operators = {
'+': (num1, num2) => num1 + num2,
'-': (num1, num2) => num1 - num2,
'*': (num1, num2) => num1 * num2,
'/': (num1, num2) => num1 / num2,
};
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
if (operator in operators) {
return operators[operator](num1, num2);
} else {
return 'Invalid operator';
}
}
这里将运算符以及对应的实现方法抽离出来了
优点是:
operators
里添加新的运算符以及对应的实现方法)calculate
函数无需修改)缺点是:
扩展一个类(Class)、函数(Function)甚至是模块(Module)的功能需要保持其兼容性,基类实现的功能扩展的子类也应当实现
保持兼容性
有一个编辑器软件的某一模块,是实现JavaScript代码语法检查,现欲添加支持TypeScript代码语法检查,那么一个比较好的思路是在原有支持JavaScript语法检查的基础上添加TypeScript支持(即兼容原有功能),而不是修改为只支持TypeScript
现实现一个计算函数,会根据运算提示词进行四则运算
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
default:
return 'Invalid operator';
}
}
// Examples
calculate(1, 2, '+'); // 3
calculate(5, 2, '-'); // 3
calculate(10, 5, '*'); // 50
calculate(20, 5, '/'); // 4
上述函数虽然一个函数就解决了四则运算,但是这个函数接收的运算只能是加减乘除,现改为扩展为一个能接受其他运算符的函数
function newCalculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
case '//':
return num1 / num2 / num2;
default:
return 'Invalid operator';
}
}
这里添加了一个//
运算符(自定义的)
优点是:
缺点是:
扩展的类(Class)、函数(Function)甚至是模块(Module)不应出现其不使用的方法
简化扩展后的代码
为了保持兼容性,在扩展一个原有基类时我们会(甚至是不得不)实现其原定义的所有方法,如果有的方法是用不到的,这就会显得代码冗余
再举一个例子,有一个编辑器软件的某一模块,是实现JavaScript代码语法检查和代码风格检查,现欲添加支持TypeScript代码语法检查而不需要代码风格检查,需求如此所以就应该只需要TypeScript代码语法检查而不需要代码风格检查,但是由于我们扩展的这个模块规定了需要实现代码风格检查,所以不得不实现代码风格检查,但这是冗余的功能,不是我们需要的
解决办法之一就是拆分原有基类,扩展的新类只需继承需要的那几个基类即可
现实现一个计算函数,会根据运算提示词进行四则运算
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
default:
return 'Invalid operator';
}
}
// 扩展的新函数(必须实现原有的功能)
function newCalculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
case '//':
return num1 / num2 / num2;
default:
return 'Invalid operator';
}
}
上述函数虽然一个函数就解决了四则运算,但是扩展这个函数就必须实现加减乘除,现拆分为四个基函数,并扩展出一个新的calculate
函数
function add(num1, num2) {
return num1 + num2;
}
function subtract(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
function divide(num1, num2) {
if (num2 !== 0) {
return num1 / num2;
} else {
return 'Invalid division';
}
}
// 扩展的新函数(必须实现原有的功能,因为扩展的加减函数,所以只需实现加减功能)
function newCalculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return add(num1, num2);
case '-':
return subtract(num1, num2);
case '//':
return num1 / num2 / num2;
default:
return 'Invalid operator';
}
}
这里新增加的newCalculate
函数就不那么冗余
优点是:
缺点是:
上层模块不应该依赖底层模块,它们都应该依赖于抽象
抽象不应该依赖于细节,细节应该依赖于抽象
保持上层模块与底层模块的相对独立
在现在流行的前后端分离的开发模式下,前端与后端开发人员会在开始前约定接口(即API),然后前后端可以开始同时开发,前端开发人员无需等待后端开发好即可预知后端数据API,而前端开发好后,后端修改也无需前端再次修改,只需保证API与原来一致即可
可以理解为,前端就是上层模块,它不关注后端怎么实现的,只需要符合约定的API即可,后端是底层模块,它怎么实现都行,但是要符合约定的API,前端和后端相互独立
如果没有约定API,那么前端接收到什么数据由后端决定,这就是上层模块依赖底层模块,而API约定就是实现依赖倒置
现实现一个计算函数,会根据运算提示词进行四则运算
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
switch(operator) {
case '+':
return num1 + num2;
case '-':
return num1 - num2;
case '*':
return num1 * num2;
case '/':
return num1 / num2;
default:
return 'Invalid operator';
}
}
现将上述calculate
函数改为由外部operators
决定具体运算
const operators = {
'+': (num1, num2) => num1 + num2,
'-': (num1, num2) => num1 - num2,
'*': (num1, num2) => num1 * num2,
'/': (num1, num2) => num1 / num2,
};
function calculate(num1, num2, operator) {
num1 = parseFloat(num1);
num2 = parseFloat(num2);
if (operator in operators) {
return operators[operator](num1, num2);
} else {
return 'Invalid operator';
}
}
这里将运算符以及对应的实现方法抽离出来了,具体的运算由外部operators
实现
优点是:
operators
里修改)缺点是:
较五大原则新加入了迪米特法则(Law of Demeter)
只与你的直接朋友交谈,不跟“陌生人”说话
一个类(Class)、函数(Function)甚至是模块(Module)应当尽可能少的与其他实体发生相互作用
降低耦合度,提高模块的相对独立性
军队里面有元帅、军官和士兵,元帅认识军官,军官认识自己管辖的士兵。元帅要攻击敌军,他不必直接对士兵下命令,只需要下命令给自己认识的军官,由军官将指令转发给自己所辖士兵即可。用迪米特法则解释,元帅和军官、军官和士兵是“朋友”,元帅和士兵是“陌生人”,元帅只应该与自己直接的“朋友”——军官说话,不要跟“陌生人”——士兵说话
现给出一个四则运算的Calculator
类,它使用外部的加减乘除函数实现四则运算
function add(num1, num2) {
return num1 + num2;
}
function subtract(num1, num2) {
return num1 - num2;
}
function multiply(num1, num2) {
return num1 * num2;
}
function divide(num1, num2) {
if (num2 !== 0) {
return num1 / num2;
} else {
return 'Invalid division';
}
}
class Calculator {
#num1;
#num2;
calculate(num1, num2, operator) {
this.#num1 = parseFloat(num1);
this.#num2 = parseFloat(num2);
switch(operator) {
case '+':
return add(this.#num1, this.#num2);
case '-':
return subtract(this.#num1, this.#num2);
case '*':
return multiply(this.#num1, this.#num2);
case '/':
return divide(this.#num1, this.#num2);
default:
return 'Invalid operator';
}
}
现修改为Calculator
调用内部函数
class Calculator {
#num1;
#num2;
#add() {
return this.#num1 + this.#num2;
}
#subtract() {
return this.#num1 - this.#num2;
}
#multiply() {
return this.#num1 * this.#num2;
}
#divide() {
return this.#num1 / this.#num2;
}
calculate(num1, num2, operator) {
this.#num1 = parseFloat(num1);
this.#num2 = parseFloat(num2);
switch(operator) {
case '+':
return this.#add();
case '-':
return this.#subtract();
case '*':
return this.#multiply();
case '/':
return this.#divide();
default:
return 'Invalid operator';
}
}
}
修改后的类,只调用了内部方法,同时只暴露了calculate
函数给外部
优点是:
缺点是:
较六大原则新加入了较上面相比新加入了组合/聚合复用原则 (Composite/Aggregate Reuse Principle)
优先使用组合或聚合关系,而不是继承关系
提高代码的灵活性和可维护性
一台计算机,是由CPU、内存、输入设备、输出设备和外存等组装而成。计算机对象为整体,CPU、内存、输入设备、输出设备和外存等为部分,它们是聚合关系。如果一台计算机没有打印功能,可以加入一个打印机,使打印机成为计算机的一部分,从而重用打印机的打印功能。换一种角度来看,如果需要计算机有打印的责任,那么就可以将该责任委托给作为部分的打印机
使用组合/聚合实现复用有如下好处:
当然,这种复用也有缺点。其中最主要的缺点就是系统中会有较多的对象需要管理
继承是面向对象语言特有的复用工具。由于使用继承关系时,新的实现较为容易,因父类的大部分功能可以通过继承的关系自动进入子类;同时,修改和扩展继承而来的实现较为容易。于是,在面向对象设计理论的早期,程序设计师十分热衷于继承,好像继承就是最好的复用手段,于是继承也成为了最容易被滥用的复用工具。然而,继承有多个缺点:
正是因为继承有上述缺点,所以应首先使用组合/聚合,其次才考虑继承,达到复用的目的
[1] 六大设计原则(SOLID) - 简书 (jianshu.com)