最近在看别人写的文章的时候,提到了开放封闭和最少知识原则,一脸懵逼…所以特来恶补。
参考1:【六大原则,写的挺好,因为涉及到java感觉有点吃力,接口啥玩意,抽象类】 https://www.cnblogs.com/yeya/p/10655760.html
参考2:【这个链接很多设计模式可以看】http://c.biancheng.net/view/1324.html
参考3:【这个讲的是依赖倒置原则,我觉得讲的挺好的】http://blog.sina.com.cn/s/blog_855ecd790102vzn5.html
官方定义
单一职责原则 | 类发生更改的原因应该只有一个 |
---|---|
开放封闭原则 | 一个软件实体如类、模块和函数应该对扩展开放,对修改关闭 |
里氏替换原则 | 如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型 |
接口隔离原则 | 客户端不应该依赖它不需要的接口 |
依赖倒置原则 | 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象; |
最少知识原则/迪米特原则 | 一个对象应该对其他对象有最少的了解 |
查阅资料发现少有javascript语言解释六大原则的,都是java,看的我有点费劲,尤其是里氏替换原则那块,如果大家有好的助于理解,请在评论区留言告知我。
以下是大白话,助理解:
1. 单一职责原则,意思就是一个类只担负一个职责
举例:在创业公司里,由于人力成本控制和流程不够规范的原因,往往一个人需要担任N个职责,一个工程师可能不仅要出需求,还要写代码,甚至要面谈客户,代码如下:
class Engineer {
makeDemand(){}
writeCode(){}
meetClient(){}
}
这种写法很明显不符合单一职责的原则,因为引起类的变化不只有一个,至少有三个方法都可以引起类的变化,比如有天因为业务需要,出需求的方法需要加个功能
(比如需求的成本分析),或者是见客户也需要个参数之类的,那样一来类的变化就会有多种可能性了,其他引用该类的类也需要相应的变化,如果引用类的数目很多的话,代码维护的成本可想而知会有多高。所以我们需要把这些方法拆分成独立的职责,可以让一个类只负责一个方法,每个类只专心处理自己的方法即可。
优点:
- 复杂性降低,职责明确;
- 可读性、可维护性提高;
2. 开放封闭原则,也就是说,一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码实现变化。这是为软件实体的未来事件而制定的对现行开发设计进行约束的一个原则。
举例:比如我们要开发一个超市结账的程序,我们写一个类,如下:
class Bill{
// 获取总价
getPrice() {
val unit = getUnit()
val number = getNumber()
val price = unit * number
return price
}
// 获取单价
getUnit() {}
// 获取数量
getNumber() {}
}
}
新来了一个需求,希望我们在七夕节当天,全部打7.7折扣
class Bill{
// 获取总价
getPrice() {
val unit = getUnit()
val number = getNumber()
val price = unit * number
if (todayIsLoversDay()) {
return price * 0.77
}
return price
}
// 获取单价
getUnit() {}
// 获取数量
getNumber() {}
}
}
这里写明显不符合开闭原则,我们应该对打折这种情况进行扩展。
3. 里氏替换原则,通俗来讲子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
举例:拿著名的长方形和正方形的例子,正方形是一种特殊的长方形,如果将正方形设计为长方形的子类,不符合里氏替换原则
// 长方形
class Rectangle {
width;
height;
setWidth(width) {
this.width = width;
}
getWidth() {
return this.width;
}
setHeight(height) {
this.height = height;
}
getHeight() {
return this.height;
}
}
// 正方形继承长方形
class Square extends Rectangle{
side;
setWidth(width) {
this.side = width;
}
getWidth() {
return this.side;
}
setHeight(height) {
this.side = height;
}
getHeight() {
return this.side;
}
}
function resize(r) {
while (r.getHeight() <= r.getWidth()) {
r.setHeight(r.getHeight() + 1);
}
}
调用resize方法的时候,如果传入的是父类(长方形),程序正常,如果传入的是子类(正方形),将会陷入死循环,按照里氏替换原则,应该是父类可以的地方,子类都可以,这违背了里氏替换原则,原因是因为正方形类重写set,get函数,正确的做法是取消正方形原来的继承关系,定义长方形和正方形的父类,比四边形类,他们都有长和宽。
4. 接口隔离原则,意思就是客户端需要什么接口就提供什么接口,把不需要的接口剔除掉,这就需要对接口进行细化,保证接口的纯洁性。换成另一种说法就是,类间的依赖关系应该建立在最小的接口上,也就是建立单一的接口。
class Animal {
eat() {}
fly() {}
run() {}
}
class Dog extend Animal {
}
class Bird extend Animal {
}
发现问题没,狗是不会飞的,他只需要吃和跑两个方法,但是却继承了他不需要的方法,同样,鸟是不会跑的,它只需要吃和飞。正确的写法应该是
class Animal {
eat() {}
}
class Dog extend Animal {
run() {}
}
class Bird extend Animal {
fly() {}
}
5. 依赖倒置原则,是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口是来实现的,这就是俗称的面向接口编程(目的是通过要面向接口的编程来降低类间的耦合性)
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率。
举例:场景是这样的,母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了。
function Book(){
this.getContent=function(){
return "很久很久以前有一个阿拉伯的故事……";
}
}
function Mother(){
this.narrate=function(book){
alert("妈妈开始讲故事");
alert(book.getContent());
}
}
var mother = new Mother();
mother.narrate(new Book());
运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:
function Newspaper(){
this.getContent=function(){
return "林书豪38+7领导尼克斯击败湖人……";
}
}
这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:
function IReader(){
this.getContent=function(){};
}
Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:
function Newspaper(){
IReader.call(this); //继承
this.getContent=function(){
return "林书豪17+9助尼克斯击败老鹰……";
}
}
function Book(){
IReader.call(this); //继承
this.getContent=function(){
return "很久很久以前有一个阿拉伯的故事……";
}
}
function Mother(){
this.narrate=function(reader){
alert("妈妈开始讲故事");
alert(reader.getContent());
}
}
var mother = new Mother();
mother.narrate(new Book());
mother.narrate(new Newspaper());
运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
妈妈开始讲故事
林书豪17+9助尼克斯击败老鹰……
这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。这只是一个简单的例子,实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。
采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大
优点: 可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险
6. 最少知识原则/迪米特原则,也就是说,一个类应该对自己需要耦合或调用的类知道的最少,类与类之间的关系越密切,耦合度越大,那么类的变化对其耦合的类的影响也会越大,这也是我们面向设计的核心原则:低耦合,高内聚。
举例,老师让学生去拿10个篮球。
class Teacher {
command(monitor) {
var list = [];
for(var i = 0 ;i < 10; i++) {
list.push(new BasketBall());
}
monitor.takeball(list)
}
}
class Student {
takeball(list) {
console.log('篮球数量:', list.length)
}
}
class BasketBall {
}
var teacher = new Teacher();
teacher.command(new Student());
结果是正确的,但我们的程序其实还是存在问题,从场景来说,老师只需命令班长拿篮球即可,Teacher只需要一个朋友----Monitor,但在程序里,Teacher的方法体中却依赖了BasketBall类,也就是说,Teacher类与一个陌生的类有了交流,这样Teacher的健壮性就被破坏了,因为一旦BasketBall类做了修改,那么Teacher也需要做修改,这很明显违背了迪米特法则。
因此,我们需要对程序做些修改,在Teacher的方法中去掉对BasketBall类的依赖,只让Teacher类与朋友类Monitor产生依赖,修改后的代码如下:
class Teacher {
command(monitor) {
monitor.takeball()
}
}
class Student {
takeball() {
var list = [];
for(var i = 0 ;i < 10; i++) {
list.push(new BasketBall());
}
console.log('篮球数量:', list.length)
}
}
class BasketBall {
}
var teacher = new Teacher();
teacher.command(new Student());
优点: 减少依赖,解耦。