[译]开发者须知的SOLID原则

原文:SOLID Principles every Developer Should Know – Bits and Piecesjavascript

SOLID Principles every devloper should know

面向对象为软件开发带来了新的设计方式,它使得开发者能够将具备相同目的或功能的数据结组合到一个类中来完成单一的目的,不须要考虑整个应用。java

可是,面向对象编程没有减小混乱和不可维护的程序。正是这样,Robert C. Martin发展出了5条指南/准则,让开发者能够易于建立可读且易于维护的程序。数据库

这5条准则就是S.O.L.I.D原则(缩写是Michael Feathers推演出来的)编程

  • S: Single Responsibilty Principle 单一功能原则
  • O: Open-Closed Principle 开闭原则
  • L: Liskov Substitution Principle 里氏替换
  • I: Interface Segregation Principle 接口分离
  • D: Dependency Inversion Principle 依赖反转

接下来咱们详细讨论上述原则。
注意: 本文的大部分例子可能不能知足或者适用现实世界的应用程序。要视你本身的实际设计和使用场景来定。最重要的是理解和掌握如何运用或遵循这些原则。数组

建议:使用Bit这样的工具来实践SOLID原则,它能帮助你组织,发现和重用构建新应用程序的组件。组件能够在不一样项目之间被发现和共享,因此你能够更快地构建应用程序,不妨试试。网络

单一功能原则 Single Responsibilty Principle

“...You had one job”---Loki to Skurge in Thor: Ragnarok函数

一个类只作一件工做

一个类只负责一件事。若是一个类有多项责任,它就变耦合了。一个功能的变更会形成另一个功能改变。微服务

  • 注意: 这条原则不只仅适用于类,也适用于软件组件和微服务。

例如,考虑这样一个设计:工具

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

这里的Animal类是否违背了单一功能原则(SRP)?post

怎样违背的?

SRP中说一个类应只含一个功能,如今咱们能分出两个功能:动物数据管理和动物特性管理。构造函数和getAnimalName管理动物特性,而saveAnimal负责动物在数据库中的存储。

这个设计未来会引起怎样的问题?

那部分若是应用程序对数据库管理相关函数做变动,使用了动物特性功能的代码也要会受影响而且要从新编译来适应新的变动。

可见这个系统显得很死板,好像一个多米诺骨牌效应,触动一张牌就会影响排列中的全部其余牌。

为了符合SRP,咱们建立另外一个单一功能的类只负责将一个动物存储到一个数据库中:

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

class AnimalDB {
  getAnimal(a: Animal) { }
  saveAnimal(a: Animal) { }
}
When designing our classes, we should aim to put related features together,
so whenever they tend to change they change for the same reason.
And we should try to separate features if they will change for different reasons. 
咱们在设计类的时候,要以将相关的特性放在一块儿为目标,
当他们须要改变时应当是出于相同的缘由,
若是咱们发现他们会由于不一样的缘由改变,则需考虑将特性拆分开来
                                                      ---Steve Fenton

开闭原则 Open-Closed Principle

Software entities(Classes, modules, functions) should be open for extension, not modification.
软件实体(类,模块,函数等)应当对扩展开放,而对变动是封闭的

继续讨论Animal类,

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

咱们想遍历一个animal列表而且让发出他们的声音。

//...
const animals: Array<Animals> = [
  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')
];
//...

咱们不得不修改AnimalSoound函数

//...
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语句将在程序中不断地重复出现。

那怎样使它符合开闭原则(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添加发声音的实现,遍历动物数组的时候只须要调用它们的makeSound方法。

这样,若是有新动物要添加,AnimalSound不要改变。咱们只须要向动物数组中添加新的动物。

再举一例:

假设你有一家商店,你但愿给你最喜好的那些顾客20%的优惠,下面是类实现:

class Discount {
  giveDiscount() {
    return this.price * 0.2;
  }
}

当你决定给VIP用户的折扣翻倍,你可能会这样修改类:

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 Substitution Principle

A sub-class must be substitutable for its super class
子类必定能用父级类替换

这条原则就是目的就是确保子类能无差错地代替父类的位置。若是代码发现它还须要检查子类的类型,那么它就不符合这条原则。

用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。它必须肯定每种动物的类型并调用相应的计腿方法。

每当新增一种动物,这个函数都须要作出修改来适应。

//...
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]));
  }
}
AimalLegCount(animals);

要使这个函数符合LSP,须要遵循Steven Fenton 提出的如下要求:

  • 若是父类(Animal)有一个接受父类类型(Animal)的参数的方法,它的子类(Pigeon)应该接受一个父类类型(Animal)或子类类型(Pigeon)做为参数
  • 若是父类返回一个父类类型(Animal),其子类应当返回一个父类类型(Animal)或子类类型(Pigeon)。

如今来从新实现AnimalLegCount函数:

function AnimalLegCount(a: Array<Animal>) {
  for(let i = 0; i <= a.length; i++) {
    log(a[i].LegCount());
  }
}
AnimalLegCount(animals);

