面向对象的7种设计原则
面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。
常见的面向对象设计原则主要有7种:
- 单一职责原则 (Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责
- 开闭原则 (Open-Closed Principle, OCP):软件实体应对扩展开放,而对修改关闭
- 里氏代换原则 (Liskov Substitution Principle, LSP):所有引用基类对象的地方能够透明地使用其子类的对象
- 依赖倒转原则 (Dependence Inversion Principle, DIP):抽象不应该依赖于细节,细节应该依赖于抽象
- 接口隔离原则 (Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口
- 合成复用原则 (Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的
- 迪米特法则 (Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用
1. 单一职责原则
单一职责原则(Single Responsibility Principle, SRP):一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
初始方案
public class CustomerDataChart{
//用于连接数据库
public Connection getConnection(){
...
}
//用于查询所有的客户信息
public List findCustomers(){
...
}
//用于创建图表
public void createChart(){
...
}
//用于显示图表
public void displayChart(){
...
}
}
数据库的链接以及查询客户信息不是CustomerDataChart独有的,其他类也会用到这些功能,同时如果要更改数据库信息或者显示图标的信息,都会更改该类,引起它变化的原因就不止一个了。
使用单一职责原则重构后的方案
public class CustomerDataChart{
private CustomerDao dao;
public void createChart(){
...
}
public void displayChart(){
...
}
}
public class CustomerDao{
private DBUtil util;
public List findCustomers(){
...
}
}
public class DNUtil{
public Connection getConnection(){
...
}
}
个人理解:不能将各种职责耦合在一个类中,要将这些职责进行分离,不同的职责封装在不同类中,不同变化的原因封装在不同类中,总同时发生改变的多个职责封装在同一个类中。
2. 开闭原则
开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
初始方案
public class ChartDisplay{
public void display(String type){
if(type.equals("pie")){
PieChart chart=new PieChart();
chart.display();
}
if(type.equals("bar")){
BarChart chart=new Barchart();
chart.display();
}
}
}
public class PieChart{
public void display(){
...
}
}
public class BarChart{
public void display(){
...
}
}
如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。
对系统进行重构,是其符合开闭原则
public class ChartDisplay{
private AbstractChart chart;
public void setChart(AbstractCahrt chart){
this.chart=chart;
}
public void disolay{
chart.display();
}
}
public abstract class AbstractChart{
pulic void display();
}
public PieChart extends AbstractChart{
@override
public void display(){
...
}
}
public BarChart extends AbstractChart{
@override
public void display(){
...
}
}
个人理解:使用接口、抽象类等机制来定义系统的抽象层,扩展则通过增加新的具体类,否则将需要修改已有代码。所以系统在扩展的时候不需要对源代码(如ChartDisplay
)进行修改就符合开闭原则。开闭原则是面向对象设计的目标。
3. 里氏代换原则
里氏代换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象。
例如有两个类,一个类为BaseClass,另一个是SubClass类,并且SubClass类是BaseClass类的子类,那么一个方法如果可以接受一个BaseClass类型的基类对象base的话,如:method1(base),那么它必然可以接受一个BaseClass类型的子类对象sub,method1(sub)能够正常运行。反过来的代换不成立,如一个方法method2接受BaseClass类型的子类对象sub为参数:method2(sub),那么一般而言不可以有method2(base),除非是重载方法。
public class EmailSender{
public void send(CommonCustomer customer){
...
}
public void send(VIPCustomer customer){
...
}
}
public class CommonCustomer{
public void test(){
...
}
}
public class VIPCustomer{
public void test(){
...
}
}
使用抽象客户类Customer重构后
public class EmailSender{
public void send(Customer customer){
...
}
}
public Abstract class Customer{
public void test();
}
public class CommonCustomer extends Customer{
public void test(){
...
}
}
public class VIPCustomer extends Customer{
public void test(){
...
}
}
个人理解:里氏代换原则是实现开闭原则的重要方式之一。针对基类编程,在程序运行时再确定具体子类,可以在传递参数、定义成员变量、定义局部变量、确定方法返回类型时都可以使用历史代换原则。
4. 依赖倒转原则
依赖倒转原则定义如下: 依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
为了实现依赖倒转原则,需要针对抽象层编程,将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
初始系统
public class CustomerDao{
public void addCustomers(){
//使用TXTDataConvertor
//...
//如果使用ExcelDataConvertor则需要把上面注释掉,添加下面的代码
...
}
}
public class TXTDataConvertor{
public void readFile(){
...
}
}
public class ExcelDataConvertor{
public void readFile(){
...
}
}
使用不同的DataConvertor需要更改Customer的源码,添加新的DataConvertor也需要改源码,不符合开闭原则。
重构后的代码
public class CustomerDao{
//可以使用构造注入,设值注入或接口注入
private DataConvertor convertor;
public void addCustomers(){
convertor.readFile();
}
}
public abstract class DataConvertor{
public void readFile();
}
public class TXTDataConvertor extends DataConvertor{
@override
public void readFile(){
...
}
}
public class ExcelDataConvertor extends DataConvertor{
@override
public void readFile(){
...
}
}
public static void main(String[] args){
//通过配置文件获得DataConvertor的实现类bean
//通过构造,设值或者接口注入到CustoerDao的convertor中
//调用CustomerDao的方法
}
在上述重构过程中,我们使用了开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
个人理解:依赖倒转原则是面向对象设计的主要实现机制之一,针对接口或基类编程,而不是针对实现编程。
5. 接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
初始系统
public interface CustomerDataDisplay{
void dataRead();
void transformToXML();
void createChart();
void displayChart();
void createReport();
void displayReport();
}
如果一个具体的数据显示类无须进行数据转换(源文件本身就是XML格式),但由于实现了该接口,将不得不实现其中声明的transformToXML()方法(至少需要提供一个空实现);如果需要创建和显示图表,除了需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序编译时将报错。
将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。
public interface DataHandler{
void dataRead();
}
public interface XMLTransformer{
void transformToXML();
}
public interface ChartHandler{
void createChart();
void displayChart();
}
public interface ReportHandler{
void createReport();
void displayReport();
}
public class ConcreteClass implements DataHandler,ChartHandler{
...
}
个人理解:每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。
6. 合成复用原则
合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的。
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用。
组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用。
如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。
初始系统
public class DBUtil{
public Connection getConnection(){
...
}
}
public class CustomerDao extends DBUtils{
public void addCustomer(){
Connection connection=super.getConnection
...
}
}
更换数据库链接方式需要修改CustomerDao或者DBUtils的源码,违反了开闭原则,使用合成复用原则对其进行重构
public class CustomerDao{
private DBUtil util;
public void addCustomer(){
Connection connection=util.getConnection();
...
}
}
public class DBUtil(){
public Connection getConnection(){
...
}
}
public class MysqlDBUtil extends DBUtil{
public Connection getConnection(){
...
}
}
可以用过依赖注入的方式将DBUtil对象注入到CustomerDao中,而MysqlDBUtil是DBUtil的子类也可注入进去
个人理解:通过对象组合来进行复用可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。
7. 迪米特法则
迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
(1) 当前对象本身(this);
(2) 以参数形式传入到当前对象方法中的对象;
(3) 当前对象的成员对象;
(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5) 当前对象所创建的对象。
任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互,这样做可以降低系统的耦合度,一个对象的改变不会给太多其他对象带来影响。
原始系统
public class A{
private B b;
private C c;
private D d;
public void aChange(){
b...
c...
d...
}
}
public class B{
private C c;
public void bChange(){
c...
}
}
public class C{}
public class D{}
由于类之间的交互关系复杂,导致在该系统中增加新的对象时需要修改与之交互的其它类的源代码,系统扩展性较差,也不便于增加和删除新对象。
可以通过引入一个专门用于控制对象间交互的中介类(Mediator)来降低各对象之间的耦合度。引入中间类之后,相关对象之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其它对象的调用。当需要增加或删除新的对象时,只需修改中间类即可,无须修改新增对象或已有对象的源代码,重构后如下:
public class Mediator{
private Class a;
private Class b;
private Class c;
private Class d;
public void aChange(){
b...
c...
d...
}
public void bChange(){
c...
}
}
个人理解:应当尽可能少地与其他实体发生相互作用,过引入一个合理的第三者来降低现有对象之间的耦合度。