
最近工作之余对之前《统一任务分发调度执行框架》 中的一些通过if else代码进行优化,意在通过策略模式替代原有的if else判断逻辑,以方便后续能进行更好的扩展。
(一)原始代码import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.cloud.commons.lang.StringUtils;
import com.alibaba.fastjson.JSON;
import com.xxx.domain.AjaxResult;
import com.xxx.domain.AcsTask;
import com.xxx.rpc.AcsClientNew;
import com.xxx.service.IAcsTaskService;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
/**
*
* @ClassName: AcsTaskManager
* @Description: ACS任务管理器,统一管理发往ACS的任务(包括直到ACS的任务和需要到达CPE的任务)
* @Author: liulianglin
* @DateTime 2022年1月20日 下午5:02:29
*/
@Component
public class AcsTaskManager {
private static final Logger log = LoggerFactory.getLogger(AcsTaskManager.class);
@Autowired
private AcsClientNew acsClientNew;
@Autowired
private IAcsTaskService acsTaskService;
@Autowired
private AcsTaskCache acsTaskCache;
@Autowired
AcsTaskResultParser acsTaskResultParser;
public AcsTaskManager() {
log.info("AcsTaskManager 启动。。。。");
}
/**
*
* @Description: 任务提交接口: 提供给业务层或者Controller层其他模块使用
* @Author: liuliagnlin
* @Datetime: 2022年1月20日 下午5:44:47
* @param originalTask
* @return AjaxResult
* @throws
*/
public AjaxResult submitTask(OriginalTask originalTask, ICallBack callback) {
if(!validateParam(originalTask)) {
return AjaxResult.error("[ACS任务管理器]--提交任务参数异常, 任务={" + JSON.toJSONString(originalTask) +"}");
}
/*
* ADD -->POST
* DELETE --> DELETE
* UPDATE --> PUT
* QUERY -->GET
*
* actionType:值为ADD DELETE UPDATE QUERY中的一种
*/
if(originalTask.getActionType().equals(ActionTypeEnum.ADD.getActionType())) {
return submitAddTask(originalTask, callback);
}
if(originalTask.getActionType().equals(ActionTypeEnum.QUERY.getActionType())) {
return submitQueryTask(originalTask, callback);
}
if(originalTask.getActionType().equals(ActionTypeEnum.UPDATE.getActionType())) {
return submitPutTask(originalTask, callback);
}
if(originalTask.getActionType().equals(ActionTypeEnum.DELETE.getActionType())) {
return submitDeleteTask(originalTask, callback);
}
log.error(originalTask.getObjectId() +
originalTask.getDescription() +
"任务提交失败,不支持的actionType, 原始任务={"+JSON.toJSONString(originalTask) + "}");
return AjaxResult.error(originalTask.getObjectId() +
originalTask.getDescription() +
"任务提交失败,不支持的actionType");
}
/**
*
* @Description: 对应处理POST请求
* @Author: liuliagnlin
* @Datetime: 2022年1月21日 上午11:46:54
* @return AjaxResult
* @throws
*/
private AjaxResult submitAddTask(OriginalTask originalTask, ICallBack callback) {
/*
*add类型任务可能的URL包括:
* /tasks//retry (走CPE, 没有请求body, objectType为TASKS)
* /devices//tasks?timeout=3000&connection_request=1 (走CPE,请求body为json,objectType为DEVICES_TASKS)
*
*/
if (originalTask.getObjectType().equals(ObjectTypeEnum.DEVICES_TASKS.getObjectType()) ||
originalTask.getObjectType().equals(ObjectTypeEnum.TASKS_RETRY.getObjectType())) {
try {
// 将任务持久化到Mysql中
AcsTask acsTask = storageTask(originalTask, callback);
if(null == acsTask) {
log.error("提交任务失败,acsTask为空");
return AjaxResult.error("提交任务失败, AcsTask为空");
}else {
// 此处用处:这里和taskManager添加任务那里形成一个阻塞唤醒机制,更好的进行线程控制。
// 写入后唤醒可能处于等待状态的AcsTaskDispatcher线程
synchronized (acsTaskCache.getSleepLock()) {
log.info("将一个<{}-{}>任务写入内存中的任务表中, 原始任务originalTask = {}",
originalTask.getObjectId(), originalTask.getDescription(), JSON.toJSONString(originalTask));
if (StringUtils.isNotBlank(originalTask.getTaskPriority()) &&
originalTask.getTaskPriority().equals(TaskPriorityEnum.URGENT.type()) ) {
// 1.处理紧急任务: 将任务添加为任务表头部的第一个元素
log.info("当前任务为紧急任务: 将任务添加为任务表头部的第一个元素");
acsTaskCache.addUrgentOriginalTask(acsTask);
}else {
// 2.处理普通任务: 将任务写入内存中的任务表中,追加到列表尾部
acsTaskCache.addNormalOriginalTask(acsTask);
}
log.info("<{}-{}>任务提交后,任务队列的长度={}", originalTask.getObjectId(), originalTask.getDescription(),
acsTaskCache.getOriginalTaskQueue().size());
acsTaskCache.getSleepLock().notify();
log.info("线程<{}>获取到了锁, 准备唤醒正在wait的线程...", Thread.currentThread().getName());
}
}
} catch(Exception e) {
log.error("[ACS任务管理器]--提交任务失败");
e.printStackTrace();
return AjaxResult.error(originalTask.getObjectId()+ originalTask.getDescription() +"任务提交失败");
}
return AjaxResult.success("[ACS任务管理器]--提交ADD任务成功");
}
log.error(originalTask.getObjectId() +
originalTask.getDescription() +
"任务提交失败,不支持的objectType, 原始任务={"+JSON.toJSONString(originalTask) + "}");
return AjaxResult.error(originalTask.getObjectId() +
originalTask.getDescription() +
"任务提交失败,不支持的objectType");
}
/**
* 对应处理GET请求
* @Description: 目前GET请求都是不到CPE的任务,可以直接调用AcsClientNew进行请求下发及结果处理
* @Author: liuliagnlin
* @Datetime: 2022年1月21日 上午11:47:11
* @return AjaxResult
* @throws
*/
private AjaxResult submitQueryTask(OriginalTask originalTask, ICallBack callback) {
/*
*query类型任务可能的URL包括:
* /tasks/? (不走CPE, objectType为TASKS)
* /tasks/?query={0}
* /devices/?projection=<参数> (不走CPE, objectType为DEVICES)
* /devices/?query=<参数> (不走CPE, objectType为DEVICES)
* /devices/?query=<查找字符串>&projection=<参数路径> (不走CPE, objectType为DEVICES)
*
*/
// 将任务持久化到Mysql中
AcsTask acsTask = storageTask(originalTask, callback);
// 向ACS发起连接并进行结果处理
HttpclientForestResponse response = acsClientNew.execGet(acsTask.getUrl());
log.info("{}{}执行完毕,开始解析结果",originalTask.getObjectId(), originalTask.getDescription() );
return acsTaskResultParser.parseResultForGetReq(acsTask, response);
}
/**
* @Description: 提交PUT请求(系统中暂时还未使用到)
* @Author: liuliagnlin
* @Datetime: 2022年1月25日 上午10:42:52
* @return AjaxResult
* @throws
*/
private AjaxResult submitPutTask(OriginalTask originalTask, ICallBack callback) {
/*
*put类型任务可能的URL包括:
*/
// 将任务持久化到Mysql中
AcsTask acsTask = storageTask(originalTask, callback);
// 向ACS发起连接并进行结果处理
HttpclientForestResponse response = acsClientNew.execPut(acsTask.getUrl(), originalTask.getMsgBody());
// 结果解析, 后期需要更改
log.info("{}{}执行完毕,开始解析结果",originalTask.getObjectId(), originalTask.getDescription() );
return acsTaskResultParser.parseResultForGetReq(acsTask,response);
}
/**
*
* @Description: 提交删除 *** 作请求
* @Author: liuliagnlin
* @Datetime: 2022年1月25日 下午2:23:39
* @return AjaxResult
* @throws
*/
private AjaxResult submitDeleteTask(OriginalTask originalTask, ICallBack callback) {
/*
*delete类型任务可能的URL包括:
* /files/ (不走CPE,objectType为FILES)
* /devices/ (不走CPE, objectType为DEVICES)
* /tasks/ (不走CPE, objectType为TASKS)
*/
AcsTask acsTask = storageTask(originalTask, callback);
HttpclientForestResponse response = acsClientNew.execDelete(acsTask.getUrl());
log.info("{}{}执行完毕,开始解析结果",originalTask.getObjectId(), originalTask.getDescription() );
return acsTaskResultParser.parseResultForDeleteReq(acsTask, response);
}
/**
*
* @Description: 将任务持久化到Mysql中
* @Author: liuliagnlin
* @Datetime: 2022年1月25日 上午10:44:02
* @return AcsTask
* @throws
*/
private AcsTask storageTask(OriginalTask originalTask, ICallBack callback) {
try {
// 动态生成URL
String url = DynamicUrlBuilder.buildMethodUrl(originalTask);
if (StringUtils.isBlank(url)) {
log.error("生成URL为空");
return null;
}
// 转换task
AcsTask acsTask = translateToAcsTask(originalTask, url, callback);
log.info("将{}{}任务转化为内部AcsTask, 转化后的任务=[{}]", originalTask.getObjectId(),
originalTask.getDescription(), JSON.toJSONString(acsTask));
// 将任务写入Mysql任务表,返回的任务ID=acsTask.getId, QUERY类型的任务不再插入到Mysql任务表中。
if (!originalTask.getActionType().equals(ActionTypeEnum.QUERY.getActionType())) {
log.info("将任务插入到ne_task任务表中,taskId={}", acsTask.getId());
acsTaskService.insertAcsTask(acsTask);
}
return acsTask;
}catch(Exception ex) {
ex.printStackTrace();
throw ex;
}
}
/**
*
* @Description: 参数校验
* @Author: liuliagnlin
* @Datetime: 2022年1月20日 下午5:10:34
* @param originalTask
* @return
* @return boolean
* @throws
*/
private boolean validateParam(OriginalTask originalTask) {
// 校验标识:如果为true,表示校验通过
boolean validateFlag = true;
if (null == originalTask) {
log.error("[ACS任务管理器]--提交任务不能为空");
validateFlag = false;
}
if (StringUtils.isBlank(originalTask.getActionType())) {
log.error("[ACS任务管理器]--任务的actionType不能为空");
validateFlag = false;
}
if (StringUtils.isBlank(originalTask.getObjectType())) {
log.error("[ACS任务管理器]--任务的objectType不能为空");
validateFlag = false;
}
//TODO 请求参数校验需要考虑到GET请求的复杂参数类型 liulianglin
return validateFlag;
}
/**
*
* @Description: 将原始任务OriginalTask转为内部的AcsTask, 以便存入Mysql数据库表任务中
* @Author: liuliagnlin
* @Datetime: 2022年1月24日 上午10:38:52
* @return AcsTask
* @throws SecurityException
* @throws NoSuchMethodException
* @throws
*/
private AcsTask translateToAcsTask(OriginalTask originalTask, String url, ICallBack callback) {
if (null == originalTask) {
log.error("[translateToAcsTask]--转换任务:原始任务originalTask不能为空");
return null;
}
AcsTask acsTask = new AcsTask();
acsTask.setActionType(originalTask.getActionType());
acsTask.setObjectType(originalTask.getObjectType());
acsTask.setObjectId(originalTask.getObjectId());
acsTask.setCreateTime(new Date());
if (!originalTask.getActionType().equals(ActionTypeEnum.ADD.getActionType())) {
// 除了POST请求是异步的,其他请求都是直接发送给ACS,所以开始时间近似等于创建时间
acsTask.setStartTime(new Date());
}
acsTask.setMsgBody(JSON.toJSONString(originalTask.getMsgBody()));
acsTask.setProjectionParam(originalTask.getProjectionParam());
acsTask.setQueryParam(originalTask.getQueryParam());
acsTask.setDescription(originalTask.getDescription());
// 设置URL和反射方法名称
acsTask.setUrl(url);
acsTask.setCallback(callback);
acsTask.setExtendsParam(originalTask.getExtendsParam());
return acsTask;
}
}
本次的重点改造区域是submitTask(), 我想消除这些if卫语句,同时将其内部针对submitAddTask、submitQueryTask、submitPutTask、submitDeleteTask进行公用代码抽离,借此消除一些重复冗余代码。
OK,开始编码
(二)策略+工厂+模板的改造之旅1. 首先定义一个Http方法处理策略接口,核心方法:
- handleTask():处理不同类型的请求任务
- getType() : 获取请求类型
import com.xxx.core.web.domain.AjaxResult;
import com.xxx.domain.AcsTask;
/**
*
* @ClassName: HttpMethodHandleStrategy
* @Description: Http方法处理策略接口
* @Author: liulianglin
* @DateTime 2022年4月25日 下午4:22:07
*/
public interface HttpMethodHandleStrategy {
/**
*
* @Description: 处理任务
* @Author: liuliagnlin
* @Datetime: 2022年4月25日 下午4:44:58
* @param acsTask
* @param callback
* @return AjaxResult
* @throws
*/
AjaxResult handleTask(AcsTask acsTask, ICallBack callback);
/**
*
* @Description: 获取类型
* @Author: liuliagnlin
* @Datetime: 2022年4月25日 下午4:45:11
* @return String 请求类型(目前支持的有POST、GET、DELETE、PUT)
*/
String getType();
}
2. 定义一个抽象类BaseStrategy实现HttpMethodHandleStrategy,在BaseStrategy内部对handleTask方法进行重写,这里使用到模板方法模式将一些公用代码进行了抽离,并定义一个sendAcsRequest抽象方法,由子类负责具体的差异化实现。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import com.bonc.common.core.web.domain.AjaxResult;
import com.xxx.domain.AcsTask;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
/**
*
* @ClassName: BaseStrategy
* @Description: 策略模式抽象基类
* @Author: liulianglin
* @DateTime 2022年4月25日 下午7:55:11
*/
public abstract class BaseStrategy implements HttpMethodHandleStrategy{
Logger log = LoggerFactory.getLogger(BaseStrategy.class);
@Autowired
AcsTaskResultParser acsTaskResultParser;
/**
* 处理任务
*/
@Override
public AjaxResult handleTask(AcsTask acsTask, ICallBack callback) {
log.info("将向Acs发送请求...");
HttpclientForestResponse response = sendAcsRequest(acsTask);
log.info("{}{}执行完毕,开始解析结果",acsTask.getObjectId(), acsTask.getDescription() );
return acsTaskResultParser.parseResultForGetReq(acsTask, response);
}
/**
* 这里使用模板设计模式,由子类负责具体的差异化实现
* @Description: 通过AcsClientNew向ACS侧发送请求
* @Author: liuliagnlin
* @Datetime: 2022年4月25日 下午7:10:04
* @param acsTask
* @return HttpclientForestResponse
* @throws
*/
public abstract HttpclientForestResponse sendAcsRequest(AcsTask acsTask);
}
3. 在定义完这些之后,下面就需要去定义具体的处理策略类,这里定义了DeleteMethodHandleStrategy、GetMethodHandleStrategy、PostMethodHandleStrategy、PutMehodHandleStrategy四个策略类分别用于处理客户端提交过来的DELETE、GET、POST、PUT请求(后续还会随着业务的发展,不断加入其他类型)。在四个类中分别对sendAcsRequest方法进行差异化实现。
- DeleteMethodHandleStrategy
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.xxx.domain.AjaxResult;
import com.xxx.domain.AcsTask;
import com.xxx.rpc.AcsClientNew;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
/**
*
* @ClassName: DeleteMethodHandleStrategy
* @Description: DELETE方法处理策略
* @Author: liulianglin
* @DateTime 2022年4月25日 下午4:32:09
*/
@Component
public class DeleteMethodHandleStrategy extends BaseStrategy{
@Autowired
AcsClientNew acsClientNew;
@Override
public AjaxResult handleTask(AcsTask acsTask, ICallBack callback) {
return super.handleTask(acsTask, callback);
}
@Override
public String getType() {
return ActionTypeEnum.DELETE.getActionType();
}
@Override
public HttpclientForestResponse sendAcsRequest(AcsTask acsTask) {
return acsClientNew.execDelete(acsTask.getUrl());
}
}
- GetMethodHandleStrategy
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.xxx.AjaxResult;
import com.xxx.domain.AcsTask;
import com.xxx.rpc.AcsClientNew;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
/**
*
* @ClassName: GetMethodHandleStrategy
* @Description: Get请求处理策略
* @Author: liulianglin
* @DateTime 2022年4月25日 下午4:24:07
*/
@Component
public class GetMethodHandleStrategy extends BaseStrategy{
@Autowired
AcsClientNew acsClientNew;
/*
*query类型任务可能的URL包括:
* /tasks/? (不走CPE, objectType为TASKS)
* /tasks/?query={0}
* /devices/?projection=<参数> (不走CPE, objectType为DEVICES)
* /devices/?query=<参数> (不走CPE, objectType为DEVICES)
* /devices/?query=<查找字符串>&projection=<参数路径> (不走CPE, objectType为DEVICES)
*
*/
@Override
public AjaxResult handleTask(AcsTask acsTask, ICallBack callback) {
return super.handleTask(acsTask, callback);
}
@Override
public String getType() {
return ActionTypeEnum.QUERY.getActionType();
}
@Override
public HttpclientForestResponse sendAcsRequest(AcsTask acsTask) {
return acsClientNew.execGet(acsTask.getUrl());
}
}
- PostMethodHandleStrategy
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.xxx.AjaxResult;
import com.xxx.AcsTask;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
/**
*
* @ClassName: PostMethodHandleStrategy
* @Description: POST方法处理策略
* @Author: liulianglin
* @DateTime 2022年4月25日 下午4:28:29
*/
@Component
public class PostMethodHandleStrategy extends BaseStrategy{
@Autowired
private AcsTaskCache acsTaskCache;
@Override
public AjaxResult handleTask(AcsTask acsTask, ICallBack callback) {
/*
*add类型任务可能的URL包括:
* /tasks//retry (走CPE, 没有请求body, objectType为TASKS)
* /devices//tasks?timeout=3000&connection_request=1 (走CPE,请求body为json,objectType为DEVICES_TASKS)
*
*/
if (acsTask.getObjectType().equals(ObjectTypeEnum.DEVICES_TASKS.getObjectType()) ||
acsTask.getObjectType().equals(ObjectTypeEnum.TASKS_RETRY.getObjectType())) {
try {
// 此处用处:这里和taskManager添加任务那里形成一个阻塞唤醒机制,更好的进行线程控制。
// 写入后唤醒可能处于等待状态的AcsTaskDispatcher线程
synchronized (acsTaskCache.getSleepLock()) {
log.info("将一个<{}-{}>任务写入内存中的任务表中, 原始任务acsTask = {}",
acsTask.getObjectId(), acsTask.getDescription(), JSON.toJSONString(acsTask));
// 2.处理普通任务: 将任务写入内存中的任务表中,追加到列表尾部
acsTaskCache.addNormalOriginalTask(acsTask);
log.info("<{}-{}>任务提交后,任务队列的长度={}", acsTask.getObjectId(), acsTask.getDescription(),
acsTaskCache.getOriginalTaskQueue().size());
acsTaskCache.getSleepLock().notify();
log.info("线程<{}>获取到了锁, 准备唤醒正在wait的线程...", Thread.currentThread().getName());
}
} catch(Exception e) {
log.error("[ACS任务管理器]--提交任务失败");
e.printStackTrace();
return AjaxResult.error(acsTask.getObjectId()+ acsTask.getDescription() +"任务提交失败");
}
return AjaxResult.success("[ACS任务管理器]--提交ADD任务成功");
}
log.error(acsTask.getObjectId() +
acsTask.getDescription() +
"任务提交失败,不支持的objectType, 原始任务={"+JSON.toJSONString(acsTask) + "}");
return AjaxResult.error(acsTask.getObjectId() +
acsTask.getDescription() +
"任务提交失败,不支持的objectType");
}
@Override
public String getType() {
return ActionTypeEnum.ADD.getActionType();
}
@Override
public HttpclientForestResponse sendAcsRequest(AcsTask acsTask) {
return null;
}
}
- PutMehodHandleStrategy
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import com.xxx.AjaxResult;
import com.xxx.domain.AcsTask;
import com.xxx.rpc.AcsClientNew;
import com.dtflys.forest.backend.httpclient.response.HttpclientForestResponse;
/**
*
* @ClassName: PutMehodHandleStrategy
* @Description: PUT方法处理策略
* @Author: liulianglin
* @DateTime 2022年4月25日 下午4:29:31
*/
@Component
public class PutMehodHandleStrategy extends BaseStrategy{
@Autowired
AcsClientNew acsClientNew;
@Override
public AjaxResult handleTask(AcsTask acsTask, ICallBack callback) {
return super.handleTask(acsTask, callback);
}
@Override
public String getType() {
return ActionTypeEnum.UPDATE.getActionType();
}
@SuppressWarnings("unchecked")
@Override
public HttpclientForestResponse sendAcsRequest(AcsTask acsTask) {
return acsClientNew.execPut(acsTask.getUrl(), (Map) JSON.parse(acsTask.getMsgBody()));
}
}
4. 定义完策略类后,就该思考如何获取并使用它们了。这里采用工厂设计模式,通过创建一个实现ApplicationContextAware接口工厂类,重写setApplicationContext方法,在这个方法中利用Spring的Bean管理及容器机制,自动将声明的各种策略加载到strategyMap中,并对外提供获取接口getHandleStrategy(String methodType) ,对于调用方来说只需要传入任务方法类型即可获取响应的策略处理类,不会对客户端暴露创建逻辑。
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.alibaba.nacos.common.utils.Objects;
/**
*
* @ClassName: HttpMethodHandleStrategyFactory
* @Description: 策略模式工厂
* @Author: liulianglin
* @DateTime 2022年4月25日 下午7:56:30
*/
@Component
public class HttpMethodHandleStrategyFactory implements ApplicationContextAware{
Logger log = LoggerFactory.getLogger(HttpMethodHandleStrategyFactory.class);
protected Map strategyMap = new HashMap();
/**
* 加载所有的策略
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map sourceStrategyMap = applicationContext.getBeansOfType(HttpMethodHandleStrategy.class);
sourceStrategyMap.forEach((key, value) -> strategyMap.put(value.getType(), value));
log.info("[ACS任务管理器]-加载Http请求类型处理策略完成,支持的类型:{}", strategyMap.keySet());
}
/**
*
* @Description: 获取处理策略
* @Author: liuliagnlin
* @Datetime: 2022年4月25日 下午4:53:14
* @param methodType Http方法请求类型
* @return HttpMethodHandleStrategy
*/
HttpMethodHandleStrategy getHandleStrategy(String methodType) {
if (strategyMap.isEmpty()) {
throw new RuntimeException("Http请求类型处理策略未成功加载");
}
HttpMethodHandleStrategy handleStrategy = strategyMap.get(methodType);
if (Objects.isNull(handleStrategy)) {
throw new RuntimeException("未找到对应的Http请求类型处理策略,方法类型["+methodType+"]");
}
return handleStrategy;
}
}
(三)总结
代码优化,我们会发现代码的扩展性得到增强,当业务需要新增处理类型时,只需要实现一个新的策略类并继承BaseStrategy,其他原有的业务代码并不需要进行更改,从而做到面向扩展开放的原则。同时,代码通过合理的复用也消除了部分重复冗余的代码。
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)