首页
Preview

每个开发者都应该了解的SOLID原则

面向对象的编程类型为软件开发带来了新的设计思路。

这使得开发人员可以将相同目的/功能的数据合并到一个类中,以便处理单一目的,而不考虑整个应用程序。

但是,这种面向对象编程并不能防止混乱或难以维护的程序。

因此,Robert C. Martin制定了五个准则。这五个准则/原则使开发人员能够创建可读性和可维护性的程序。

这五个原则被称为S.O.L.I.D原则(缩写由Michael Feathers衍生)。

  • S:单一责任原则
  • O:开放封闭原则
  • L:Liskov替换原则
  • I:接口隔离原则
  • D:依赖反转原则

我们将在下面详细讨论它们。

**注意:**本文中的大多数示例可能不足以代表真实情况,或在真实世界应用程序中不适用。这完全取决于你自己的设计和用例。最重要的是要理解并知道如何应用/遵循原则。

**提示:**使用像BitGitHub)这样的工具可以轻松地在项目和应用程序之间共享和重用组件(和小模块)。它还可以帮助你和你的团队节省时间,保持同步并共同构建更快。它是免费的,试试吧

Bit 轻松在应用程序和项目之间共享组件

组件发现和协作·Bit

单一责任原则

“...你只有一个工作” — 洛基 对斯克尔格在《雷神3:诸神黄昏》中说

一个类应该只负责一件事情。

一个类应该只负责一件事情。如果一个类有多个职责,它就会变得耦合。一个职责的变化会导致对另一个职责的修改。

  • **注意:**此原则不仅适用于类,还适用于软件组件和微服务。

例如,考虑以下设计:

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
    saveAnimal(a: Animal) { }
}

Animal类违反了SRP。

它如何违反SRP?

SRP规定类应该有一个职责,在这里,我们可以列出两个职责:动物数据库管理和动物属性管理。构造函数和getAnimalName管理Animal属性,而saveAnimal管理Animal存储在数据库中。

这个设计将来会如何引起问题?

如果应用程序以某种方式更改,从而影响数据库管理功能。使用Animal属性的类将不得不进行修改和重新编译,以补偿新的更改。

你会发现,这个系统充满了僵化,就像多米诺骨牌效应一样,触碰一张牌会影响到排在其后面的所有其他牌。

为了符合SRP,我们创建另一个类来处理存储动物到数据库的唯一职责:

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}class AnimalDB {
    getAnimal(a: Animal) { }
    saveAnimal(a: Animal) { }
}

在设计我们的类时,我们应该尝试将相关功能放在一起,这样无论何时它们倾向于改变,它们都会因同样的原因而改变。如果它们因不同的原因而改变,我们应该尝试将它们分开。 - Steve Fenton

通过正确应用这些准则,我们的应用程序变得高度内聚。

开放封闭原则

软件实体(类,模块,函数)应该对扩展开放,对修改关闭。

让我们继续使用Animal类。

class Animal {
    constructor(name: string){ }
    getAnimalName() { }
}

我们想要迭代一个动物列表并让它们发出声音。

//...
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语句会一遍又一遍地重复,每次添加新动物时都会在整个应用程序中重复出现。

我们如何使其(AnimalSound)符合OCP?

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迭代动物数组,只需调用其makeSound方法。

现在,如果我们添加一个新的动物,AnimalSound不需要更改。我们只需要将新动物添加到动物数组中即可。

AnimalSound现在符合OCP原则。

另一个例子:

假设你有一个商店,并使用此类为你最喜欢的客户提供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;
        }
    }
}

不,这违反了OCP原则。OCP禁止它。如果我们想为不同类型的客户提供新的百分比折扣,你会发现将添加新逻辑。

为了使其遵循OCP原则,我们将添加一个新类,该类将扩展Discount。在这个新类中,我们将实现其新行为:

class VIPDiscount: Discount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

如果你决定为超级VIP客户提供80%的折扣,则应该如下所示:

class SuperVIPDiscount: VIPDiscount {
    getDiscount() {
        return super.getDiscount() * 2;
    }
}

你会发现,扩展而无需修改。

Liskov替换原则

子类必须能够替换其超类

这个原则的目的是确保子类可以替换其超类而不出错。如果代码发现自己检查类的类型,那么它必须违反了这个原则。让我们使用动物的例子。

下面的代码违反了LSP原则(也违反了OCP原则)。它必须知道每个动物类型并调用相应的“计算腿数”函数。

每当创建新动物时,该函数必须修改以接受新动物。

