欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SOLID原则都不知道,还敢说自己是搞开发的!

程序员文章站 2022-03-21 16:29:48
面向对象编程(OOP)给软件开发领域带来了新的设计思想。很多开发人员在进行面向对象编程过程中,往往会在一个类中将具有相同目的/功能的代码放在一起,力求以最快的方式解决当下的问题。但是,这种编程方式会导致程序代码混乱和难以维护。因此,Robert C. Martin制定了面向对象编程的五项原则。这五个 ......

面向对象编程(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。

结论

本文介绍了每个软件开发人员必须遵守的五项原则。在软件开发中,要遵守所有这些原则可能会令人心生畏惧,但是通过不断的实践和坚持,它将成为我们的一部分,并将对我们的应用程序维护产生巨大影响。
SOLID原则都不知道,还敢说自己是搞开发的!

编译:一点教程

欢迎关注我的公众号::一点教程。获得独家整理的学习资源和日常干货推送。
如果您对我的系列教程感兴趣,也可以关注我的网站: