程序员该有的艺术气质—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 Principle 接口分离原则
DIP The Dependency Inversion Principle 依赖倒置原则

1. 单一责任原则(SRP)
      当须要修改某个类的时候缘由有且只有一个。换句话说就是让一个类只作一种类型责任,当这个类须要承当其余类型的责任的时候,就须要分解这个类。 类被修改的概率很大,所以应该专一于单一的功能。若是你把多个功能放在同一个类中,功能之间就造成了关联,改变其中一个功能,有可能停止另外一个功能,这时就须要新一轮的测试来避免可能出现的问题,很是耗时耗力。架构

示例:框架

新建一个Rectangle类,该类包含两个方法,一个用于把矩形绘制在屏幕上,一个方法用于计算矩形的面积。如图学习

 

Rectangle类违反了SRP原则。Rectangle类具备两个职责,若是其中一个改变,会影响到两个应用程序的变化。测试

一个好的设计是把两个职责分离出来放在两个不一样的类中,这样任何一个变化都不会影响到其余的应用程序。优化


2. 开放封闭原则(OCP)
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。this

(1)经过增长代码来扩展功能,而不是修改已经存在的代码。
(2)若客户模块和服务模块遵循同一个接口来设计,则客户模块能够不关心服务模块的类型,服务模块能够方便扩展服务(代码)。
(3)OCP支持替换的服务,而不用修改客户模块。编码

示例:spa

public boolean sendByEmail(String addr, String title, String content) {}
public boolean sendBySMS(String addr, String content) {}

// 在其它地方调用上述方法发送信息
sendByEmail(addr, title, content);
sendBySMS(addr, content);

若是如今又多了一种发送信息的方式,好比能够经过QQ发送信息,那么不只须要增长一个方法sendByQQ(),还须要在调用它的地方进行修改,违反了OCP原则,更好的方式是设计

抽象出一个Send接口,里面有个send()方法,而后让SendByEmail和SendBySMS去实现它既可。这样即便多了一个经过QQ发送的请求,那么只要再添加一个SendByQQ实现类实现Send接口既可。这样就不须要修改已有的接口定义和已实现类,很好的遵循了OCP原则。

 

3. 里氏替换原则(LSP)

当一个子类的实例应该可以替换任何其超类的实例时,它们之间才具备is-A关系

客户模块不该关心服务模块的是如何工做的;一样的接口模块之间,能够在不知道服务模块代码的状况下,进行替换。即接口或父类出现的地方,实现接口的类或子类能够代入。

示例:

public class Rectangle {

    private double width;
    private double height;

    public void setWidth(double value) {
        this.width = value;
    }

    public double getWidth() {
        return this.width;
    }

    public void setHeight(double value) {
        this.width = value;
    }

    public double getHeight() {
        return this.height;
    }

    public double Area() {
        return this.width * this.height;
    }
}

public class Square extends Rectangle {
    /* 因为父类Rectangle在设计时没有考虑未来会被Square继承,因此父类中字段width和height都被设成private,在子类Square中就只能调用父类的属性来set/get,具体省略 */
}

// 测试
void TestRectangle(Rectangle r) {
    r.Weight = 10;
    r.Height = 20;
    Assert.AreEqual(10, r.Weight);
    Assert.AreEqual(200, r.Area);
}

// 运行良好
Rectangle r = new Rectangle();

TestRectangle(r);

// 如今两个Assert测试都失败了
Square s = new Square();

TestRectangle(s);

      LSP让咱们得出一个很是重要的结论:一个模型,若是孤立地看,并不具备真正意义上的有效性,模型的有效性只能经过它的客户程序来表现。例如孤立地看Rectangle和Squre,它们时自相容的、有效的;但从对基类Rectangle作了合理假设的客户程序TestRectangle(Rectangle r)看,这个模型就有问题了。在考虑一个特定设计是否恰当时,不能彻底孤立地来看这个解决方案,必需要根据该设计的使用者所做出的合理假设来审视它。

      目前也有一些技术能够支持咱们将合理假设明确化,例如测试驱动开发(Test-Driven Development,TDD)和基于契约设计(Design by Contract,DBC)。可是有谁知道设计的使用者会做出什么样的合理假设呢?大多数这样的假设都很难预料。若是咱们预测全部的假设的话,咱们设计的 系统可能也会充满没必要要的复杂性。推荐的作法是:只预测那些最明显的违反LSP的状况,而推迟对全部其余假设的预测,直到出现相关的脆弱性的臭味(Bad Smell)时,才去处理它们。我以为这句话还不够直白,Martin Fowler的《Refactoring》一书中“Refused Bequest”(拒收的遗赠)描 述的更详尽:子类继承父类的methods和data,但子类仅仅只须要父类的部分Methods或data,而不是所有methods和data;当这 种状况出现时,就意味这咱们的继承体系出现了问题。例如上面的Rectangle和Square,Square自己长和宽相等,几何学中用边长来表示边, 而Rectangle长和宽之分,直观地看,Square已经Refused了Rectangle的Bequest,让Square继承 Rectangle是一个不合理的设计。

      如今再回到面向对象的基本概念上,子类继承父类表达的是一种IS-A关系,IS-A关系这种用法被认为是面向对象分析(OOA)基本技术之一。但正方形的 的确确是一个长方形啊,难道它们之间不存在IS-A关系?关于这一点,《Java与模式》一书中的解释是:咱们设计继承体系时,子类应该是可替代的父类的,是可替代关系,而不只仅是IS-A的关系;而PPP一书中的解释是:从行为方式的角度来看,Square不是Rectangle,对象的行为方式才是软件真正所关注的问题;LSP清楚地指出,OOD中IS-A关系时就行为方式而言的,客户程序是能够对行为方式进行合理假设的。其实两者表达的是同一个意思。

 