为了使该函数遵循LSP原则,我们将遵循Steve Fenton提出的LSP要求:

  • 如果超类(Animal)具有接受超类类型(Animal)参数的方法,则其子类(Pigeon)应接受超类类型(Animal类型)或子类类型(Pigeon类型)作为参数。
  • 如果超类返回超类类型(Animal),则其子类应返回超类类型(Animal类型)或子类类型(Pigeon)。

现在,我们可以重新实现AnimalLegCount函数:

AnimalLegCount函数不关心传递的Animal类型,它只是调用LegCount方法。它只知道参数必须是Animal类型,即Animal类或其子类。

Animal类现在必须实现/定义一个LegCount方法:

它的子类也必须实现LegCount方法:

当它传递给AnimalLegCount函数时,它返回狮子的腿数。

你看,AnimalLegCount不需要知道Animal的类型就能返回它的腿数,它只是调用Animal类型的LegCount方法,因为按照契约,Animal类的子类必须实现LegCount函数。

接口隔离原则

制定客户特定的细粒度接口

客户不应被迫依赖它们不使用的接口。

这个原则处理实现大型接口的缺点。

让我们看看下面的IShape接口:

这个接口绘制正方形、圆形、矩形。实现IShape接口的Circle、Square或Rectangle类必须定义drawCircle()、drawSquare()、drawRectangle()方法。

上面的代码看起来相当有趣。矩形类实现(drawCircle和drawSquare)它没有用到的方法,同样的,正方形实现drawCircle和drawRectangle,圆形类实现drawSquare和drawSquare。

如果我们向IShape接口添加另一个方法,如drawTriangle(),

则类必须实现新方法,否则将抛出错误。

我们看到,无法实现一个可以绘制圆但不能绘制矩形、正方形或三角形的形状。我们只能实现抛出错误的方法,显示无法执行操作。

ISP不赞成IShape接口的设计。客户端(这里是Rectangle、Circle和Square)不应被迫依赖于它们不需要或不使用的方法。此外,ISP指出接口应该只执行一个作业(就像SRP原则一样),任何额外的行为分组都应该被抽象到另一个接口中。

在这里,我们的IShape接口执行应该由其他接口独立处理的操作。

为了使我们的IShape接口符合ISP原则,我们将操作分离到不同的接口中:

ICircle接口仅处理圆的绘制,IShape处理任何形状的绘制,ISquare处理仅正方形的绘制,IRectangle处理矩形的绘制。

或者,类(Circle、Rectangle、Square、Triangle等)可以继承IShape接口并实现自己的draw行为。

我们可以使用I-interfaces创建特定形状,如半圆、直角三角形、等边三角形、钝角矩形等。

依赖倒置原则

依赖应该是抽象的,而不是具体的。

A.高层模块不应依赖于低层模块。两者都应该依赖于抽象。

B.抽象不应该依赖于细节。细节应该依赖于抽象。

在软件开发中,有一个时刻我们的应用程序将大部分由模块组成。当这种情况发生时,我们必须通过使用_依赖注入_来澄清事情。高层组件依赖于低层组件以进行功能。

在这里,Http是高层组件,而HttpService是低层组件。这种设计违反了DIP A:高层模块不应依赖于低层级模块。它应该依赖于它的抽象。

Http类被迫依赖于XMLHttpService类。如果我们要更改Http连接服务,也许我们想通过Nodejs连接到Internet,甚至模拟http服务。我们将不得不费力地浏览所有Http实例来编辑代码,这违反了OCP原则。

Http类应该不关心你正在使用的Http服务类型。我们创建一个Connection接口:

Connection接口有一个请求方法。有了这个,我们将一个类型为Connection的参数传递给我们的Http类:

所以现在,无论传递给Http的Http连接服务的类型是什么,它都可以轻松地连接到网络,而不必知道网络连接的类型。

我们现在可以重新实现XMLHttpService类以实现Connection接口:

我们可以创建许多HttpConnection类型并将其传递给我们的Http类,而不必担心任何错误。

现在,我们可以看到高层模块和低层模块都依赖于抽象。Http类(高层模块)依赖于Connection接口(抽象),而Http服务类型(低层模块)反过来又依赖于Connection接口(抽象)。

此外,此DIP将迫使我们不违反Liskov替换原则:Connection类型Node-XML-MockHttpService可以替换其父类型Connection。# 结论

我们在这里介绍了每个软件开发人员必须遵守的五个原则。起初遵守所有这些原则可能会让人望而却步,但是通过持续的练习和遵守,它将成为我们的一部分,并对我们应用程序的维护产生巨大影响。

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

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
一个人玩
先找到想要的,然后出发

评论(0)

添加评论