AnimalLegCount函数如今更少关心传递的Animal的类型,它只是调用LegCount方法。它只知道传入的参数必须是Animal类型,不管是Animal类型仍是他的子类。

Animal类型如今须要实现/定义一个LegCount方法:

class Animal {
  //...
  LegCount();
}

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

class Lion extends Animal{
  //...
  LegCount() {
    //...
  }
}

当它被传递给AnimalLegCount函数时,他将返回一头狮子的腿数。

可见AnimalLegCount函数不须要知道Animal的具体类型,只须要调用Animal类的LegCount方法,由于按约定Animal类的子类都必须实现LegCount函数。

接口分离原则 Interface Segregation Principle

Make fine grained interfaces that are client specific
为特定客户制做细粒度的接口
Clients should not be forced to depend upon interfacees that they do not use
客户应当不会被迫以来他们不会使用的接口

这条原则用于处理实现大型接口时的弊端。来看以下接口IShape:

interface Ishape {
  drawCircle();
  drawSquare();
  drawRectangle();
}

这个接口能够画圆形,方形,矩形。Circle类,Square类,Rectangel类实现IShape接口的时候必须定义drawCircle(),drawSqure(),drawRectangle()方法。

class Circle implements Ishape {
  drawCircle(){
    //...
  }
  
  drawSquare(){
    //...
  }
  
  drawRectangle(){
    //...
  }
}

class Square implements Ishape {
  drawCircle() {
    //...
  }
  
  drawSquare(){
    //...
  }
  
  drawRectangle(){
    //...
  }
}

class Rectangel implements Ishape {
  drawCircle() {
    //...
  }
  
  drawSquare(){
    //...
  }
  
  drawRectangle(){
    //...
  }
}

上面的代码看起来就很怪。Rectangle类药实现它用不上的drawCircle(),drawSquare()方法,Square类和Circle类也同理。

若是咱们向Ishape中增长一个接口,如drawTriangle():

interface IShape {
  drawCircle();
  drawSquare();
  drawRectangle();
  drawTriangle();
}

全部子类都须要实现这个新方法,不然就会报错。

也能看出不可能实现一个能够画圆可是不能画方,或画矩形及三角形的图形类。咱们能够只是为上述子类都实现全部方法可是抛出错误指明不正确的操做不能被执行。

ISP不提倡IShape的上述实现。客户(这里的Circle, Rectangle, Square, Triangle)不该被强迫依赖于它们不须要或用不上的方法。ISP还指出一个接口只作一件事(与SRP相似),全部其余分组的行为都应当被抽象到其余的接口中。

这里, Ishape接口执行了本应由其余接口独立处理的行为。

为了使IShape符合ISP原则,咱们将这些行为分离到不一样的接口中去:

interface Ishape {
  draw();
}

interface ICircle {
  drawCircle();
}

interface ISquare {
  drawSquare();
}

interface IRecetangle {
  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() {
    //...
  }
}

我如今还可使用I-接口来建立更多特殊形状,如Semi
circle, Right-Angled Triangle, Equilateral Triangle, Blunt-Edged Rectangle等等。

依赖反转 Dependency Inverse Principle

Dependency should be on abstractions not concretion
依赖于抽象而非具体实例

A. High-level modules should not depend upon low-level modules. Both should depend upon avstractions.
B. Abstractions should not depend on deatils. Details should depend upon abstractions.
A. 上层模块不该该依赖于下层模块。它们都应该依赖于抽象。
B. 抽象不该该依赖于细节。细节应该依赖于抽象。

这对开发由许多模块构成的应用程序十分重要。这时候,咱们必须使用依赖注入(dependency injection) 来理清关系、上层元件依赖于下层元件来工做。

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方法。咱们经过他传递一个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链接服务传递过来,Http类均可以轻松的链接到网络,无需关心网络链接的类型。

如今咱们能够从新实现XMLHttpService类来实现Connection 接口:

class XMLHttpService implements Connection {
  const xhr = new XMLHttpRequest();
  //...
  request(url: string, opts: any) {
     xhr.open();
     xhr.send();
  }
}

咱们能够建立许多的Http Connection类型而后传递给Http类但不会引起任何错误。

class NodeHttpService implements Connection {
  request(url: string, opts: any){
    //...
  }
}

class MockHttpService implements Connection {
  request(url: string, opts:any) {
    //...
  }
}

如今,能够看到上层模块和下层模块都依赖于抽象。 Http类(上层模块)依赖于Connection接口(抽象),并且Http服务类型(下层模块)也依赖于Connection接口(抽象)。

结语

咱们讨论了每一个软件开发者都须要听从的五大原则。刚开始的时候要遵照这些原则可能会有点难,可是经过持续的练习和坚持,它将成为咱们的一部分而且对维护咱们的应用程序产生巨大的影响。

若是您有任何疑问或者有认为须要增长,更正或者移除的内容,尽管在下方留言,我会乐意与您讨论!

原文:SOLID Principles every Developer Should Know – Bits and Pieces

相关文章
相关标签/搜索