面向对象设计-S.O.L.I.D原则

时间:2022-12-10 14:55:52

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)

导致类发生变化的原因不要超过一个,也就是说一个类有且只有一个职责。这并不是指该类只应该含有一个属性或方法,而是该类的属性或方法都是为一个目的而存在的。例如:

/**
*
* 类描述:矩形类<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...
 }
上述的矩形类有两个不同的职责:计算面积和画图。若应用程序只需要Retangle类计算方法,并不需要画图方法,也得去加载画图方法需要的类库。而且一旦修改,你的重新编译和测试Retangle类。因此根据单一职责原则,修改如下:

1、修改Retangle类,去掉draw()方法:

/**
*
* 类描述:矩形类<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...
 }
2、增加RectangleUI类,继承Rectangle类:

/**
*
* 类描述:矩形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;
}
}
Rectangle类和RectangleUI类职责分明,Rectangle类主要用于几何计算,RectangleUI类主要用于画图(也可以使用继承的几何计算)。这样若是修改,都不用对另一个类进行重新编译和测试。

单一职责原则不仅仅适用于类,同样适用于方法。

开放封闭原则(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信息呢?一般是通过字符判断去发送消息。例如:

/**
*
* 类描述:发送消息<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;
}
}
这其实是违背了OCP原则的。从上述的例子,若是增加微信、短信等,又的去改代码,增加判断。解决方案如下:

1、增加Send接口,提供sendMessage方法:

/**
*
* 类描述:发送接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface Send {

/**
*
* 方法说明:发送消息<br>
*
* @param message 消息
* @return
*/
public boolean sendMessage(String message);
}
2、修改SendEmial类,实现Send接口:

/**
*
* 类描述:发送邮件<br>
*
* @author 戴丹
* @date 2015年12月15日
* @version v1.0
*/
public class SendEmail implements Send{

@Override
public boolean sendMessage(String message) {
return false;
}

}
3、修改SendMessage类:

/**
*
* 类描述:发送消息<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;
}
}
从上述来看,抽离了sendMessage方法,若要增加qq、微信等发送消息,只需要写一个实现Send接口的类,并且作为参数传到SendMessage类的send方法即可。既可以扩展新功能,又不需要修改代码。

里氏替换原则(LSP)

适合继承层次结构,它是指”子类必须能够替换它的基类“,意思是”客户端依赖的父类可以被子类替代,并且不需要了解这个变化“。因此子类可以有特殊的功能,但是必须符合父类的预期行为。这是为了确保继承能够被正确的使用。

例如鸟类,若定义鸟类的父类,一般写成如下:

/**
*
* 类描述:父类:鸟接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface Bird {

public void fly();
}
我们知道鸟会飞,一般会定义一个飞的方法。

再定义子类:老鹰类,继承鸟类,并实现fly()的方法,如下:

/**
*
* 类描述:子类:老鹰<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public class Glede implements Bird {

@Override
public void fly() {
// TODO Auto-generated method stub

}
}
没问题。若定义一个特殊的子类:鸵鸟,继承鸟类。完蛋了,鸵鸟不会飞!!但是它确实属于鸟类。这就违反了LSP原则。修改起来当然也挺简单的,从Brid再派生出会飞的Brid子类和不会飞的Brid子类。

我们再来看在单一职责原则提到的例子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大接口去掉飞和说话的方法:

/**
*
* 类描述:鸟行为接口<br>
*
* @author David
* @date 2015年12月15日
* @version v1.0
*/
public interface IBirdAction {


/**
*
* 方法说明:走<br>
*
*/
public void walk();

/**
*
* 方法说明:吃<br>
*
*/
public void eat();

}
2、为会飞的鸟增加IBirdFlyAction接口,并加上fly方法:

public interface IBirdFlyAction extends IBirdAction {

/**
*
* 方法说明:飞<br>
*
*/
public void fly();
}
3、为会说话的鸟增加IBirdSpeakAction接口,并机上speak方法:
public interface IBirdSpeakAction extends IBirdAction {		/**	 * 	 * 方法说明:说话<br>	 *	 */	public void speak();}
上面为不同的特殊行为,增加了不同的接口,这样不需要的接口就不用实现,例如麻雀没有说话的行为,则不需要实现IBirdSpeakAction接口,而鸵鸟直接实现IBirdAction接口即可。通过上述进行接口分离,使系统更加灵活、稳定。

这时,有人会问是否需要把IBirdAction接口再次分离为IBirdEatAction和IBirdWalkAction接口,可以这样做,当是不赞成。为何有继承?对象共同的属性和方法形成父类,对象特殊的属性和方法形成子类,也就是子类有父类没有的属性或方法,具有特殊性。同样,鸟的共同行为包括了吃和走,因此不赞成分离。并且若接口分离过多,也会对调用者造成一定的麻烦。

依赖倒置原则(DIP)

依赖倒置(依赖反转)原则:

  • 高层次模块不应该依赖于低层次模块,两者都应该依赖于抽象;
  • 抽象不应该依赖于细节,而细节依赖于抽象。

以手机为例,手机是由外壳、屏幕和电池等部分组成,我们可以把手机看成高层次实例/模块,而外壳等组成部分看成低层次实例/模块。若直接把外壳作为手机的实例,不同的外壳,得写不同的实例和不同的手机实例,耦合度很高,看示例:

/**
*
* 类描述:手机<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;
}
}
若是phone要改成红色的外壳,你不但要定义一个RedShell类,而且还得去修改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;
}

}
增加外壳的接口ISheel,让BlackShell和RedBlackShell去实现它,这时我再去创建手机实例时,只要把实现类传出过,就能生成不同外壳的手机,Phone类也不需要修改。这就是典型的”高层次模块不应该依赖于低层次模块,两者都应该依赖于抽象“。

这些原则并不是孤立存在,是相互联系的,就好像鸟的示例,它即违反了里氏替换原则,又违反了接口分离原则。我们学习、理解并使用这些原则,即加深了读面向对象编码的理解,也提供了自己代码的质量。