掌握设计模式精髓:五大核心原则深度解析

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

定义:

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

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

案例:

客户信息图形统计模块

20241229154732_pLo4amG4.webp

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

重构

20241229154732_2jnU7yLX.webp

代码实现:

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
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("获取数据库连接");
}
}

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

定义:

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

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

案例

20241229154732_sxgIpcu4.webp

代码

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

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

重构

20241229154732_IKIGT8WG.webp

代码:

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
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();
}
}

里氏替换 (Liskov Substitution Principle, LSP)

定义:

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

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

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

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

案例

20241229154732_C3xVQGJs.webp

重构

20241229154732_a9HhpLh9.webp

代码

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
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);
}
}

依赖倒转 (Dependency Inversion Principle, DIP)

定义:

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

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

案例

20241229154732_2jnU7yLX.webp

代码

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
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());
}
}

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

重构

20241229154732_zejrvvRa.webp

代码

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
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());
}
}

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

接口隔离 (Interface Segregation Principle, ISP)

定义:

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

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

案例

20241229154732_9YohyQXM.webp

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

重构

20241229154732_RxaRuJtG.webp

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

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

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

定义:

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

作用:
降低耦合度

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

案例

20241229154732_d1buxWKK.webp

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

重构

20241229154732_wW9BZmoq.webp

使用关联关系

代码

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
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();
}
}

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

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

定义:

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

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

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

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

引用