系列文章目录
第一章 Java 设计模式之七大设计原则
文章目录
- 系列文章目录
- 前言
- 一、单一职责原则
- 1.案例分析
- 2.改进
- 二、开闭原则
- 1.案例分析
- 2.改进
- 三、里氏替换原则
- 1.案例分析
- 2.改进
- 四、依赖倒转原则
- 五、接口隔离原则
- 1.案例分析
- 2.改进
- 六、合成复用原则
- 1.案例分析
- 2.改进
- 七、迪米特原则
- 1.案例分析
- 2.改进
- 总结
前言
面向对象设计原则为支持可维护性复用而诞生,这些原则蕴含在很多设计模式中,它们是从许多设计方案中总结出的指导性原则。面向对象设计原则也是我们用于评价一个设计模式的使用效果的重要指标之一,本文就记录了常见7种面向对象设计原则学习笔记。
一、单一职责原则
单一职责原则是最简单的面向对象设计原则,它用于控制类的粒度大小。单一职责原则定义如下:一个类只负责一个功能领域中的相应职责
,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
一个类(大到模块,小到方法)承担的职责越多,它被复用的可能性就越小,而且一个类承担的职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化时,可能会影响其他职责的运作,因此要将这些职责进行分离,将不同的职责封装在不同的类中,即将不同的变化原因封装在不同的类中,如果多个职责总是同时发生改变则可将它们封装在同一类中。
单一职责原则是实现高内聚、低耦合的指导方针
,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离
1.案例分析
针对某客户关系管理系统中客户信息图形统计模块,设计出如下方案:
public class CustomerDataChart {
public void getConnetion(){
System.out.println("数据库连接...");
}
public void findCustomers(){
System.out.println("查询客户信息...");
}
public void createChart(){
System.out.println("创建图表...");
}
public void displayChart(){
System.out.println("展示图表...");
}
}
问题:CustomerDataChart类承担了太多的职责,既包含与数据库相关的方法,又包含与图表生成和显示相关的方法。如果在其他类中也需要连接数据库或者使用findCustomers()方法查询客户信息,则难以实现代码的重用。无论是修改数据库连接方式还是修改图表显示方式都需要修改该类,它不止一个引起它变化的原因,违背了单一职责原则。因此需要对该类进行拆分,使其满足单一职责原则。
2.改进
类CustomerDataChart拆分为如下三个类:
- DBUtil:负责连接数据库,包含数据库连接方法getConnection();
- CustomerDAO:负责操作数据库中的Customer表,包含对Customer表的增删改查等方法,如findCustomers();
- CustomerDataChart:负责图表的生成和显示,包含方法createChart()和displayChart()
public class CustomerDataChartImprove {
private CustomerDao customerDao;
public void createChart() {
System.out.println("创建图表...");
}
public void displayChart() {
System.out.println("展示图表...");
}
}
public class CustomerDao {
private DBUtil dbUtil;
public void findCustomers() {
System.out.println("查询客户信息...");
}
}
public class DBUtil {
public void getConnetion() {
System.out.println("数据库连接...");
}
}
二、开闭原则
开闭原则是面向对象的可复用设计的第一块基石,它是最重要的面向对象设计原则。其定义如下:开闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展
为了满足开闭原则,需要对系统进行抽象化设计,抽象化是开闭原则的关键
。在Java、C#等编程语言中,可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移至具体的实现层中完成。在很多面向对象编程语言中都提供了接口、抽象类等机制,可以通过它们定义系统的抽象层,再通过具体类来进行扩展。如果需要修改系统的行为,无须对抽象层进行任何改动,只需要增加新的具体类来实现新的业务功能即可,实现在不修改已有代码的基础上扩展系统的功能,达到开闭原则的要求。
1.案例分析
某客户关系管理系统中可以显示各种类型的图表,如饼状图和柱状图等,为了支持多种图表显示方式,设计方案如下:
public class PieChart {
public void display() {
System.out.println("画饼状图表...");
}
}
public class BarChart {
public void display() {
System.out.println("画柱状图表...");
}
}
public class ChartDisplay {
public void display(String type){
if("pie".equals(type)){
PieChart pieChart = new PieChart();
pieChart.display();
}else if("bar".equals(type)){
BarChart barChart = new BarChart();
barChart.display();
}
}
}
public class Client {
public static void main(String[] args) {
ChartDisplay chartDisplay = new ChartDisplay();
chartDisplay.display("pie");
}
}
问题:在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了开闭原则。
2.改进
由于在ChartDisplay类的display()方法中针对每一个图表类编程,因此增加新的图表类不得不修改源代码。可以通过抽象化的方式对系统进行重构,使之增加新的图表类时无须修改源代码,满足开闭原则。具体做法如下:
- 增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;
- ) ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表;
public abstract class AbstractChart {
/**
* 抽象方法
*/
public abstract void display();
}
public class BarChart extends AbstractChart {
@Override
public void display() {
System.out.println("画柱状图表...");
}
}
public class PieChart extends AbstractChart{
@Override
public void display() {
System.out.println("画饼状图表...");
}
}
public class ChartDisplay {
private AbstractChart abstractChart;
public void setAbstractChart(AbstractChart abstractChart) {
this.abstractChart = abstractChart;
}
public void display(){
abstractChart.display();
}
}
public class Client {
public static void main(String[] args) {
ChartDisplay chartDisplay = new ChartDisplay();
chartDisplay.setAbstractChart(new BarChart());
chartDisplay.display();
chartDisplay.setAbstractChart(new PieChart());
chartDisplay.display();
}
}
三、里氏替换原则
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型S是类型T的子类型。这个定义比较拗口且难以理解,因此我们一般使用它的另一个通俗版定义: 里氏替换原则(Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象
。
在使用里氏代换原则时需要注意如下几个问题:
子类的所有方法必须在父类中声明,或子类必须实现父类中声明的所有方法
。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。我们在运用里氏代换原则时,尽量把父类设计为抽象类或者接口,让子类继承父类或实现父接口,并实现在父类中声明的方法
,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。里氏代换原则是开闭原则的具体实现手段之一。- Java语言中,在编译阶段,Java编译器会检查一个程序是否符合里氏代换原则,这是一个与实现无关的、纯语法意义上的检查,但Java编译器的检查是有局限的。
1.案例分析
某客户关系管理系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客(CommonCustomer)两类,系统需要提供一个发送Email的功能,设计方案如下:
public class CommonCustomer {
private String name;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public class VIPCustomer {
private String name;
private String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public class EmailSender {
public void send(CommonCustomer commonCustomer) {
System.out.println("发送普通用户邮件...");
}
public void send(VIPCustomer vipCustomer) {
System.out.println("发送Vip用户邮件...");
}
}
问题:无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构。
2.改进
public abstract class Customer {
protected String name;
protected String email;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
public class EmailSender {
public void send(Customer customer) {
System.out.println("发送" + customer.getName() + "邮件...");
}
}
public class Client {
public static void main(String[] args) {
Customer customer = new CommonCustomer();
customer.setName("普通用户");
EmailSender sender = new EmailSender();
sender.send(customer);
customer = new VIPCustomer();
customer.setName("VIP用户");
sender.send(customer);
}
}
四、依赖倒转原则
如果说开闭原则是面向对象设计的目标的话,那么依赖倒转原则就是面向对象设计的主要实现机制之一,它是系统抽象化的具体实现。定义如下: 依赖倒转原则(Dependency Inversion Principle, DIP):抽象不应该依赖于细节,细节应当依赖于抽象。换言之,要针对接口编程,而不是针对实现编程。
依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。为了确保该原则的应用,一个具体类应当只实现接口或抽象类中声明过的方法,而不要给出多余的方法,否则将无法调用到在子类中增加的新方法。
在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,而将具体类写在配置文件中,这样一来,如果系统行为发生变化,只需要对抽象层进行扩展,并修改配置文件,而无须修改原有系统的源代码,在不修改的情况下来扩展系统的功能,满足开闭原则的要求。
在实现依赖倒转原则时,我们需要针对抽象层编程,而将具体类的对象通过依赖注入(DependencyInjection, DI)的方式注入到其他对象中,依赖注入是指当一个对象要与其他对象发生依赖关系时,通过抽象来注入所依赖的对象。常用的注入方式有三种,分别是:构造注入,设值注入(Setter注入)和接口注入。构造注入是指通过构造函数来传入具体类的对象,设值注入是指通过Setter方法来传入具体类的对象,而接口注入是指通过在接口中声明的业务方法来传入具体类的对象。这些方法在定义时使用的是抽象类型,在运行时再传入具体类型的对象,由子类对象来覆盖父类对象。
参照上面开闭原则例子
五、接口隔离原则
接口隔离原则(Interface Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
根据接口隔离原则,当一个接口太大时,我们需要将它分割成一些更细小的接口,使用该接口的客户端仅需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干。
1.案例分析
某客户关系管理系统中,客户数据显示模块设计如下接口,其中方法dataRead()用于从文件中读取数据,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,方法displayReport()用于显示文字报表。
public interface CustomerDataDisplay {
void dataRead();
void createChart();
void displayChart();
void createReport();
void displayReport();
}
public class ConcreteClass implements CustomerDataDisplay{
@Override
public void dataRead() {
System.out.println("数据读取...");
}
@Override
public void createChart() {
System.out.println("创建图表...");
}
@Override
public void displayChart() {
System.out.println("展示图表...");
}
@Override
public void createReport() {
System.out.println("创建报表...");
}
@Override
public void displayReport() {
System.out.println("展示报表...");
}
}
问题:由于在接口CustomerDataDisplay中定义了太多方法,即该接口承担了太多职责,一方面导致该接口的实现类很庞大,在不同的实现类中都不得不实现接口中定义的所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量;另一方面由于客户端针对大接口编程,将在一定程序上破坏程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。因此需要将该接口按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。
2.改进
public interface DataHanler {
void dataRead();
}
public interface ChartHandler {
void createChart();
void displayChart();
}
public interface ReportHandler {
void createReport();
void displayReport();
}
public class ConcreteClassImprove implements DataHanler, ChartHandler {
@Override
public void dataRead() {
System.out.println("数据读取...");
}
@Override
public void createChart() {
System.out.println("创建图表...");
}
@Override
public void displayChart() {
System.out.println("展示图表...");
}
}
在使用接口隔离原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口隔离原则,灵活性较差,使用起来很不方便。
六、合成复用原则
合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),其定
义如下:尽量使用对象组合,而不是继承来达到复用的目的。
合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。
1.案例分析
某客户关系管理系统中,初期考虑到客户数量不多系统采用MySQL作为数据库,与数据库操作有关的类如CustomerDAO类等都需要连接数据库,连接数据库的方法getConnection()封装在DBUtil类中,由于需要重用DBUtil类的getConnection()方法,设计人员将CustomerDAO作为DBUtil类的子类,设计方案如下:
public class DBUtil {
public void getConnection(){
System.out.println("获取数据库连接...");
}
}
public class CustomerDao extends DBUtil{
public void addCustomer(){
super.getConnection();
System.out.println("添加用户信息...");
}
}
问题:随着客户数量的增加,系统决定升级为Oracle数据库,因此需要增加一个新的OracleDBUtil类来连接Oracle数据库,由于在初始设计方案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接方式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的子类,这将违反开闭原则。
2.改进
public abstract class DBUtil {
public abstract void getConnection();
}
public class OracleDBUtil extends DBUtil{
@Override
public void getConnection() {
System.out.println("获取oracle数据库连接...");
}
}
public class CustomerDao {
private DBUtil dbUtil;
public void addCustomer(){
dbUtil.getConnection();
System.out.println("添加用户信息...");
}
public void setDbUtil(DBUtil dbUtil) {
this.dbUtil = dbUtil;
}
}
CustomerDAO和DBUtil之间的关系由继承关系变为关联关系,采用依赖注入的方式将DBUtil对象注入到CustomerDAO中,可以使用构造注入,也可以使用Setter注入。如果需要对DBUtil的功能进行扩展,可以通过其子类来实现,如通过子类OracleDBUtil来连接Oracle数据库。由于CustomerDAO针对DBUtil编程,根据里氏代换原则,DBUtil子类的对象可以覆盖DBUtil对象,只需在CustomerDAO中注入子类对象即可使用子类所扩展的方法。
七、迪米特原则
迪米特法则(Law of Demeter, LoD):一个软件实体应当尽可能少地与其他实体发生相互作用。
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就会尽量少地影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。迪米特法则可降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则还有几种定义形式,包括:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,对于一个对象,其朋友包括以下几类:
- 当前对象本身(this);
- 以参数形式传入到当前对象方法中的对象;
- 当前对象的成员对象;
- 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
- 当前对象所创建的对象。
1.案例分析
某客户关系管理系统中,包含很多业务操作窗口,在这些窗口中,某些界面控件之间存在复杂的交互关系,一个控件事件的触发将导致多个其他界面控件产生响应,例如,当一个按钮(Button)被单击时,对应的列表框(List)、组合框(ComboBox)、文本框(TextBox)、文本标签(Label)等都将发生改变,在初始设计方案中,界面控件之间的交互关系可简化为如下所示结构:
由于界面控件之间的交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他控件的源代码,系统扩展性较差,也不便于增加和删除新控件。
可以通过引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类之后,界面控件之间不再发生直接引用,而是将请求先转发给中间类,再由中间类来完成对其他控件的调用。当需要增加或删除新的控件时,只需修改中间类即可,无须修改新增控件或已有控件的源代码
2.改进
总结
设计模式是一套经过反复使用的代码设计经验,目的是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 设计模式于己于人于系统都是多赢的,它使得代码编写真正工程化,它是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。