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。

结论

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

编译:一点教程

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

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

原文地址:https://www.cnblogs.com/yiidian/p/12297785.html

时间: 2024-08-30 04:41:58

SOLID原则都不知道,还敢说自己是搞开发的!的相关文章

设计模式之SOLID原则再回首

    本科阶段学过设计模式,那时对设计模式的五大原则--SOLID原则的概念与理解还是比较模糊,此时过去了2年时间,在学习<高级软件工程>课程中老师又提到了设计模式,课程中还详细讨论了五大原则的过程,这次SOLID原则再回首作者提出了一些更通俗的理解吧~ 一. 什么是设计模式?     那么,什么是设计模式呢? 从广义角度讲设计模式是可解决一类软件问题并能重复使用的设计方案; 从狭义角度讲设计模式是对被用来在特定场景下解决一般设计问题的类和相互通信的对象的描述,是在类和对象的层次描述的可重复

类设计的SOLID原则

SOLID原则是面向对象范式的核心 单一职责原则(Single Responsible Principle, SRP):对于一个类,应该仅有一个引起它变化的原因.其基础是内聚,表示类完成单一功能的程度. 开放—封闭原则(Open Closed Principle,OCP):类应该对扩展开放,对修改关闭.是SOLID原则中最重要的一个.OCP 的一个原则就是将类之间的耦合到抽象级别. Liskov替换原则(Liskov Substitution Principle,LSP):子类型应该能够替换它们

SOLID原则(OOD&amp;OOP)

SOLID原则是面向对象编程和面向对象设计的头五大原则.学习及应用这五大原则可以构建一个易于维护和扩展的应用程序,我们一起看看到底是那五大原则. S--单一责任原则(SRP) --Single Responsibility Principle O--开放封闭原则(OCP)-- Open-Closed  Principle L--里式替换原则(LSP)-- Liskov Substitution Principle I –- 接口分离原则(ISP)--Interface Segregation P

【译】浅谈SOLID原则

SOLID原则是一种编码的标准,为了避免不良设计,所有的软件开发人员都应该清楚这些原则.SOLID原则是由Robert C Martin推广并被广泛引用于面向对象编程中.正确使用这些规范将提升你的代码的可扩展性.逻辑性和可读性. 当开发人员按照不好的设计来开发软件时,代码将失去灵活性和健壮性.任何一点点小的修改都非常容易引起bug.因此,我们应该遵循SOLID原则. 首先我们需要花一些时间来了解SOLID原则,当你能够理解这些原则并正确使用时,你的代码质量将会得到大幅的提高.同时,它可以帮助你更

面向对象设计的SOLID原则

S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility Principle 单一责任原则 OCP The Open Closed Principle  开放封闭原则 LSP The Liskov Substitution Principle 里氏替换原则 DIP The Dependency Inversion Principle 依赖倒置原则 ISP The

【转】面向对象设计的SOLID原则

面向对象设计的SOLID原则 http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写. SRP The Single Responsibility Principle 单一责任原则 OCP The Open Closed Principle  开放封闭原则 LSP The Liskov Subst

SOLID原则 【转】

S.O.L.I.D 是面向对象设计(OOD)和面向对象编程(OOP)中的几个重要编码原则(Programming Priciple)的首字母缩写. 面向对象设计的原则 SRP  The Single Responsibility Principle 单一职责原则 OCP   The Open Closed Principle 开放封闭原则 LSP  The Liskov Substitution Principle 里氏替换原则 ISP   The Interface Segregation P

[JavaEE]设计模式之SOLID原则

1. S  The Single Responsibility Principle  单一责任原则 当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE).换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任 的时候,就需要分解这个类. 2. O The Open Closed Principle  开放封闭原则 软件实体应该是可扩展,而不可修改的.也就是说,对扩展

设计模式(0): 简介及SOLID原则

在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案. 设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案.面向对象设计模式通常以类或对象来描述其中的关系和相互作用,但不涉及用来完成应用程序的特定类或对象.设计模式能使不稳定依赖于相对稳定.具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力. 并非所有的软件模式都是设计模式,设计模式特指软件"设计"层次上的问题