• 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。

    结论

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

    编译:一点教程

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

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

  • 相关阅读:
    Spring AOP 详解
    java 线程的几种状态
    Atomic
    ConcurrentHashMap原理分析
    MySQL存储过程详解 mysql 存储过程
    spring-定时器(2)
    spring-定时器(1)
    spring-线程池(3)
    spring-线程池(2)
    spring-线程池(1)
  • 原文地址:https://www.cnblogs.com/yiidian/p/12297785.html
Copyright © 2020-2023  润新知