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

S.O.L.I.D原则是面向对象设计(OOD)和面向对象编程(OOP)的重要原则,它是其他五个省略词的组合--SRP、OCP、LSP、ISP、DIP,是由Robert C.Martin大叔提出的。

SRPThe Single Responsibility Principle单一责任原则导致类发生变化的原因有且只有一个
OCPThe Open Closed Principle开放封闭原则软件实体对扩展开发,对修改关闭
LSPThe Liskov Substitution Principle里氏替换原则子类能够替换它所继承的父类
DIPThe Dependency Inversion Principle依赖倒置原则依赖抽象,而不是具体实现
ISPThe 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类也不需要修改。这就是典型的”高层次模块不应该依赖于低层次模块,两者都应该依赖于抽象“。

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




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值