SOLID原则都不知道,还敢说自己是搞开发的!
面向对象编程(oop)给软件开发领域带来了新的设计思想。很多开发人员在进行面向对象编程过程中,往往会在一个类中将具有相同目的/功能的代码放在一起,力求以最快的方式解决当下的问题。但是,这种编程方式会导致程序代码混乱和难以维护。因此,robert c. martin制定了面向对象编程的五项原则。这五个原则使得开发人员可以轻松创建可读性好且易于维护的程序。
这五个原则被称为solid原则。
s:单一职责原则
o:开闭原理
l:里氏替换原则
i:接口隔离原理
d:依赖反转原理
我们下面将详细地展开来讨论。
单一职责原则
单一职责原则(single responsibility principle):一个类(class)只负责一件事。如果一个类承担多个职责,那么它就会变得耦合起来。一个职责的变更会导致另一职责的变更。
注意:该原理不仅适用于类,而且适用于软件组件和微服务。
例如,先看看以下设计:
class animal { constructor(name: string){ } getanimalname() { } saveanimal(a: animal) { } }
animal类就违反了单一职责原则。
** 它为什么违反单一职责原则?**
单一职责原则指出,一个类(class)应负一个职责,在这里,我们可以看到animal类做了两件事:animal的数据维护和animal的属性管理。构造方法和getanimalname方法是管理animal的属性,而saveanimal方法负责把数据存放到数据库。
这种设计将来会引发什么问题?
如果animal类的saveanimal方法发生改变,那么getanimalname方法所在的类也需要重新编译。这种情况就像多米诺骨牌效果,碰到了一片骨牌会影响所有其他骨牌。
为了更加符合单一职责原则,我们可以创建了另一个类,该类专门把animal的数据维护方法抽取出来,如下:
class animal { constructor(name: string){ } getanimalname() { } } class animaldb { getanimal(a: animal) { } saveanimal(a: animal) { } }
以上的设计,让我们的应用程序将具有更高的内聚。
开闭原则
开闭原则(open-closed principle):软件实体(类,模块,功能)应该对扩展开放,对修改关闭。
让我们继续上动物课吧。
class animal { constructor(name: string){ } getanimalname() { } }
我们想遍历所有animal,并发出声音。
//... const animals: array<animal> = [ new animal('lion'), new animal('mouse') ]; function animalsound(a: array<animal>) { for(int i = 0; i <= a.length; i++) { if(a[i].name == 'lion') log('roar'); if(a[i].name == 'mouse') log('squeak'); } } animalsound(animals);
该函数animalsound不符合开闭原则,因为它不能针对新的动物关闭。
如果我们添加新的动物,如snake:
//... const animals: array<animal> = [ new animal('lion'), new animal('mouse'), new animal('snake') ] //...
我们必须修改animalsound函数:
//... function animalsound(a: array<animal>) { for(int i = 0; i <= a.length; i++) { if(a[i].name == 'lion') log('roar'); if(a[i].name == 'mouse') log('squeak'); if(a[i].name == 'snake') log('hiss'); } } animalsound(animals);
您会看到,对于每一种新动物,都会在animalsound函数中添加新逻辑。这是一个非常简单的例子。当您的应用程序不断扩展并变得复杂时,您将看到,每次在整个应用程序中添加新动物时,都会在animalsound函数中使用if语句一遍又一遍地重复编写逻辑。
我们如何使它符合开闭原则?
class animal { makesound(); //... } class lion extends animal { makesound() { return 'roar'; } } class squirrel extends animal { makesound() { return 'squeak'; } } class snake extends animal { makesound() { return 'hiss'; } } //... function animalsound(a: array<animal>) { for(int i = 0; i <= a.length; i++) { log(a[i].makesound()); } } animalsound(animals);
现在给animal添加了makesound方法。我们让每种动物去继承animal类并实现makesound方法。
每种动物都会在makesound方法中添加自己的实现逻辑。animalsound方法遍历animal数组,并调用其makesound方法。
现在,如果我们添加了新动物,则无需更改animalsound方法。我们需要做的就是将新动物添加到动物数组中。
现在,animalsound符合开闭原则。
再举一个例子
假设你有一家商店,并使用此类向最喜欢的客户提供20%的折扣:
class discount { givediscount() { return this.price * 0.2 } }
当你决定为vip客户提供双倍的20%折扣时。您可以这样修改类:
class discount { givediscount() { if(this.customer == 'fav') { return this.price * 0.2; } if(this.customer == 'vip') { return this.price * 0.4; } } }
这就违反了开闭原则啦!因为如果我们想给不同客户提供差异化的折扣时,你将要不断地修改discount类的代码以添加新逻辑。
为了遵循开闭原则,我们将添加一个新类来继承discount。在这个新类中,我们将实现新的逻辑:
class vipdiscount: discount { getdiscount() { return super.getdiscount() * 2; } }
如果你决定向超级vip客户提供80%的折扣,则应如下所示:
class supervipdiscount: vipdiscount { getdiscount() { return super.getdiscount() * 2; } }
看吧!扩展就无需修改原本的代码啦。
里氏替换原则
里氏替换原则(liskov substitution principle):子类必须可以替代其父类。
该原理的目的是确定子类可以无错误地占据其父类的位置。如果代码中发现自己正在检查类的类型,那么它一定违反了里氏替换原则。
让我们继续使用动物示例。
//... function animallegcount(a: array<animal>) { for(int i = 0; i <= a.length; i++) { if(typeof a[i] == lion) log(lionlegcount(a[i])); if(typeof a[i] == mouse) log(mouselegcount(a[i])); if(typeof a[i] == snake) log(snakelegcount(a[i])); } } animallegcount(animals);
这就违反了里氏替换原则(同时也违反了开闭原则)。因为它必须知道每种动物类型才能去调用对应的legcount函数。
每次创建新动物时,都必须修改animallegcount函数以接受新动物,如下:
//... class pigeon extends animal { } const animals[]: array<animal> = [ //..., new pigeon(); ] function animallegcount(a: array<animal>) { for(int i = 0; i <= a.length; i++) { if(typeof a[i] == lion) log(lionlegcount(a[i])); if(typeof a[i] == mouse) log(mouselegcount(a[i])); if(typeof a[i] == snake) log(snakelegcount(a[i])); if(typeof a[i] == pigeon) log(pigeonlegcount(a[i])); } } animallegcount(animals);
为了遵循里氏替换原则,我们将遵循steve fenton提出的以下要求:
如果父类(animal)具有接受父类类型(animal)参数的方法。它的子类(pigeon)应接受父类类型(animal类型)或子类类型(pigeon类型)作为参数。
如果父类返回父类类型(animal)。它的子类应返回父类类型(animal类型)或子类类型(pigeon)。
现在,我们可以重新设计animallegcount函数:
function animallegcount(a: array<animal>) { for(let i = 0; i <= a.length; i++) { a[i].legcount(); } } animallegcount(animals);
上面animallegcount函数中,只需调用统一的legcount方法。它所关心的就是传入的参数类型必须是animal类型,即animal类或其子类。
animal类现在必须定义legcount方法:
class animal { //... legcount(); }
其子类必须实现legcount方法:
//... class lion extends animal{ //... legcount() { //... } } //...
当传递给animallegcount函数时,它返回狮子的腿数。
你会发现,animallegcount函数只管调用animal的legcount方法,而不需要知道animal的具体类型即可返回其腿数。因为根据规则,animal类的子类必须实现legcount函数。
接口隔离原则
接口隔离原则(interface segregation principle):定制客户端的细粒度接口,不应强迫客户端依赖于不使用的接口。该原理解决了实现大接口的缺点。
让我们看下面的ishape接口:
interface ishape { drawcircle(); drawsquare(); drawrectangle(); }
该接口有绘制正方形,圆形,矩形三个方法。实现ishape接口的circle,square或rectangle类必须同时实现drawcircle(),drawsquare(),drawrectangle()方法,如下所示:
class circle implements ishape { drawcircle(){ //... } drawsquare(){ //... } drawrectangle(){ //... } } class square implements ishape { drawcircle(){ //... } drawsquare(){ //... } drawrectangle(){ //... } } class rectangle implements ishape { drawcircle(){ //... } drawsquare(){ //... } drawrectangle(){ //... } }
看上面的代码很有意思。rectangle类实现了它没有使用的方法(drawcircle和drawsquare),同样square类实现了drawcircle和drawrectangle方法,circle类也实现了drawsquare,drawsquare方法。
如果我们向ishape接口添加另一个方法,例如drawtriangle(),
interface ishape { drawcircle(); drawsquare(); drawrectangle(); drawtriangle(); }
这些类必须实现新方法,否则会编译报错。
接口隔离原则不赞成使用以上ishape接口的设计。不应强迫客户端(rectangle,circle和square类)依赖于不需要或不使用的方法。另外,接口隔离原则也指出接口应该仅仅完成一项独立的工作(就像单一职责原理一样),任何额外的行为都应该抽象到另一个接口中。
为了使我们的ishape接口符合接口隔离原则,我们将不同绘制方法分离到不同的接口中,如下:
interface ishape { draw(); } interface icircle { drawcircle(); } interface isquare { drawsquare(); } interface irectangle { drawrectangle(); } interface itriangle { drawtriangle(); } class circle implements icircle { drawcircle() { //... } } class square implements isquare { drawsquare() { //... } } class rectangle implements irectangle { drawrectangle() { //... } } class triangle implements itriangle { drawtriangle() { //... } } class customshape implements ishape { draw(){ //... } }
icircle接口仅处理图形,ishape处理任何形状的图形,isquare仅处理正方形的图形,irectangle处理矩形的图形。
当然,还有另一个设计是这样:
类(圆形,矩形,正方形,三角形等)可以仅从ishape接口继承并实现其自己的draw行为,如下所示。
class circle implements ishape { draw(){ //... } } class triangle implements ishape { draw(){ //... } } class square implements ishape { draw(){ //... } } class rectangle implements ishape { draw(){ //... } }
依赖倒置原则
依赖倒置原则(dependency inversion principle):依赖应该基于抽象而不是具体。高级模块不应依赖于低级模块,两者都应依赖抽象。
先看下面的代码:
class xmlhttpservice extends xmlhttprequestservice {} class http { constructor(private xmlhttpservice: xmlhttpservice) { } get(url: string , options: any) { this.xmlhttpservice.request(url,'get'); } post() { this.xmlhttpservice.request(url,'post'); } //... }
在这里,http是高级组件,而httpservice是低级组件。此设计违反了依赖倒置原则:高级模块不应依赖于低级模块,它应取决于其抽象。
http类被强制依赖于xmlhttpservice类。如果我们要修改http请求方法代码(如:我们想通过node.js模拟http服务)我们将不得不修改http类的所有方法实现,这就违反了开闭原则。
怎样才是更好的设计?我们可以创建一个connection接口:
interface connection { request(url: string, opts:any); }
该connection接口具有请求方法。这样,我们将类型的参数传递connection给http类:
class http { constructor(private httpconnection: connection) { } get(url: string , options: any) { this.httpconnection.request(url,'get'); } post() { this.httpconnection.request(url,'post'); } //... }
现在,无论我们调用http类的哪个方法,它都可以轻松发出请求,而无需理会底层到底是什么样实现代码。
我们可以重新设计xmlhttpservice类,让其实现connection接口:
class xmlhttpservice implements connection { const xhr = new xmlhttprequest(); //... request(url: string, opts:any) { xhr.open(); xhr.send(); } }
以此类推,我们可以创建许多connection类型的实现类,并将其传递给http类。
class nodehttpservice implements connection { request(url: string, opts:any) { //... } } class mockhttpservice implements connection { request(url: string, opts:any) { //... } }
现在,我们可以看到高级模块和低级模块都依赖于抽象。http类(高级模块)依赖于connection接口(抽象),而xmlhttpservice类、mockhttpservice 、或nodehttpservice类 (低级模块)也是依赖于connection接口(抽象)。
与此同时,依赖倒置原则也迫使我们不违反里氏替换原则:上面的实现类node- xml- mockhttpservice可以替代他们的父类型connection。
结论
本文介绍了每个软件开发人员必须遵守的五项原则。在软件开发中,要遵守所有这些原则可能会令人心生畏惧,但是通过不断的实践和坚持,它将成为我们的一部分,并将对我们的应用程序维护产生巨大影响。
编译:一点教程
欢迎关注我的公众号::一点教程。获得独家整理的学习资源和日常干货推送。
如果您对我的系列教程感兴趣,也可以关注我的网站: