每位开发者都应该知道SOLID原则

By Chidume Nnamdi | Oct 9, 2018

原文

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

这使开发人员能够在一个类中组合具有相同目的/功能的数据,来实现单独的一个功能,不必关心整个应用程序如何。

但是,这种面向对象的编程还是会让开发者困惑或者写出来的程序可维护性不好。

为此,Robert C.Martin指定了五项指导方针。遵循这五项指导方针能让开发人员轻松写出可读性和可维护性高的程序

这五个原则被称为S.O.L.I.D原则(首字母缩写词由Michael Feathers派生)。

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

我们在下文会详细讨论它们

笔记:本文的大多数例子可能不适合实际应用或不满足实际需求。这一切都取决于您自己的设计和用例。这都不重要,关键是您要了解明白这五项原则。

提示:SOLID原则旨在用于构建模块化、封装、可扩展和可组合组件的软件。Bit是一个帮助你践行这些原则的强大工具:它可以帮助您在团队中大规模地在不同项目中轻松隔离,共享和管理这些组件.来试试吧。

Bit

你也可以在这里学习更多关于SOLID原则和Bit的东西。

单一责任原则

“......你有一份工作” - Loki来到雷神的Skurge:Ragnarok

一个类只实现一个功能

一个类应该只负责一件事。如果一个类负责超过一件事,就会变得耦合。改功能的时候会影响另外一个功能。

  • 笔记:该原则不仅适用于类,还适用于软件组件和微服务。

举个例子,考虑这个设计:

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

这个Animal类违反了SRP(单一责任原则)

怎么违反了呢?

SRP明确说明了类只能完成一项功能,这里,我们把两个功能都加上去了:animal数据管理和animal属性管理。构造函数和getAnimalName方法管理Animal的属性,然而,saveAnimal方法管理Animal的数据存储。

这种设计会给以后的开发维护带来什么问题?

如果app的更改会影响数据库的操作。必须会触及并重新编译使用Animal属性的类以使app的更改生效。

你会发现这样的系统缺乏弹性,像多米诺骨牌一样,更改一处会影响其他所有的地方。

让我们遵循SRP原则,我们创建了另外一个用于数据操作的类:

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

“我们在设计类时,我们应该把相关的功能放在一起,所以当他们需要发生改变时,他们会因为同样的原因而改变。如果是因为不同的原因需要改变它们,我们应该尝试把它们分开。” - Steven Fenton

遵循这些原则让我们的app变得高内聚。

开闭原则

软件实体(类,模块,函数)应该是可以扩展的,而不是修改。

继续看我们的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')
            return 'roar';
        if(a[i].name == 'mouse')
            return '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')
            return 'roar';
        if(a[i].name == 'mouse')
            return 'squeak';
        if(a[i].name == 'snake')
            return 'hiss';
    }
}
AnimalSound(animals);

每当新的动物加入,AnimalSound函数就需要加新的逻辑。这是个很简单的例子。当你的app变得庞大和复杂时,你会发现每次加新动物的时候就会加一条if语句,随后你的app和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++) {
        a[i].makeSound();
    }
}
AnimalSound(animals);

现在Animal有个makeSound的私有方法。我们每一个animal继承了Animal类并且实现了私有方法makeSound。

每个animal实例都会在makeSound中添加自己的实现方式。AnimalSound方法遍历animal数组并调用其makeSound方法。

现在,如果我们添加了新动物,AnimalSound方法不需要改变。我们需要做的就是添加新动物到动物数组。

AnimalSound方法现在遵循了开闭原则。

另一个例子:

假设您有一个商店,并且您使用此类给您喜爱的客户打2折:

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;
        }
    }
}

哈哈哈,这样不就背离开闭原则了么?如果我们又想加新的折扣,那又是一堆if语句。

为了遵循开闭原则,我们创建了继承Discount的新类。在这个新类中,我们将会实现新的行为:

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

如果你决定给VIP80%的折扣,就像这样:

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

你看,这不就不用改了。

里氏替换

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)
            return LionLegCount(a[i]);
        if(typeof a[i] == Mouse)
            return MouseLegCount(a[i]);
        if(typeof a[i] == Snake)
            return SnakeLegCount(a[i]);
    }
}
AnimalLegCount(animals);

这已经违反了里氏替换(也违反了OCP原则)。它必须知道每个Animal的类型并且调用leg-conunting相关(返回动物腿数)的方法。

如果要加入新的动物,这个方法必须经过修改才能加入。

