👉文章示例代码👈

定义

一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

开闭原则强调的是用抽象构建框架,用实现来扩展细节。

场景示例

笔者这里以超市中的商品做一个简单的示例来方便理解。

创建商品接口

首先创建一个商品接口,同时在接口内部定义获取商品ID、名称、价格的各个方法。一般来说,一个商品是一个实体,后续我们可以去写一个类包含商品ID、名称、价格这三个成员变量,这里仅仅为了演示开闭原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @author zhh
* @description 商品接口
* @date 2020-02-04 22:47
*/
public interface IGoods {

/**
* 获取商品ID
*/
Integer getId();

/**
* 获取商品名称
*/
String getName();

/**
* 获取商品价格
*/
Double getPrice();
}

创建商品子类

我们的商品其实是有很多分类的,比如食品、用品、电器等等。

下面笔者以食品为例,创建食品类的同时实现商品接口。

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
/**
* @author zhh
* @description 食品类
* @date 2020-02-04 23:42
*/
public class Food implements IGoods {

private Integer id;

private String name;

private Double price;

public Food(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}

public Integer getId() {
return this.id;
}

public String getName() {
return this.name;
}

public Double getPrice() {
return this.price;
}
}

测试类及输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @author zhh
* @description 测试类
* @date 2020-02-04 23:57
*/
public class Test {

public static void main(String[] args) {
IGoods iGoods = new Food(1, "巧克力", 9.9);

String msg = String.format("商品ID: %s, 商品名称: %s, 商品价格: %s",
iGoods.getId(), iGoods.getName(), iGoods.getPrice());

System.out.println(msg);
}
}

测试类的输出结果如下:

商品ID: 1, 商品名称: 巧克力, 商品价格: 9.9

类结构图

以上示例类的结构图如下所示

image.png

需求变动

假设现在超市进行一系列的商品促销活动,全场食品类的商品打6折,那我们如何来实现这个需求?

方式一

针对上述的场景示例,我们想当然地会进行如下的改动,既快速又方便。

  • 首先在商品接口中新定义一个获取商品折扣价的方法。
1
2
3
4
5
6
7
8
9
10
11
/**
* @author zhh
* @description 商品接口
* @date 2020-02-04 22:47
*/
public interface IGoods {
// 此处省略其余方法

// 新增 获取商品折扣价格 方法
Double getDiscountPrice();
}
  • 然后在具体的商品子类当中去实现该方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author zhh
* @description 食品类
* @date 2020-02-04 23:42
*/
public class Food implements IGoods {
// 此处省略其余方法

// 子类实现该方法
public Double getDiscountPrice() {
return getPrice() * 0.6;
}
}

上述简单的小改动,看样子确实是满足了我们的需求,但是仔细观察你会发现,这种改动不仅修改了接口,同时也对具体的实现类也进行了改动。

问题也就来了。假设我们商品分类有很多,那么所有的商品实现类都需要去实现接口新增的方法。而接口作为一种契约,是不应该经常变化的,它应该是稳定且可靠的。

方式二

通过扩展新建一个食品的子类用来处理食品类打折,覆写食品类获取价格的方法,同时提供一个获取商品原价的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* @author zhh
* @description 食品打折类
* @date 2020-02-05 00:27
*/
public class FoodDiscount extends Food {

public FoodDiscount(Integer id, String name, Double price) {
super(id, name, price);
}

public Double getOriginPrice() {
return super.getPrice();
}

@Override
public Double getPrice() {
return this.getOriginPrice() * 0.6;
}
}

测试类及输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @author zhh
* @description 测试类
* @date 2020-02-04 23:57
*/
public class Test {

public static void main(String[] args) {
IGoods iGoods = new FoodDiscount(1, "巧克力", 9.9);

FoodDiscount foodDiscount = (FoodDiscount) iGoods;

String msg = String.format("商品ID: %s, 商品名称: %s, 商品原价: %s, 商品折后价: %s",
foodDiscount.getId(), foodDiscount.getName(), foodDiscount.getOriginPrice(), foodDiscount.getPrice());

System.out.println(msg);
}
}

测试类的输出结果如下:

商品ID: 1, 商品名称: 巧克力, 商品原价: 9.9, 商品折后价: 5.94

类结构图

方式二改动后类的结构图如下所示

image.png

好处

  • 不修改底层的基类和接口,防止了风险的扩散。
  • 通过继承基类,使得对扩展开放,对于修改接口和基类是关闭的。

优点

提高软件系统的可复用性和可维护性

参考

  • 《Head First 设计模式》
  • 《大话设计模式》