设计模式之七大原则


1/16/2017 设计模式

设计模式基本原则

单一职责原则 (Single Responsibility Principle, SRP)

定义:

一个类只负责一个功能领域中相应职责 (对一个类而言,应该只有一个引起它变化的原因)

作用: 实现高内聚,低耦合

案例:

客户信息图形统计模块

违背单一职责原则 如果修改数据库连接方式或者修改图标显示方式都需要修改这个类; 不能重用数据库连接的代码

重构

代码实现:

class CustomerDataChart {
    private CustomerDao customerDao;

    public void createChart(){
        System.out.println("创建图表");
    }

    public void displayChart(){
        System.out.println("显示图表");
    }
}

class CustomerDao {
    private DBUtil dbUtil;

    public List<Customer> findCustomers() {
        System.out.println("获取全部的客户列表");
    }
}

class DBUtil {
    public Connection getConnection() {
        System.out.println("获取数据库连接");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

开闭原则 (Open-Closed Principle, OCP)

定义:

一个软件实体应当对扩展开放, 对修改关闭 (尽量不修改原来的代码, 而是添加新代码实现需求)

作用: 使系统拥有适应性和灵活性,同时具备较好的稳定性和延续性

案例

代码

......  
if (type.equals("pie")) {  
    PieChart chart = new PieChart();  
    chart.display();  
}  
else if (type.equals("bar")) {  
    BarChart chart = new BarChart();  
    chart.display();  
}  
...... 
1
2
3
4
5
6
7
8
9
10

问题: 当需要新增一种图表显示时,必须修改源代码,增加判断语句,违背开闭原则

重构

代码:

class ChartDisplay {
	private AbstractChart chart;

	public void setChart(AbstractChart chart) {
		this.chart = chart;
	}

	public void display(){
		chart.display();
	}
}

abstract class AbstractChart {
	public abstract void display();
}

class PieChart extends AbstractChart {
	public void display() {
		System.out.println("圆饼图形显示");
	}
}

class BarChart extends AbstractChart {
	public void display() {
		System.out.println("条形图形显示");
	}
}

// 新增显示类, 不需要修改原有代码
class CurveChart extends AbstractChart {
	public void display() {
		System.out.println("曲线图形显示");
	}
}

public class OCPTest {
	public static void main(String[] args) {
		ChartDisplay c	= new ChartDisplay();
		// 修改客户端代码, 或者使用 xml 或者 properties 配置文件,修改配置文件实现类字符串实现
		AbstractChart chart = new CurveChart();
		c.setChart(chart);
		c.display();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

里氏替换 (Liskov Substitution Principle, LSP)

定义:

所有引用基类的地方必须能透明的使用其子类的对象 在软件中将一个基类对象替换成它的子类对象, 程序将不会产生任何错误和异常, 反过来则不成立, 如果一个软件实体使用的是一个子类对象的话, 那么它不一定能够使用基类对象.

作用: 里氏代换原则是实现开闭原则的重要方式之一, 由于使用基类对象的地方都可以使用子类对象, 因此在程序中尽量使用基类类型来对对象进行定义, 而在运行时再确定其子类类型, 用子类对象来替换父类对象.

在使用里氏代换原则时需要注意如下几个问题:

  1. 子类的所有方法必须在父类中声明, 或子类必须实现父类中声明的所有方法. 根据里氏代换原则, 为了保证系统的扩展性, 在程序中通常使用父类来进行定义, 如果一个方法只存在子类中, 在父类中不提供相应的声明, 则无法在以父类定义的对象中使用该方法.
  2. 我们在运用里氏代换原则时, 尽量把父类设计为抽象类或者接口, 让子类继承父类或实现父接口, 并实现在父类中声明的方法, 运行时, 子类实例替换父类实例, 我们可以很方便地扩展系统的功能, 同时无须修改原有子类的代码, 增加新的功能可以通过增加一个新的子类来实现. 里氏代换原则是开闭原则的具体实现手段之一.
  3. Java 语言中, 在编译阶段, Java 编译器会检查一个程序是否符合里氏代换原则, 这是一个与实现无关的、纯语法意义上的检查, 但 Java 编译器的检查是有局限的.

案例

重构

代码

class EmailSender {
	public void send(Customer customer){
		System.out.print(customer.getName() + "发送邮件");
	}
}

abstract class Customer {
	protected String name;
	protected String email;
	public void setName(String name){
		this.name = name;
	}
	public String getName(){
		return name;
	}
	public void setEmail(String email){
		this.email = email;
	}
	public String getEmail(){
		return email;
	}
}

class CommonCustomer extends Customer{
	
} 

class VIPCustomer extends Customer{
	
} 

public class LSPTest {
	public static void main(String[] args) {
		Customer customer = new CommonCustomer();
		customer.setName("普通用户");
		new EmailSender().send(customer);
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

依赖倒转 (Dependency Inversion Principle, DIP)

定义:

抽象不应该依赖于细节,细节应当依赖于抽象(针对接口编程,而不是针对实现编程) 依赖倒转原则要求我们在程序代码中传递参数时或在关联关系中, 尽量引用层次高的抽象层类, 即使用接口和抽象类进行变量类型声明、参数类型声明、方法返回类型声明, 以及数据类型的转换等, 而不要用具体类来做这些事情.

作用: 在引入抽象层后, 系统将具有很好的灵活性, 在程序中尽量使用抽象层进行编程, 而将具体类写在配置文件中, 这样一来, 如果系统行为发生变化, 只需要对抽象层进行扩展, 并修改配置文件, 而无须修改原有系统的源代码, 在不修改的情况下来扩展系统的功能, 满足开闭原则的要求

案例

代码

class CustomerDao {
	public void addCustomers(TXTDataConvertor convertor){
		convertor.readFile();
		System.out.println("存入数据库");
	}
}

class TXTDataConvertor {
	public void readFile(){
		System.out.println("从文本转换数据");
	}
}

class ExcelDataConvertor {
	public void readFile(){
		System.out.println("从excle转换数据");
	}
}


class DIPTest {
	public static void main(String[] args) {
		CustomerDao customerDao = new CustomerDao();
		customerDao.addCustomers(new TXTDataConvertor());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

当需要从 excel 文件转换数据时,必须修改 CustomerDao 实现代码, 违背开闭原则.

重构

代码

class CustomerDao {
      // 改成抽象类
	public void addCustomers(DataConvertor convertor){
		convertor.readFile();
		System.out.println("存入数据库");
	}
}

abstract class DataConvertor {
	public abstract void readFile();
}

class TXTDataConvertor extends DataConvertor {
	public void readFile(){
		System.out.println("从文本转换数据");
	}
}

class ExcelDataConvertor extends DataConvertor {
	public void readFile(){
		System.out.println("从excle转换数据");
	}
}

class DIPTest {
	public static void main(String[] args) {
		CustomerDao customerDao = new CustomerDao();
		// 这里可以从配置文件中读取类名,然后使用反射创建对象
		customerDao.addCustomers(new TXTDataConvertor());
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

里氏代换原则是基础, 依赖倒转原则是手段, 开闭原则是目标

接口隔离 (Interface Segregation Principle, ISP)

定义:

使用多个专门的接口, 而不使用单一的中接口 ( 客户端不应该依赖那些不需要的接口)

作用: 避免实现不需要的功能, 造成类过大

案例

实现 CustomerDataDisplay , 必须全部实现里面的抽象方法,但是显示类有时候并不需要某些方法, 这是因为 CustomerDataDisplay 声明了太多抽象方法

重构

显示类本身有3个方法, 如果需要其他功能, 可以实现对应功能的接口即可

在使用接口隔离原则时, 我们需要注意控制接口的粒度, 接口不能太小, 如果太小会导致系统中接口泛滥, 不利于维护;接口也不能太大, 太大的接口将违背接口隔离原则, 灵活性较差, 使用起来很不方便. 一般而言, 接口中仅包含为某一类用户定制的方法即可, 不应该强迫客户依赖于那些它们不用的方法.

合成复用原则 (Composite Reuse Principle, CRP)

定义:

尽量使用对象组合, 而不是进程来达到复用的目的 在面向对象设计中, 可以通过两种方法在不同的环境中复用已有的设计和实现, 即通过组合 / 聚合关系或通过继承, 但首先应该考虑使用组合 / 聚合, 组合 / 聚合可以使系统更加灵活, 降低类与类之间的耦合度, 一个类的变化对其他类造成的影响相对较少;其次才考虑继承, 在使用继承时, 需要严格遵循里氏代换原则, 有效使用继承会有助于对问题的理解, 降低复杂度, 而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度, 因此需要慎重使用继承复用.

作用: 降低耦合度

is-a 关系 (继承): 一个类是另一个的 一种 has-a 关系 (组合/聚合): 某个角色具有某一项责任

案例

将获取数据库连接的方法提取到 DBUtil 中, CustomerDao 继承 DBUtil 当需要更换数据库时, 必须修改 DBUtil 代码,违反了开闭原则,或者修改 CustomerDao, 继承另一个获取数据库连接实现类,同样潍坊了开闭原则

重构

使用关联关系

代码

class DBUtil {
	public Connection getConnection(){
		System.out.println("得到数据库连接");
		return null;
	}
}
class OracleDBUtil extends DBUtil {
	public Connection getConnection(){
		System.out.println("得到Oracle数据库连接");
		return null;
	}
}

class CustomerDao {
	private DBUtil dbUtil;
	public CustomerDao(DBUtil dbUtil){
		this.dbUtil = dbUtil;
	}
	public void addCustomerDao(){
		dbUtil.getConnection();
		System.out.println("添加操作");
	}
}
public class CRPTest {
	public static void main(String[] args) {
		DBUtil dbUtil = new OracleDBUtil();
		// 可以从配置文件中获取数据库实现类,然后使用反射创建对象
		CustomerDao customerDao = new CustomerDao(dbUtil);
		customerDao.addCustomerDao();
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

这样重构后,当再次更换数据库时,只需要添加一个获取数据库的实现类,然后继承 DBUtil 即可.不需要更改任何代码

迪米特法则 (Law of Demeter, LoD)

定义:

一个软件实体应当尽可能少地与其他实体发生相互作用. 如果一个系统符合迪米特法则, 那么当其中某一个模块发生修改时, 就会尽量少地影响其他模块, 扩展会相对容易, 这是对软件实体之间通信的限制, 迪米特法则要求限制软件实体之间通信的宽度和深度. 迪米特法则可降低系统的耦合度, 使类与类之间保持松散的耦合关系.

作用: 使系统更容易扩展, 降低耦合度

迪米特法则还有几种定义形式, 包括: 不要和 “陌生人” 说话、只与你的直接朋友通信等, 在迪米特法则中, 对于一个对象, 其朋友包括以下几类:

  1. 当前对象本身 (this);
  2. 以参数形式传入到当前对象方法中的对象;
  3. 当前对象的成员对象;
  4. 如果当前对象的成员对象是一个集合, 那么集合中的元素也都是朋友;
  5. 当前对象所创建的对象.

引用

面向对象设计原则之单一职责原则 面向对象设计原则之开闭原则 面向对象设计原则之里氏代换原则 面向对象设计原则之依赖倒转原则 面向对象设计原则之接口隔离原则 面向对象设计原则之合成复用原则 面向对象设计原则之迪米特法则

Last Updated: 7/3/2019, 6:17:56 PM