前言:软件设计开发需要关注需求的长期变化,SOLID(S-单一职责原则,O-开放封闭原则,L-里氏替换原则,I-接口隔离原则,D-依赖倒置原则)软件设计原则中,开闭原则是软件设计的重要方向。开闭原则即对扩展开放,对修改关闭,最简单的理解为:新增需求时,尽量通过新增代码(主要是新增类)而不是通过修改已有代码来实现,因为修改现有代码会引入新的不稳定因素,原本的单元测试也需要重新开展。
设计模式是在特定场景下遵循设计原则的解决方案,学习并参考设计模式可以帮助新手了解如何写出易扩展、易维护的代码。
本案例是在学习部分设计模式后,将工厂和模板模式使用到实际开发场景的实践,提升了系统的可扩展性。
一、简单工厂模式
工厂模式一般分为三种细分的类型:简单工厂、工厂方法和抽象方法。本案例由于创建逻辑简单(只是简单的new),无需组合其他类对象并做各种初始化操作,因此无需将对象的创建逻辑拆分到不同的类中,使用简单工厂即可满足需求。
工厂模式将对象的创建交给专门的工厂完成,目的在于将对象的创建和使用解耦,使类的职责更加单一,方便修改和扩展。
1.1 不使用简单工厂例子
现有这样一个场景:根据配置文件的后缀——json、xml、yaml、properties,选择不同的解析器——JsonRuleConfigParser、XmlRuleConfigParser等,将存储在文件中的配置解析成内存对象RuleConfig。
不使用简单工厂时,代码实现如下:
public class RuleCofigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
// 由当前类完成对象的创建
IRuleConfigParser parser = createParser(ruleConfigFileExtension);
if (parser == null) {
throw new InvalidRuleConfigException(
"Rule config file format is not supported: " + ruleConfigFilePath);
}
String configText = "";
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如rule.json,返回json
return "json";
}
private IRuleConfigParser createParser(String configFormat) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(configFormat)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(configFormat)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(configFormat)) {
parser = new YamlRuleConfigParser();
} else if ("properties".equalsIgnoreCase(configFormat)) {
parser = new PropertiesRuleConfigParser();
}
return parser;
}
}
1.2 使用简单工厂
上述代码将IRuleConfigParser类型对象的创建封装在createParser()函数中,代码逻辑相对清晰,但为了让类的职责更加单一、代码更加清晰,还可以进一步将createParser()函数剥离到一个独立的类中,让这个类只负责对象的创建,这个类就是简单工厂。具体代码如下:
-
简单工厂的第一种实现方式
public class RuleConfigSource { public RuleConfig load(String ruleConfigFilePath) { String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath); IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension); if (parser == null) { throw new InvalidRuleConfigException( "Rule config file format is not supported: " + ruleConfigFilePath); } String configText = ""; RuleConfig ruleConfig = parser.parse(configText); return ruleConfig; } private String getFileExtension(String filePath) { //...解析文件名获取扩展名,比如rule.json,返回json return "json"; } } public class RuleConfigParserFactory { public static IRuleConfigParser createParser(String configFormat) { IRuleConfigParser parser = null; if ("json".equalsIgnoreCase(configFormat)) { parser = new JsonRuleConfigParser(); } else if ("xml".equalsIgnoreCase(configFormat)) { parser = new XmlRuleConfigParser(); } else if ("yaml".equalsIgnoreCase(configFormat)) { parser = new YamlRuleConfigParser(); } else if ("properties".equalsIgnoreCase(configFormat)) { parser = new PropertiesRuleConfigParser(); } return parser; } } -
简单工厂的第二种实现方式
在上面的代码实现中,每次调用RuleConfigParserFactory的createParser()方法时,都会创建一个新的IRuleConfigParser类型的对象,如果这个对象是可以复用的,为了节省内存和对象创建的时间,可以将其事先创建好缓存起来。当调用createParser()方法的时候,我们可以从缓存中取出对象直接使用。
public class RuleConfigParserFactory { private static final MapcachedParsers = new HashMap<>(); static { cachedParsers.put("json", new JsonRuleConfigParser()); cachedParsers.put("xml", new XmlRuleConfigParser()); cachedParsers.put("yaml", new YamlRuleConfigParser()); cachedParsers.put("properties", new PropertiesRuleConfigParser()); } public static IRuleConfigParser createParser(String configFormat) { if (configFormat == null || configFormat.isEmpty()) { return null; } IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase()); return parser; } }
如果现在需要为一个新的后缀格式添加一个新的解析器,在不使用简单工厂时,所有获取IRuleConfigParser解析器的类中,都要做相应的修改,关键这些创建的代码散布在项目各处,修改既费劲又很难保证完全统一。
使用简单工厂后,修改RuleConfigParserFactory类即可,调用处无需做任何修改,虽然没有做到完全符合开闭原则,但是修改集中,方便维护和扩展。
简单工厂和其他两种工厂的作用均可归结如下:
1.4 适用场景
**封装变化:**创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
**代码复用:**创建代码抽离到独立的工厂类之后可以复用。
**隔离复杂性:**封装复杂的创建逻辑,调用者无需了解如何创建对象。
**控制复杂度:**将创建代码抽离出来,让原本的函数或类职责更单一,代码更简洁。
代码中存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。这种情况下,就可以考虑使用简单工厂模式,将一大推 if-else 创建对象的代码抽离出来,放到简单工厂中。
二、模板模式模板方法模式在一个方法中定义一个算法骨架,并将某些步骤推迟到子类中实现。模板方法模式可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。
这里的“算法”,我们可以理解为广义上的“业务逻辑”,并不特指数据结构和算法中的“算法”。这里的算法骨架就是“模板”,包含算法骨架的方法就是“模板方法”,这也是模板方法模式名字的由来。
2.1 基本结构public abstract class AbstractClass {
// 模板方法,定义算法骨架
public final void templateMethod() { // 定义为 final,是为了避免子类重写它
//...
method1();
//...
method2();
//...
}
// 需要子类实现的方法
protected abstract void method1();
protected abstract void method2();
}
public class ConcreteClass1 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
}
public class ConcreteClass2 extends AbstractClass {
@Override
protected void method1() {
//...
}
@Override
protected void method2() {
//...
}
AbstractClass demo = ConcreteClass1();
demo.templateMethod();
在模板模式经典的实现中,模板方法定义为 final,可以避免被子类重写。需要子类重写的方法定义为 abstract,可以强迫子类去实现。不过,在实际项目开发中,模板模式的实现比较灵活,以上两点都不是必须的。
2.2 作用和适用场景-
作用
模板模式有两大作用:复用和扩展。其中,复用指的是,所有的子类可以复用父类中提供的模板方法的代码。扩展指的是,框架通过模板模式提供功能扩展点,让框架用户可以在不修改框架源码的情况下,基于扩展点定制化框架的功能。
-
适用场景
当多个类的算法除一些细微不同之外几乎完全一样时,可以使用该模式。将相似的实现步骤提取到超类中以去除重复代码,子类可针对特定步骤定制其不同的处理方式。
需求可以概述为,开发节点擦除器,针对流程图中的不同节点,例如第二列的并行节点,第三列的串行节点,做对应的节点擦除操作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nCyOwWMy-1657625958065)(…/images/应对需求扩展——工厂+模板实践总结/image-20220712153354398.png)]
3.2 设计-
模板模式
节点擦除操作可以拆解为一系列固定步骤的组合:
1、判断当前节点是否需要进行擦除
2、获取节点擦除器
3、进行擦除操作
其中,前两个步骤所有节点的操作是完全相同的,区别在于具体的擦除操作,如:并行节点的擦除逻辑为擦除当前节点和节点前后的连接线即可,串行节点则暂不支持擦除操作,其擦除方法中做的是将节点信息打印到日志中。
因此,该应用场景与模板模式契合,我们可以定义一个抽象类Wiper,在Wiper中定义擦除节点的模板方法,模板方法定义操作步骤骨架:判断是否需要擦除->获取节点擦除器,以上两个方法作为私有方法写在Wiper中,供子类复用,最后定义一个擦除操作的抽象方法,将擦除操作延迟到子类去实现,实现差异化的擦除操作。
-
简单工厂
Wiper中需要根据参数获取节点擦除器,为了使Wiper的职责更加单一,代码结构更加清晰简洁,将节点擦除器的创建封装到简单工厂WiperFactory中,这样以后添加新的节点擦除器时修改也能更集中,提供代码的易扩展性和易维护性。
-
模板抽象类-Wiper
public abstract class Wiper { protected static final String ID = "id"; protected static final String EDGE_SOURCENODEID = "sourceNodeId"; protected static final String EDGE_TARGETNODEID = "targetNodeId"; private static final String AUTOSELECTION = "autoSelection"; private static final String ISSKIPNODE = "isSkipNode"; public static FontViewDataDto wipe(WiperParam wiperParam) { Boolean needWipe = isNeedWipe(wiperParam); if (!needWipe) { return wiperParam.getFontViewDataDto(); } Wiper wiper = getWiper(wiperParam); FontViewDataDto ret = wiper.reallyWipe(wiperParam); return ret; } /抽象方法/ protected abstract FontViewDataDto reallyWipe(WiperParam wiperParam); /私有方法/ private static Boolean isNeedWipe(WiperParam wiperParam) { PartFlowDto partFlowDto = wiperParam.getPartFlowDto(); Object autoSelection = partFlowDto.getAutoSelection(); MappartData = partFlowDto.getPartData(); Object isSkipNode = partData.get(ISSKIPNODE); Boolean b1 = autoSelection == null ? false : (Boolean) autoSelection; Boolean b2 = isSkipNode == null ? false : (Boolean) isSkipNode; return (b1 && b2); } private static Wiper getWiper(WiperParam wiperParam) { Wiper wiper = WiperFactory.getInstance(wiperParam); return wiper; } } -
实现子类-SerialNodeWiper
@Slf4j public class SerialNodeWiper extends Wiper { @Override protected FontViewDataDto reallyWipe(WiperParam wiperParam) { // 组织日志打印信息 String nodeId = wiperParam.getPartFlowDto().getNodeId(); List -
实现子类-ParallelNodeWiper
@Slf4j public class ParallelNodeWiper extends Wiper { @Override protected FontViewDataDto reallyWipe(WiperParam wiperParam) { String nodeId = wiperParam.getPartFlowDto().getNodeId(); FontViewDataDto fontViewDataDto = wiperParam.getFontViewDataDto(); wipeNodeAndEdges(nodeId, fontViewDataDto); return fontViewDataDto; } private void wipeNodeAndEdges(String nodeId, FontViewDataDto fontViewDataDto) { List -
简单工厂-WiperFactory
@Service public class WiperFactory { private static final Mapwipers = new HashMap<>(2); private static CustomerProcessor customerProcessor; static { wipers.put(WiperType.PARALLELNODE_WIPER, new ParallelNodeWiper()); wipers.put(WiperType.SERIALNODE_WIPER, new SerialNodeWiper()); } @Autowired public WiperFactory(CustomerProcessor customerProcessor) { WiperFactory.customerProcessor = customerProcessor; } public static Wiper getInstance(WiperParam wiperParam) { PartFlowDto partFlowDto = wiperParam.getPartFlowDto(); String flowModuleId = partFlowDto.getFlowModuleId(); String nodeId = partFlowDto.getNodeId(); Boolean parallelNode = customerProcessor.isParallelNode(flowModuleId, nodeId); if (parallelNode) { return wipers.get(WiperType.PARALLELNODE_WIPER); } else { return wipers.get(WiperType.SERIALNODE_WIPER); } } }
在面对以后节点擦除功能的需求扩展时,只需要新增一个具体的节点擦除器,继承Wiper并覆写其抽象方法,然后在WiperFactory的缓存map中添加新的节点擦除器,即可完成功能的扩展。不同节点之间的擦除逻辑互不影响,逻辑清晰,改动集中,有利于扩展和维护。
四、总结本案例简要介绍了简单工厂模式和模板模式的基本结构和适用场景,并以实际开发场景为例,阐述了如何运用简单工厂+模板模式提高代码的可扩展性和可维护性,具有一定借鉴意义。
参考文献王争——设计模式之美



