一、面向对象的基础
在认识开闭原则之前,我们先提几个面向对象中几个核心的词语:封装(对于外部是黑盒)、继承(代码重用)、多态(行为差异),小菜鸟记性很差,具体这个定义也记不住了,我们使用chatgpt给我们解释下:
- 封装:通过将数据和操作数据的代码捆绑在一起,隐藏了对象的内部实现细节,只向外部提供有限的接口。这样做的目的是保护数据,防止外部代码随意修改数据,同时也提高了代码的重用性。封装的主要手段包括构造函数、访问修饰符、getter和setter方法等。
- 继承:通过继承,子类可以获得父类的所有属性和方法,同时也可以添加自己特有的属性和方法。这样做的目的是实现了代码的重用,使得子类可以扩展父类的功能,而不需要重新编写相同的代码。继承是一种强大的工具,但也需要注意过度使用继承可能导致的代码复杂化问题。
- 多态:同一消息被不同的对象接收时可以有不同的行为。多态性是面向对象编程中最重要的特性之一,它提高了代码的可读性和可维护性,同时也使得代码更具通用性和可扩展性。多态的实现需要使用继承和动态绑定等技术。
- 上面这三大核心,也是我们在学习面向对象基础的时候,跳不过去的学习章节。也是在工作中经常使用到的一些理论知识。
对于封装、继承、多态,稍微有那么点工作经验的同学,都经常用到(如果你说你没用到过,简直是在胡扯,哈哈哈)。
在面向对象中,除了上面说的封装、继承、多态,还有一个比较重要的层面-抽象,那么什么是抽象呢?抽象是对一种忽略主题中与当前目标无关的方面,重点认知与把握有关的方面。其次,抽象不关心细节,也不关心问题的全部方面,只关心问题的部分方面。
二、认识开闭原则
那么我们现在回到正题上来,聊一聊开闭原则:对扩展开放、对修改关闭。我们要正确的认知这十个字。对扩展开放很好理解,大白话的意思就是在不改变原有的东西下,加一些外部的东西,我们只需要关心新增的这部分东西的正确性和稳定性;那么对修改关闭,就不要一味的理解为,凡是现有的东西,都不能修改,只是我们要做到对已存在东西的侵入度做到最低。说到这里,是不是想起来我们前两篇文章谈论的单一职责原则和依赖倒置原则中提及到的对扩展开放,对修改关闭。只不过依赖倒置是开闭原则的一种基于多态实现的对扩展开放、对修改关闭的体现。
存在即合理,既然开闭原则存在了那么多年了,我们来研究下,在我们实际工作中,我们如何更好的做到对扩展开放呢?,那就是把握住变化的层面和不变的层面,隔离变化的层面,封装不变的层面。
1、对扩展开放
在我们平时写功能需求的时候,都会有参数校验、业务逻辑处理、服务调用、数据库交互、接口返回值转换与封装等操作(抛开简单无脑的crud不谈)。流程一般如下:
举个例子来全面认知下这个事情。随着生活水平的提高,大家没事都喜欢没事喝个奶茶、喝个咖啡,那么我们现在准备开个饮品店。里面有两个个师傅,一个奶茶师傅、一个咖啡师傅,每个师傅分别制作不同的饮品。
我们现在把关注点放在每个不同的师傅做饮品的这个处理单元中来,我们先看什么是不变的东西,首先,不管是哪种饮品,都需要水、然后放入原料制作,制作完成之后装杯、然后根据不同的饮品走不同的制作流程,然后不同类型的杯子包装,下面我们来一点点的剥离这件事情。
对于每种饮品都需要水,那么从水源处取水这个事情是不变的,我们将不可变的进行封装,我们现在用代码来模拟一下,首先来看不变的部分:
csharp
/**
* 基类,主要负责输入输出的包装
* @param <Input>
* @param <Output>
*/
public abstract class BaseAbstractMake<Input,Output> {
protected Input input;
public Input getInput() {
return input;
}
public void setInput(Input input) {
this.input = input;
}
//输出
public abstract Output make() throws Exception;
}
scala
/**
*
* 该抽象类,主要封装一些需要外部服务的方法
*/
public abstract class AbstractMake<Input, Output> extends BaseAbstractMake<Input, Output> {
protected WaterService waterService;
/**
* 提供公共取水的方法
*/
protected void getWater(){
waterService.getWater();
}
}
csharp
/**
* 整体流程的把控
* @param <Input>
* @param <Output>
*/
public abstract class AbstractMakeDrink<Input, Output> extends AbstractMake<Input, Output> {
@Override
public Output make() {
check();
beforeProcessing();
processing();
afterProcessing();
return cup();
}
//制作中的处理
protected abstract void processing();
//制作之前的处理
protected abstract void beforeProcessing();
//制作之后的处理
protected abstract void afterProcessing();
//装杯
protected abstract Output cup();
public abstract void check();
}
那么可变的是什么,可变的是每种饮品的制作流程不一样、包括每种原材料的检查是否合格的方式,制作前的准备,制作中的处理方式、制作完成后饮品种类不同,用不同的杯子包装。那么对于这些可变的东西,我们进行隔离,我们用代码模拟一下:
csharp
public class MakeCoffee extends AbstractMakeDrink <CoffeePowder, Coffee> {
private Coffee coffee;
@Override
protected void processing() {
System.out.println("正在做咖啡");
}
@Override
protected void beforeProcessing() {
System.out.println("做咖啡前的准备工作");
}
@Override
protected void afterProcessing() {
System.out.println("咖啡做完了,准备装杯");
coffee = new Coffee();
}
@Override
protected Coffee cup() {
System.out.println("使用高端咖啡杯装杯");
return coffee;
}
@Override
public void check() {
this.getInput();
System.out.println("对咖啡粉的进行检查,是否合格、过期等");
}
}
csharp
public class MakeMilkTea extends AbstractMakeDrink<MilkTeaRawMaterial, MilkTea> {
private MilkTea milkTea;
@Override
protected void processing() {
System.out.println("制作奶茶");
}
@Override
protected void beforeProcessing() {
System.out.println("做奶茶前的准备工作");
}
@Override
protected void afterProcessing() {
System.out.println("奶茶做好了,准备装杯");
milkTea = new MilkTea();
}
@Override
protected MilkTea cup() {
System.out.println("使用高端奶茶杯装杯");
return milkTea;
}
@Override
public void check() {
this.getInput();
System.out.println("对奶茶原材料的进行检查,是否合格、过期等");
}
}
那么这么处理之后,我们如果再想打造一个爆款的饮料,是不是就很容易扩展呢,假如我们又增加了一种天然茶水的饮品,那么我们只需要无缝扩展就行了,如下:
csharp
public class MakeTeaWater extends AbstractMakeDrink<TeaLeaf, Tea> {
private Tea tea;
@Override
protected void processing() {
System.out.println("正在做茶水中");
}
@Override
protected void beforeProcessing() {
System.out.println("做茶水前");
}
@Override
protected void afterProcessing() {
System.out.println("茶水师傅做茶水,准备装杯");
tea = new Tea();
}
@Override
protected Tea cup() {
System.out.println("使用高端茶杯装杯");
return tea;
}
@Override
public void check() {
this.getInput();
System.out.println("对茶叶的进行检查,是否合格、过期等");
}
}
是不是感觉很nice,这不对扩展就开放了吗,哈哈哈。细心的朋友可能已经发现了,这不是模板方法模式吗,对这就是模板方法模式,在第一篇浅谈依赖导致原则的时候,就说过,设计原则是经济基础,设计模式是上层建筑,模板方法模式,其实就是对开闭原则的一种直观实现。
2、对修改关闭
假如我们现在需要修改制作饮品的流程,比如我们现在我们在装杯之后,需要交给饮品质检师进行引用检查,检查口感、味道之类的,那么我们现在只需要新建一个抽象类AbstractMakeDrinkExt继承一下AbstractMakeDrink这个类就行了,然后,具体的饮品类继承这个新建的类,这样我们就做到了对AbstractMakeDrink这个类不修改,进而达到遵循对修改关闭的原则,如下:
scala
public abstract class AbstractMakeDrinkExt<Input,Output> extends AbstractMakeDrink<Input,Output>{
/**
* 质检
*/
protected abstract void qualityInspection();
}
public class MakeTeaWater extends AbstractMakeDrinkExt<TeaLeaf, Tea> {
//此处省略实现
}
那么又有同学问了,这不是原有的实现进行修改了吗?上面说过了,对于修改关闭,我们只需要尽可能的减少对代码的侵入度,就像我们在日常开发中,使用多态的道理一样。
三、总结
在我们日常开发中,良好的抽象能力,可以帮助你减少很多的机械劳动,最后,使用AIGC赞美一下开闭原则:
修改屏蔽不伤神,
抽象封装皆为敬。
多态实现更妙绝,
开闭原则真奇妙。