转载自此,原做者为 Chidume Nnamdi。javascript
面向对象编程带来了新的软件开发设计方法。它使得开发人员可以将具备相同做用 / 功能的数据组合到一个类中,实现惟一的目的,而无论整个应用程序如何。java
可是,这种面向对象的编程并不能防止难以理解或不可维护的程序。所以,Robert C. Martin 制定了五项指导原则,使开发人员很容易建立出可读性强且可维护的程序。这五项原则被称为 S.O.L.I.D 原则(这种缩写是由 Michael Feathers 提出的):数据库
下面咱们将展开详细的讨论。
注意:本文中的大多数示例可能不能知足实际状况或不能应用于实际的应用程序。这彻底取决于你本身的设计和场景。最重要的是理解并知道如何应用 / 遵循这些原则。
提示:SOLID 原则是为构建模块化、可扩展和可组合的封装组件而设计的。编程
一个类只应该负责一件事。若是一个类有多个职责,那么它变成了耦合的。对一个职责的修改会致使对另外一个职责的修改。
注意:这个原则不只适用于类,也适用于软件组件和微服务。
例如,考虑下面的设计:数组
class Animal { constructor(name: string){ } getAnimalName() { } saveAnimal(a: Animal) { } }
上面的 Animal 就违反了单一职责原则(SRP)。网络
SRP 指出,类应该有一个职责,在这里,咱们能够得出两个职责:动物数据库管理和动物属性管理。构造函数和 getAnimalName 管理动物属性,而 saveAnimal 管理 Animal 在数据库中的存储。模块化
若是应用程序的修改影响了数据库管理功能,使用 Animal 属性的类就必须修改和从新编译,以适应这种新的变化。这个系统就有点像多米诺骨牌,触碰一张牌就会影响到其余牌。函数
为了使这个类符合 SRP,咱们建立了另外一个类,它负责将动物存储到数据库中这个单独的职责:微服务
class Animal { constructor(name: string){ } getAnimalName() { } } class AnimalDB { getAnimal(a: Animal) { } saveAnimal(a: Animal) { } }
在设计咱们的类时,咱们应该把相关的特性放在一块儿,这样,每当它们须要改变的时候,它们都是由于一样的缘由而改变。若是它们因不一样的缘由而改变,咱们就应该尝试将它们分开。——Steve Fentonpost
恰当运用这条原则,咱们的应用程序就会变成高内聚的。
软件实体(类、模块、函数)应该对扩展开放,对修改关闭。
让咱们继续以 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 不符合开闭原则,由于它不能对新的动物关闭。若是咱们添加一种新的动物蛇:
//... 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 函数。这是一个很是简单的例子。当应用程序变得庞大而复杂时,你会看到,每添加一种新动物,if 语句就得在 AnimalSound 函数中重复一遍。
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 客户双倍的折扣(40%)时,你可能会这样修改这个类:
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; } }
就是这样,扩展而不修改。
子类必须能够替换它的超类。
这个原则的目的是确保子类能够替换它的超类而没有错误。若是你发现本身的代码在检查类的类型,那么它必定违反了这个原则。
让咱们以 Animal 为例。
//... 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);
上述方法违反了 LSP 原则(也违反了 OCP 原则)。它必须知道每一种 Animal 类型,并调用相应的数腿函数。
每次建立一个新的动物类,都得修改这个函数:
//... 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);
为了使这个函数符合 LSP 原则,咱们将遵循 Steve Fenton 提出的 LSP 要求:
若是超类(Animal)有一个方法接受超类类型(Anima)的参数,那么它的子类(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 类的一个子类必须实现 LegCount 函数。
建立特定于客户端的细粒度接口。不该该强迫客户端依赖于它们不使用的接口。
这个原则是为了克服实现大接口的缺点。让咱们看看下面的 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 和 drawRectangle。
若是咱们向 IShape 接口添加另外一个方法,好比 drawTriangle():
interface IShape { drawCircle(); drawSquare(); drawRectangle(); drawTriangle(); }
那么,这些类就必须实现新方法,不然就会抛出错误。
咱们看到,不可能实现这样一种形状类,它能够画圆,但不能画矩形、正方形或三角形。咱们在实现方法时能够只抛出一个错误,代表操做没法执行。
ISP 反对 IShape 接口的这种设计。客户端(这里是 Rectangle、Circle 和 Square)不该该被迫依赖于它们不须要或不使用的方法。另外,ISP 指出,接口应该只执行一个任务(就像 SRP 原则同样),任何额外的行为都应该抽象到另外一个接口中。
在这里,咱们的 IShape 接口执行了应该由其余接口独立处理的操做。为了使 IShape 接口符合 ISP 原则,咱们将对不一样接口的操做进行隔离:
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 处理矩形的绘制。
或者,类(Circle、Rectangle、Square、Triangle)必须继承 IShape 接口,并实现本身的绘制行为。
class Circle implements IShape { draw(){ //... } } class Triangle implements IShape { draw(){ //... } } class Square implements IShape { draw(){ //... } } class Rectangle implements IShape { draw(){ //... } }
而后,咱们可使用 I- 接口建立具体的形状,如半圆、直角三角形、等边三角形、钝边矩形等。
依赖应该是抽象的,而不是具体的。
高级模块不该该依赖于低级模块。二者都应该依赖于抽象。
抽象不该该依赖于细节。细节应该依赖于抽象。
在软件开发中,咱们的应用程序最终主要是由模块组成。当这种状况出现时,咱们必须使用依赖注入来解决。高级组件依赖于低级组件发挥做用。
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 是低级组件。这种设计违反了 DIP A:高级模块不该该依赖于低级模块。它应该依赖于它的抽象。
该 Http 类被迫依赖于 XMLHttpService 类。若是咱们要修改 Http 链接服务,也许咱们想经过 Nodejs 链接到互联网,甚至模拟 http 服务。咱们将不得不费力地遍历全部 Http 实例来编辑代码,这违反了 OCP 原则。
Http 类不该该关心使用的 Http 服务的类型。咱们作了一个 Connection 接口:
interface Connection { request(url: string, opts:any); }
Connection 接口有一个 request 方法。有了这个接口,咱们就能够向 Http 类传递一个 Connection 类型的参数:
class Http { constructor(private httpConnection: Connection) { } get(url: string , options: any) { this.httpConnection.request(url,'GET'); } post() { this.httpConnection.request(url,'POST'); } //... }
所以,不管传递给 Http 类的 Http 链接服务是什么类型,它均可以轻松地链接到网络,而无需知道网络链接的类型。
如今,咱们从新实现 XMLHttpService 类来实现 Connection 接口:
class XMLHttpService implements Connection { const xhr = new XMLHttpRequest(); //... request(url: string, opts:any) { xhr.open(); xhr.send(); } }
咱们能够建立许多 Http 链接类型,并将其传递给 Http 类,而没必要担忧错误。
class NodeHttpService implements Connection { request(url: string, opts:any) { //... } } class MockHttpService implements Connection { request(url: string, opts:any) { //... } }
如今,咱们能够看到,高级模块和低级模块都依赖于抽象。Http 类(高级模块)依赖于 Connection 接口(抽象),而 Http 服务类型(低级模块)也依赖于 Connection 接口(抽象)。
此外,DIP 原则会强制咱们遵循里氏替换原则:Connection 类型 Node-XML-MockHttpService 能够替换它们的父类型链接。
本文介绍了每一个软件开发人员都必须遵照的五个原则。首先,要遵照全部这些原则可能会使人生畏,可是随着不断的实践和坚持,它们会成为咱们的一部分,并将对应用程序的维护产生巨大的影响。
关于这些原则,若是你以为有什么须要添加、纠正或删除,请在下面的评论区留言,我很是乐意与你讨论!
英文原文:
https://blog.bitsrc.io/solid-principles-every-developer-should-know-b3bfa96bb688