javaSOLID原则
众所周知,Java编程最基本的原则就是要追求⾼内聚和低耦合的解决⽅案和代码模块设计,S.O.L.I.D是⾯向对象设计和编程(OOD&OOP)中⼏个重要编码原则
(Programming Priciple)的⾸字母缩写。
SRP单⼀责任原则
OCP开放封闭原则
LSP⾥⽒替换原则
ISP接⼝分离原则
DIP依赖倒置原则
1. 单⼀责任原则(SRP)
当需要修改某个类的时候原因有且只有⼀个。换句话说就是让⼀个类只做⼀种类型责任,当这个类需要承当其他类型的责任的时候,就
需要分解这个类。类被修改的⼏率很⼤,因此应该专注于单⼀的功能。如果你把多个功能放在同⼀个类中,功能之间就形成了关联,改变其中⼀个功能,有可能中⽌另⼀个功能,这时就需要新⼀轮的测试来避免可能出现的问题,⾮常耗时耗⼒。
⽰例:
新建⼀个Rectangle类,该类包含两个⽅法,⼀个⽤于把矩形绘制在屏幕上,⼀个⽅法⽤于计算矩形的⾯积。如图
Rectangle类违反了SRP原则。Rectangle类具有两个职责,如果其中⼀个改变,会影响到两个应⽤程序的变化。
⼀个好的设计是把两个职责分离出来放在两个不同的类中,这样任何⼀个变化都不会影响到其他的应⽤程序。
2. 开放封闭原则(OCP)
软件实体应该是可扩展,⽽不可修改的。也就是说,对扩展是开放的,⽽对修改是封闭的。这个原则是诸多⾯向对象编程原则中最抽象、最难理解的⼀个。
(1)通过增加代码来扩展功能,⽽不是修改已经存在的代码。
(2)若客户模块和服务模块遵循同⼀个接⼝来设计,则客户模块可以不关⼼服务模块的类型,服务模块可以⽅便扩展服务(代码)。
(3)OCP⽀持替换的服务,⽽不⽤修改客户模块。
⽰例:
public boolean ndByEmail(String addr, String title, String content) {
}
public boolean ndBySMS(String addr, String content) {
}
// 在其它地⽅调⽤上述⽅法发送信息
ndByEmail(addr, title, content);
ndBySMS(addr, content);
如果现在⼜多了⼀种发送信息的⽅式,⽐如可以通过QQ发送信息,那么不仅需要增加⼀个⽅法ndByQQ(),还需要在调⽤它的地⽅进⾏修改,违反了OCP原则,更好的⽅式是
public boolean nd(int type, String addr, String title, String content) {
if(type == 0) {
// 通过所有⽅式发送
}
if(type == 1) {
// 通过Email发送
}
if(type == 2) {
// 通过...发送
...
}
}
// 在其它地⽅调⽤上述⽅法发送信息
nd(0, addr, title, content);
3. ⾥⽒替换原则(LSP)
当⼀个⼦类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系
客户模块不应关⼼服务模块的是如何⼯作的;同样的接⼝模块之间,可以在不知道服务模块代码的情况下,进⾏替换。即接⼝或⽗类出现的地⽅,实现接⼝的类或⼦类可以代⼊。
⽰例:
public class Rectangle {
private double width;
private double height;
public void tWidth(double value) {
this.width = value;
}
public double getWidth() {
return this.width;
}
public void tHeight(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中就只能调⽤⽗类的属性来t /get,具体省略 */
}
// 测试
void TestRectangle(Rectangle r) {
r.Weight=10;
r.Height=20;
Asrt.AreEqual(10,r.Weight);
Asrt.AreEqual(200,r.Area);
}
// 运⾏良好
Rectangle r = new Rectangle ();
TestRectangle(r);
// 现在两个Asrt测试都失败了
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》⼀书中“Refud Bequest”(拒收的遗赠)描述的更详尽:⼦类继承⽗类的methods和data,但⼦类仅仅只需要⽗类的部分Methods或data,⽽不是全部methods 和data;当这种情况出现时,就意味这我们的继承体系出现了问题。例如上⾯的Rectangle和Square,Square本⾝长和宽相等,⼏何学中⽤边长来表⽰边,⽽Rectangle长和宽之分,直观地看,Square已经Refud了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。
这⼏条原则是⾮常基础⽽且重要的⾯向对象设计原则。正是由于这些原则的基础性,理解、融汇贯通这些原则需要不少的经验和知识的积累。举的例⼦可能不太贴切也不太准确,反正理解了就⾏,以后去公司实习什么的⼀定要遵循这些原则,不能让⾃⼰写的代码让别⼈批的⼀⽆是处然后胎死腹中,当然还有其他的⼀些很重要的原则,我会在后⾯的时间⾥继续学习和分享!