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 | 依赖倒置原则 |
solid是一个框架,用于帮助人们设计出更好的面向对象代码。它由Robert Martin作为五大原则引入。
solid代表的含义:
1、单一职责原则(Single Responsibility Principle,SPR)
在面向对象编程和函数式编程中,都可以使用这种思想,即一个类或者一个函数只干一件事,如果这个类或者函数要干多件事,请将它拆分。一个类拥有的功能越多,维护起来就越困难。
2、开/闭原则(Open / close principle,OCP)
即封装原则。对象应该对扩展开放,对修改关闭。
3、LisKov替换原则(LisKov replacement principle,LSP)
LSP是指任何子类型都应该能轻松的被同一对象树下的其他对象替代,而没有任何影响。如一个模型车对象被一个真实车对象替代
4、接口隔离原则(Interface isolation principle,ISP)
ISP是指拥有多个客户定制接口好于为所有客户定制一个通用接口。该原则用于简化实体键的数据交换,一个好的例子是将垃圾、堆肥即回收利用分开,而不是使用一个包含三个特定垃圾的大型垃圾箱。
5、依赖倒置原则(DIP)
DIP是一个指导我们依赖抽象而非具体的原则。也就是说我们应该构建分层或者对象的继承树。
1. 单一责任原则(SRP)
当需要修改某个类的时候原因有且只有一个。换句话说就是让一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。 类被修改的几率很大,因此应该专注于单一的功能。如果你把多个功能放在同一个类中,功能之间就形成了关联,改变其中一个功能,有可能中止另一个功能,这时就需要新一轮的测试来避免可能出现的问题,非常耗时耗力。
示例:
新建一个Rectangle类,该类包含两个方法,一个用于把矩形绘制在屏幕上,一个方法用于计算矩形的面积。
Rectangle类违反了SRP原则。Rectangle类具有两个职责,如果其中一个改变,会影响到两个应用程序的变化。
一个好的设计是把两个职责分离出来放在两个不同的类中,这样任何一个变化都不会影响到其他的应用程序。
2. 开放封闭原则****(OCP)
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。
(1)通过增加代码来扩展功能,而不是修改已经存在的代码。
(2)若客户模块和服务模块遵循同一个接口来设计,则客户模块可以不关心服务模块的类型,服务模块可以方便扩展服务(代码)。
(3)OCP支持替换的服务,而不用修改客户模块。
示例:
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.widththis.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。
总结:
一个对象只承担一种责任,所有服务接口只通过它来执行这种任务。
程序实体,比如类和对象,向扩展行为开放,向修改行为关闭。
子类应该可以用来替代它所继承的类。
一个类对另一个类的依赖应该限制在最小化的接口上。
依赖抽象层(接口),而不是具体类。