//...
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)
            return LionLegCount(a[i]);
        if(typeof a[i] == Mouse)
            return MouseLegCount(a[i]);
         if(typeof a[i] == Snake)
            return SnakeLegCount(a[i]);
        if(typeof a[i] == Pigeon)
            return PigeonLegCount(a[i]);
    }
}
AnimalLegCount(animals);

来,我们依据里氏替换改造这个方法,我们按照Steve Fenton说的来:

  • 如果超类(Animal)有一个接受超类类型(Animal)参数的方法。 它的子类(Pigeon)应该接受超类型(Animal类型)或子类类型(Pigeon类型)作为参数。
  • 如果超类返回超类类型(Animal)。 它的子类应该返回一个超类型(Animal类型)或子类类型(Pigeon)。

现在,开始改造:

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

AnimalLegCount函数更少关注传递的Animal类型,它只调用LegCount方法。它就只知道这参数是Animal类型,或者是其子类。

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

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

然后它的子类就需要实现LegCount方法:

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

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

你看,AnimalLegCount不需要知道Animal的类型来返回它的腿数,它只调用Animal类型的LegCount方法,Animal类的子类必须实现LegCount函数。

接口隔离原则

制定特定客户的细粒度接口
不应强迫客户端依赖它不需要的接口

该原则解决实现大接口的缺点。

让我们看下下面这段代码:

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

这个接口定义了画正方形、圆形、矩形的方法。圆类、正方形类或者矩形类就必须实现 drawCircle()、 drawSquare()、drawRectangle().

class Circle implements Shape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}
class Square implements Shape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}
class Rectangle implements Shape {
    drawCircle(){
        //...
    }
    drawSquare(){
        //...
    }
    drawRectangle(){
        //...
    }    
}

上面的代码看着很好笑。矩形类实现了它不需要的方法。其他类也同样的。

让我们再加一个接口。

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

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

我们看到不可能实现可以绘制圆形而不是矩形或正方形或三角形的形状。 我们可以实现方法来抛出一个错误,表明无法执行操作。

这个Shape接口的设计不符合接口隔离原则。(此处为Rectangle,Circle和Square)不应强制依赖于他们不需要或不使用的方法。

此外,接口隔离原则要求接口应该只执行一个动作(就像单一责任原则一样)任何额外的行为分组都应该被抽象到另一个接口。

这里,我们的Shape接口执行应由其他接口独立处理的动作。

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

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

ICircle接口仅处理圆形绘画,Shape处理任何形状的绘图:),ISquare处理仅正方形的绘制和IRectangle处理矩形绘制。

依赖反转

依赖应该是抽象而不是concretions
高级模块不应该依赖于低级模块。 两者都应该取决于抽象。
抽象不应该依赖于细节。 细节应取决于抽象。

在软件开发有一点,就是我们的app主要由模块组成。当发生这种情况时,我们必须通过使用依赖注入来清除问题。 高级组件取决于低级组件的功能。

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连接服务,也许我们想通过Nodejs连接到互联网,甚至模拟http服务。我们将艰难地通过Http的所有实例来编辑代码,这违反了OCP(依赖反转)原则。

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();
    }
}

我们可以创建很多各种用途的Http类并且不用担心出问题。

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

现在,我们可以看到高级模块和低级模块都依赖于抽象。Http类(高级模块)依赖Connection接口(抽象),Http服务(低级模块)实现Connection接口。

此外,依赖反转还强制我们不要违反里式替换:连接类型Node-XML-MockHttpService可替换其父类型Connection。

结论

我们涵盖了每个软件开发人员必须遵守的五项原则。 一开始可能难以遵守所有这些原则,但通过长期的坚持,它将成为我们的一部分,并将极大地影响我们的应用程序的维护。

如果您有任何疑问,请随时在下面发表评论,我很乐意谈谈!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容

  • 目录: 设计模式六大原则(1):单一职责原则 设计模式六大原则(2):里氏替换原则 设计模式六大原则(3):依赖倒...
    加油小杜阅读 723评论 0 1
  • 设计模式六大原则 设计模式六大原则(1):单一职责原则 定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类...
    viva158阅读 765评论 0 1
  • 今天看到一句话,瞬间被秒到:所谓成功,就是有时间陪自己的小孩。出自《穷爸爸富爸爸》一书。这也让我开始反思自己从原来...
    溯源而上阅读 185评论 0 0
  • 上午主题课是“如何打断别人说话”,小朋友的参与度还是挺高的,中午在卧室陪小朋友,阿西娅因我阻止了她的行为而不高兴,...
    Carsyn_aa55阅读 176评论 1 0
  • Roader阅读 283评论 0 0