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

面向对象编程(OOP)给软件开发领域带来了新的设计思想。不少开发人员在进行面向对象编程过程当中,每每会在一个类中将具备相同目的/功能的代码放在一块儿,力求以最快的方式解决当下的问题。可是,这种编程方式会致使程序代码混乱和难以维护。所以,Robert C. Martin制定了面向对象编程的五项原则。这五个原则使得开发人员能够轻松建立可读性好且易于维护的程序。java

这五个原则被称为SOLID原则。数据库

S:单一职责原则编程

O:开闭原理数组

L:里氏替换原则并发

I:接口隔离原理yii

D:依赖反转原理函数

咱们下面将详细地展开来讨论。微服务

单一职责原则

单一职责原则(Single Responsibility Principle):一个类(class)只负责一件事。若是一个类承担多个职责,那么它就会变得耦合起来。一个职责的变动会致使另外一职责的变动。post

注意:该原理不只适用于类,并且适用于软件组件和微服务。学习

例如,先看看如下设计:

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。

结论

本文介绍了每一个软件开发人员必须遵照的五项原则。在软件开发中,要遵照全部这些原则可能会使人心生畏惧,可是经过不断的实践和坚持,它将成为咱们的一部分,并将对咱们的应用程序维护产生巨大影响。
file

编译:一点教程

https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688

欢迎关注个人公众号::一点教程。得到独家整理的学习资源和平常干货推送。
若是您对个人系列教程感兴趣,也能够关注个人网站:yiidian.com

相关文章
相关标签/搜索