4. 接口分离原则(ISP)

不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。

客户模块不该该依赖大的接口,应该裁减为小的接口给客户模块使用,以减小依赖性。如Java中一个类实现多个接口,不一样的接口给不用的客户模块使用,而不是提供给客户模块一个大的接口。

示例:

public interface Animal {

    public void eat(); // 吃 public void sleep(); // 睡 public void crawl(); // 爬 public void run(); // 跑 }

    public class Snake implements Animal {

        public void eat() {
        }

        public void sleep() {
        }

        public void crawl() {
        }

        public void run() {
        }
    }

    public class Rabit implements Animal {

        public void eat() {
        }

        public void sleep() {
        }

        public void crawl() {
        }

        public void run() {
        }
    }
}

上面的例子,Snake并无run的行为而Rabbit并无crawl的行为,而这里它们却必须实现这样没必要要的方法,更好的方法是crawl()和run()单独做为一个接口,这须要根据实际状况进行调整,反正不要把什么功能都放在一个大的接口里,而这些功能并非每一个继承该接口的类都所必须的。


5. 依赖注入或倒置原则(DIP)

1. 高层模块不该该依赖于低层模块,两者都应该依赖于抽象
2. 抽象不该该依赖于细节,细节应该依赖于抽象

这个设计原则的亮点在于任何被DI框架注入的类很容易用mock对象进行测试和维护,由于对象建立代码集中在框架中,客户端代码也不混乱。有不少方式能够实现依赖倒置,好比像AspectJ等的AOP(Aspect Oriented programming)框架使用的字节码技术,或Spring框架使用的代理等。

(1).高层模块不要依赖低层模块;
(2).高层和低层模块都要依赖于抽象;
(3).抽象不要依赖于具体实现; 
(4).具体实现要依赖于抽象;
(5).抽象和接口使模块之间的依赖分离

先让咱们从宏观上来看下,举个例子,咱们常常会用到宏观的一种体系结构模式--layer模式,经过层的概念分解和架构系统,好比常见得三层架构等。那么依赖关系应该是自上而下,也就是上层模块依赖于下层模块,而下层模块不依赖于上层,以下图所示。

这应该仍是比较容易理解的,由于越底层的模块相对就越稳定,改动也相对越少,而越上层跟需求耦合度越高,改动也会越频繁,因此自上而下的依赖关系使上层发生变动时,不会影响到下层,下降变动带来的风险,保证系统的稳定。

上面是立足在总体架构层的基础上的结果,再换个角度,从细节上再分析一下,这里咱们暂时只关注UI和Service间的关系,以下面这样的依赖关系会有什么样的问题?

第一,当须要追加提供一种新的Service时,咱们不得不对UI层进行改动,增长了额外的工做。

第二,这种改动可能会影响到UI,带来风险。

第三,改动后,UI层和Logic层都必须从新再作Unit testing。

 

那么具体怎么优化依赖关系才能让模块或层间的耦合更低呢?想一想前面讲的OCP原则吧,观点是相似的。

咱们能够为Service追加一个抽象层,上层UI不依赖于Service的details,UI和Service同时依赖于这个Service的抽象层。以下图是咱们的改进后的结果。

这样改进后会有什么好处呢?

第一,Service进行扩展时,通常状况下不会影响到UI层,UI不须要改动。

第二,Service进行扩展时,UI层不须要再作Unit testing。

 

总结:

  1. 一个对象只承担一种责任,全部服务接口只经过它来执行这种任务。
  2. 程序实体,好比类和对象,向扩展行为开放,向修改行为关闭。
  3. 子类应该能够用来替代它所继承的类。
  4. 一个类对另外一个类的依赖应该限制在最小化的接口上。
  5. 依赖抽象层(接口),而不是具体类。

      这几条原则是很是基础并且重要的面向对象设计原则。正是因为这些原则的基础性,理解、融汇贯通这些原则须要很多的经验和知识的积累。举的例子可能不太贴切也不太准确,反正理解了就行,之后去公司实习什么的必定要遵循这些原则,不能让本身写的代码让别人批的一无可取而后胎死腹中,固然还有其余的一些很重要的原则,我会在后面的时间里继续学习和分享!

相关文章
相关标签/搜索