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