S.O.L.I.D原则是面向对象设计(OOD)和面向对象编程(OOP)的重要原则,它是其他五个省略词的组合--SRP、OCP、LSP、ISP、DIP,是由Robert C.Martin大叔提出的。
SRP | The Single Responsibility Principle | 单一责任原则 | 导致类发生变化的原因有且只有一个 |
OCP | The Open Closed Principle | 开放封闭原则 | 软件实体对扩展开发,对修改关闭 |
LSP | The Liskov Substitution Principle | 里氏替换原则 | 子类能够替换它所继承的父类 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 | 依赖抽象,而不是具体实现 |
ISP | The Interface Segregation Principle | 接口分离原则 | 接口的细粒化 |
单一责任原则(SRP)
导致类发生变化的原因不要超过一个,也就是说一个类有且只有一个职责。这并不是指该类只应该含有一个属性或方法,而是该类的属性或方法都是为一个目的而存在的。例如:
/**上述的矩形类有两个不同的职责:计算面积和画图。若应用程序只需要Retangle类计算方法,并不需要画图方法,也得去加载画图方法需要的类库。而且一旦修改,你的重新编译和测试Retangle类。因此根据单一职责原则,修改如下:
*
* 类描述:矩形类<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
/**
*
* 方法说明:计算面积<br>
*
* @return
*/
public double area() {
return this.height * this.width;
}
/**
*
* 方法说明:画图<br>
*
* @return
*/
public Image draw() {
return null;
}
get... set...
}
1、修改Retangle类,去掉draw()方法:
/**2、增加RectangleUI类,继承Rectangle类:
*
* 类描述:矩形类<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
/**
*
* 方法说明:计算面积<br>
*
* @return
*/
public double area() {
return this.height * this.width;
}
get... set...
}
/**Rectangle类和RectangleUI类职责分明,Rectangle类主要用于几何计算,RectangleUI类主要用于画图(也可以使用继承的几何计算)。这样若是修改,都不用对另一个类进行重新编译和测试。
*
* 类描述:矩形UI类<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class RectangleUI extends Rectangle {
public RectangleUI(double width, double height) {
super(width, height);
}
/**
*
* 方法说明:画图<br>
*
* @return
*/
public Image draw() {
return null;
}
}
单一职责原则不仅仅适用于类,同样适用于方法。
开放封闭原则(OCP)
从面向对象设计角度来看,该原则规则:软件实体(类,模块,函数等)应该对扩展开放,对修改关闭。“对扩展开放”指的是设计类时要考虑到新需求提出时类可以增加新的功能。“对修改关闭”指的是一旦一个类开发完成,除了改正bug就不再修改它。看起来好像是对立的,实际上若能根据类和它的依赖关系,以一种可预见的方法去正确设计它,就能够增加功能而不修改代码。通常是通过依赖关系的抽象实现开放封闭原则,例如:
/**
*
* 类描述:发送消息<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class SendMessage {
/**
*
* 方法说明:发送<br>
*
* @param message 消息类容
* @return
*/
public boolean send(String message) {
SendEmail email = new SendEmail();
boolean isSuccess = email.sendMessage(message);
return isSuccess;
}
}
上述是一个发送消息类,用于发送消息,现在只是发送邮件,若是以后加上发送QQ信息呢?一般是通过字符判断去发送消息。例如:
/**这其实是违背了OCP原则的。从上述的例子,若是增加微信、短信等,又的去改代码,增加判断。解决方案如下:
*
* 类描述:发送消息<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class SendMessage {
/**
*
* 方法说明:发送<br>
*
* @param message
* @param flag
* @return
*/
public boolean send(String message, String flag) {
boolean isSuccess = false;
if ("qq".equals(flag)) {
SendQQ qq = new SendQQ();
isSuccess = qq.send(message);
} else {
SendEmail email = new SendEmail();
isSuccess = email.sendMessage(message);
}
return isSuccess;
}
}
1、增加Send接口,提供sendMessage方法:
/**2、修改SendEmial类,实现Send接口:
*
* 类描述:发送接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface Send {
/**
*
* 方法说明:发送消息<br>
*
* @param message 消息
* @return
*/
public boolean sendMessage(String message);
}
/**3、修改SendMessage类:
*
* 类描述:发送邮件<br>
*
* @author 戴丹
* @date 2015年12月15日
* @version v1.0
*/
public class SendEmail implements Send{
@Override
public boolean sendMessage(String message) {
return false;
}
}
/**从上述来看,抽离了sendMessage方法,若要增加qq、微信等发送消息,只需要写一个实现Send接口的类,并且作为参数传到SendMessage类的send方法即可。既可以扩展新功能,又不需要修改代码。
*
* 类描述:发送消息<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class SendMessage {
/**
*
* 方法说明:发送<br>
*
* @param message
* @param flag
* @return
*/
public boolean send(String message, Send send) {
boolean isSuccess = false;
isSuccess = send.sendMessage(message);
return isSuccess;
}
}
里氏替换原则(LSP)
适合继承层次结构,它是指”子类必须能够替换它的基类“,意思是”客户端依赖的父类可以被子类替代,并且不需要了解这个变化“。因此子类可以有特殊的功能,但是必须符合父类的预期行为。这是为了确保继承能够被正确的使用。
例如鸟类,若定义鸟类的父类,一般写成如下:
/**我们知道鸟会飞,一般会定义一个飞的方法。
*
* 类描述:父类:鸟接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface Bird {
public void fly();
}
再定义子类:老鹰类,继承鸟类,并实现fly()的方法,如下:
/**没问题。若定义一个特殊的子类:鸵鸟,继承鸟类。完蛋了,鸵鸟不会飞!!但是它确实属于鸟类。这就违反了LSP原则。修改起来当然也挺简单的,从Brid再派生出会飞的Brid子类和不会飞的Brid子类。
*
* 类描述:子类:老鹰<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class Glede implements Bird {
@Override
public void fly() {
// TODO Auto-generated method stub
}
}
我们再来看在单一职责原则提到的例子Retangle类,它属于矩形类,数学几何告诉我们,矩形又分为长方形和正方形,而它们之间的区别是宽和高是否相等。而若是让正方形去继承矩形类又会出现问题,若是set宽,宽高就不相等了,就违反了LSP原则。若是在正方形的类重写set方法,又弱化了LSP原则。从这些看来,正方形是不能继承矩形的。
回到面向对象的基本概念,子类继承父类表达的是一种is-A的关系。而鸵鸟确实是属于鸟,正方形确实属于矩形。那如何看待LSP原则?
is-A关系就是针对对象的行为方式而言的。
鸟的飞行为只是它的普通特点,并不具有绝对性,因此要分出会飞的鸟和不会飞的鸟。若有的鸟会说话,也的分出会说话的鸟和不会说话的鸟。这就是从行为来判断is-A的关系。
正方形是特殊的长方形,也就是说正方形就是长方形,它们从对象和行为而言都是一致的,从矩形派生出正方形是不符合对象逻辑的(它只是数学为了特殊区分而分开的),因此长方形只要宽和高相同,我们就认为他是正方形。修改示例:
package com.dsm;
/**
*
* 类描述:矩形类<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class Rectangle {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
/**
*
* 方法说明:计算面积<br>
*
* @return
*/
public double area() {
return this.height * this.width;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
public double getHeight() {
return height;
}
public void setHeight(double height) {
this.height = height;
}
/**
*
* 方法说明:形状<br>
*
* @return
*/
public String shapeName() {
return this.width == this.height ? "Square" : "Rectangle";
}
}
接口分离原则(ISP)
不能强迫用户去依赖他们不需要的接口,换句话说,使用多个专门的功能接口比单一的总接口要好。因为专门的功能接口有职责单一、灵活和可复用的特点,而单一的总结口,接口的方法众多,类与类之间的耦合大,若要实现接口,不需要的方法也要实现,降低了系统灵活性和复用性。
同样以鸟为例,鸟的行为有:飞、吃、走、说话等等,若是把这些行为定义到一个接口中,就会违反LSP和ISP原则,以ISP原则为例,有些鸟类是没有飞行的行为(例如鸵鸟),若是实现该接口,也会实现飞和说话不属于它行为的方法。同样只有一部分的鸟会说话(例如八哥和鹦鹉),它们有说话的行为,而其他鸟类没有。
看原IBirdAction大接口的示例:
/**修改为:
*
* 类描述:鸟行为接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface IBirdAction {
/**
*
* 方法说明:飞<br>
*
*/
public void fly();
/**
*
* 方法说明:走<br>
*
*/
public void walk();
/**
*
* 方法说明:吃<br>
*
*/
public void eat();
/**
*
* 方法说明:说话<br>
*
*/
public void speak();
}
1、IBirdAction大接口去掉飞和说话的方法:
/**2、为会飞的鸟增加IBirdFlyAction接口,并加上fly方法:
*
* 类描述:鸟行为接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface IBirdAction {
/**
*
* 方法说明:走<br>
*
*/
public void walk();
/**
*
* 方法说明:吃<br>
*
*/
public void eat();
}
public interface IBirdFlyAction extends IBirdAction {3、为会说话的鸟增加IBirdSpeakAction接口,并机上speak方法:
/**
*
* 方法说明:飞<br>
*
*/
public void fly();
}
public interface IBirdSpeakAction extends IBirdAction { /** * * 方法说明:说话<br> * */ public void speak();}上面为不同的特殊行为,增加了不同的接口,这样不需要的接口就不用实现,例如麻雀没有说话的行为,则不需要实现IBirdSpeakAction接口,而鸵鸟直接实现IBirdAction接口即可。通过上述进行接口分离,使系统更加灵活、稳定。
这时,有人会问是否需要把IBirdAction接口再次分离为IBirdEatAction和IBirdWalkAction接口,可以这样做,当是不赞成。为何有继承?对象共同的属性和方法形成父类,对象特殊的属性和方法形成子类,也就是子类有父类没有的属性或方法,具有特殊性。同样,鸟的共同行为包括了吃和走,因此不赞成分离。并且若接口分离过多,也会对调用者造成一定的麻烦。
依赖倒置原则(DIP)
依赖倒置(依赖反转)原则:
- 高层次模块不应该依赖于低层次模块,两者都应该依赖于抽象;
- 抽象不应该依赖于细节,而细节依赖于抽象。
以手机为例,手机是由外壳、屏幕和电池等部分组成,我们可以把手机看成高层次实例/模块,而外壳等组成部分看成低层次实例/模块。若直接把外壳作为手机的实例,不同的外壳,得写不同的实例和不同的手机实例,耦合度很高,看示例:
/**若是phone要改成红色的外壳,你不但要定义一个RedShell类,而且还得去修改Phone类,修改十分麻烦。这就需要用到依赖倒置原则,我们重新修改示例:
*
* 类描述:手机<br>
*
* @author 戴丹
* @date 2015年12月16日
* @version v1.0
*/
public class Phone {
// 黑色外壳
private BlackShell blackShell;
public BlackShell getBlackShell() {
return blackShell;
}
public void setBlackShell(BlackShell blackShell) {
this.blackShell = blackShell;
}
}
/**增加外壳的接口ISheel,让BlackShell和RedBlackShell去实现它,这时我再去创建手机实例时,只要把实现类传出过,就能生成不同外壳的手机,Phone类也不需要修改。这就是典型的”高层次模块不应该依赖于低层次模块,两者都应该依赖于抽象“。
*
* 类描述:手机<br>
*
* @author 戴丹
* @date 2015年12月16日
* @version v1.0
*/
public class phone {
// 外壳接口
private IShell shell;
public IShell getShell() {
return shell;
}
public void setShell(IShell shell) {
this.shell = shell;
}
}
这些原则并不是孤立存在,是相互联系的,就好像鸟的示例,它即违反了里氏替换原则,又违反了接口分离原则。我们学习、理解并使用这些原则,即加深了读面向对象编码的理解,也提供了自己代码的质量。