分享

常用设计模式-模板模式(一)

 景昕的花园 2024-05-13 发布于北京

前言

我常常觉得人们低估了设计模式的作用和意义。它们不仅是简历上的金边、程序员的黑话,也不仅是常见业务的常用处理方式或经验总结。

设计模式不仅是这些,它们更是面向对象思想理论结合实践的切入点。我们前面聊过抽象、高内聚低耦合、封装继承多态、SOLID设计原则。它们更偏理论指导,离编码实践还有一段距离。而这里要聊的设计模式,不仅有扎实的理论基础,而且实实在在地俯下身子、扎根到了实践当中。

从编码实践的角度来讲设计模式,这类文章没有一万也有八千。细抠几种设计模式之间的区别,这类文章写再多也没太大意义。这里就不凑这些热闹了。

这里,我会简单聊聊几个主要设计模式的编程应用,然后把主要精力放在它们与面向对象思想的关联上。另外,在聊过几种设计模式之后,计划提供一种复合模式,作为我使用设计模式的“最佳实践”,供读者参考。


模板模式

如果说“用到了接口-实现类,就用到了策略模式”,那么策略模式可能是我们无意之间用得最多的设计模式。如果排除“无意中”的使用,只考虑有意识、有目的情况,我想,模板模式应该算得上“使用最多的设计模式”,应该也是大多数人第一个运用到实践中的设计模式。

有意和无意,有很大差别。

我想,其中的主要原因在于我们大多数人开发的都是业务系统、业务流程。业务系统和流程有一个显著特点:在一套基础业务流程上,改一改这一步、调一调那一步,就得到了所谓新业务、新流程、新需求。模板模式与这一特点一拍即合,自然就很容易进入到开发实践中。


是什么

关于模板模式的定义,我在网上查到的了很多这样描述的:

模板模式在一个抽象类中定义了一个算法的骨架,而将一些步骤延迟到子类中。

这个定义里,其它部分都好说,就是“延迟”二字让人费解。延迟一般是指“比xxx更慢/更晚”。在模板模式里,是谁比谁更慢或更晚呢?

看了一圈,我比较喜欢这个定义:

The Template Method design pattern is a behavioral design pattern that defines the skeleton of an algorithm in a superclass but allows subclasses to override specific steps of the algorithm without changing its structure.  
模板方法模式是一种行为设计模式。它在父类中好定义算法骨架,并允许子类在不修改算法结构的情况下,重写其中的特定的步骤。

从这个定义中,我们可以找出模板模式的四个关键要素:父类、子类、算法骨架、被重写的步骤,如下图所示:

父类之于子类有很多种意义。在模板模式中,父类的主要意义在于定义算法骨架。

所谓“算法骨架”,首先是操作流程,其次则是“可重写的步骤”。有了“可重写的步骤”,子类才能够“重写”这些步骤。但也因为操作流程在父类中定义好了,因而子类只能重写个别步骤的具体实现,而不能修改算法的完整流程、步骤顺序等。

网上提到模板模式,大多强调父类一定是抽象类。其实未必。尽管不太符合里氏替换原则,模板模式中的父类也可以是普通类。这是后话。

所谓“可重写步骤”,通常即封装有部分操作流程的方法。有些时候,它们也叫“钩子方法”。子类通过重写这些方法,来改写操作流程中的部分细节。这些方法的可见性、入参、返回值,以及“颗粒度”等,都是值得慎重考虑的方面。

在这些方面中,可见性也许是其中最简单的一个。为了兼容跨包的子类,父类的可见性一般都是public。但方法的可见性则不宜过大。有些代码里一股脑地将它们也定义为public,也有些开发者把这些方法放到了接口上,实在没有必要。

这些方法只服务于两个目标:组成完整的操作流程,以及供子类重写。因此,保证这些方法能被子类访问到即可,其它调用者一律禁止入内。

一个方法的可扩展性,受方法入参影响很大。我见过不少子类A要用参数ABC、而子类B要用参数CDE的。如果方法入参定义不当,从子类一扩展到子类二时,就免不了一番伤筋动骨。


怎么做

明白了模板模式的四个要素,实现起来就很简单了:
// 父类,定义算法的骨架public class ParentClass {    // 定义算法的步骤    public final Result templateMethod(Param p) {        Temp1 temp1 = step1(p);        Temp2 temp2 = step2(p, temp1);        Result result = step3(temp1, temp2);        return result;    }
// 定义算法的第一步 protected Temp1 step1(Param p){ // 默认实现,略 }
// 定义算法的第二步 protected Temp2 step2(Param p, Temp1 temp1){ // 默认实现,略 }
// 定义算法的第三步 protected Result step3(Temp1 temp1,Temp2 temp2){ // 默认实现,略 }}
// 具体子类,实现算法的步骤class ConcreteClassA extends ParentClass { @Override protected Temp1 step1(Param p){ // 修改实现步骤1,略 }
@Override protected Result step3(Temp1 temp1,Temp2 temp2){ // 修改实现步骤3,略 }}
// 具体子类,实现算法的步骤class ConcreteClassB extends ParentClass {
@Override protected Temp2 step2(Param p, Temp1 temp1){ // 修改实现步骤2,略 }}
// 使用模板模式public class Client { public static void main(String[] args) { ParentClass template = new ConcreteClassA(); template.templateMethod();
template = new ConcreteClassB(); template.templateMethod(); }}

上面的例子看起来很简单,其实有隐藏的“坑”:“方法步骤”要怎样拆分比较合适?入参和返回值要怎样设计比较合理?

一般来说,拆分方法步骤有两种思路。第一种是在还没有子类的时候,就在父类中设计好方法步骤,并预留下供子类重写的方法。第二种则是没有子类时,父类不做扩展性考虑;在必要时,再根据子类的扩展需求,在父类中拆分方法步骤、设计可重写方法。

这两种方法,和“自上而下”和“自下而上”的方法论颇有些异曲同工。它们的优缺点也可以从类似的角度来分析。

第一种方法,“自上而下”地设计好一个模板类,为子类预留下扩展空间。如果父类的流程有明确的步骤,或者子类的扩展有清晰的方向,那么借助这种自上而下的方法,我们可以设计出一套卓有成效的父子类来。

然而,如果没有这两个前提——“父类的流程有明确的步骤”,或者“子类的扩展有清晰的方向”——自上而下的方法就很容易变成过度设计,最终得到南辕北辙的效果。

第二种方法则是“自下而上”地演化出一套模板和父子类。它很好地弥补了第一种方法的缺陷,但也有明显的问题。

“按需演化”很容易演变成“按需特化”,得到的模板不伦不类,既复杂难懂、又难以扩展。

自上而下和自下而上,有很大差别。

我个人更倾向于从上而下的方法论。条件允许时,应该优先考虑自上而下地设计,用优秀的“顶层设计”来指导、推动底层实现。如果从自下而上入手,也应注意审时度势,在必要的时候梳理顶层设计,优化模板类结构。

无论自上而下还是自下而上,都是一件费时费心的工作。我们为什么要花费时间精力来设计和使用模板模式呢?模板模式有怎样的优点,又有哪些缺点呢?




为什么

模板模式可以提高代码复用率和可扩展性。这大概是它最显而易见的两个优点,这里就不啰嗦了。



模板模式还有一个更高层次的优点:它是一种入门级的建模方式。

模型思维和普通思维,有很大差别。



个人理解,建模需要抓住事物的核心本质,并简练而明确地描述其组成元素和各元素的组织结构、运转规律。

设计模板模式正是这样一个过程。我们先要分析业务流程,然后把流程按一定顺序分解成若干步骤,识别各步骤的入参和返回值,还要把这些步骤区分为“所有子类通用的”和“允许子类修改的”。完成这些工作之后,我们才能确定父类如何实现、子类如何重写。

在这个过程中,我们得到的步骤、入参和返回值,其实就是这个流程模型中的“元素”;各步骤的执行顺序、入参和返回值的转换和传递,就是这个模型的运转规律;步骤的分类则可以理解为这个模型的组织结构的基础:我们需要借助“允许子类修改的步骤”,来组织父子类结构。

你看,一个流程模型就这样拔地而起了。

当然,建模没有这么简单, 建好模更不容易。不过,用模型思维来分析和构建系统,于系统、于自己都百利一弊——唯一的弊端在于成本偏高。

模板模式作为一种入门级的建模方式,就像是一扇通往模型思维的大门。当我们有意识、有目的地使用模板模式时,其实就已经站到了这扇门前。推开它、走进去,我们会发现更广阔的天地。

推开大门和掉头走开,有很大差别。


顺带,关于模型思维的“百利”,这里简单列举几个。

模型的7大用途(REDCAPE)

推理:识别条件并推断逻辑含义。

解释:为经验现象提供(可检验的)解释。

设计:选择制度、政策和规则的特征。

沟通:将知识与理解联系起来。

行动:指导政策选择和战略行动。

预测:对未来和未知现象进行数值和分类预测。

探索:分析探索可能性和假说。

《模型思维》

总的来说,模板模式在需要重用业务流程、同时允许子类重写某些步骤的场景中非常有用。

然而,在选择使用模板模式时,我们需要权衡其带来的代码复用和行为一致性的优点,以及可能增加的类数量和复杂性的缺点,审慎使用。



往期索引

《面向对象是什么》

从具体的语言和实现中抽离出来,面向对象思想究竟是什么?
公众号:景昕的花园面向对象是什么

抽象

抽象这个东西,说起来很抽象,其实很简单。

花园的景昕,公众号:景昕的花园抽象

高内聚与低耦合

细说几种内聚

细说几种耦合

"高内聚"与"低耦合"是软件设计和开发中经常出现的一对概念。它们既是做好设计的途径,也是评价设计好坏的标准。

花园的景昕,公众号:景昕的花园高内聚与低耦合

封装

继承

多态》

——“面向对象的三大特性是什么?”
——“封装、继承、多态。”

《[5+1]单一职责原则》
单一职责原则非常好理解:一个类应当只承担一种职责。因为只承担一种职责,所以,一个类应该只有一个发生变化的原因。
花园的景昕,公众号:景昕的花园[5+1]单一职责原则


《[5+1]开闭原则(一)

《[5+1]开闭原则(二)

什么是扩展?就Java而言,实现接口(implements SomeInterface)、继承父类(extends SuperClass),甚至重载方法(Overload),都可以称作是“扩展”。
什么是修改?在Java中,严格来说,凡是会导致一个类重新编译、生成不同的class文件的操作,都是对这个类做的修改。实践中我们会放宽一点,只有改变了业务逻辑的修改,才会归入开闭原则所说的“修改”之中。
花园的景昕,公众号:景昕的花园[5+1]开闭原则(一)

《[5+1]里氏替换原则(一)

《[5+1]里氏替换原则(二)

里氏替换原则(Liskov Substitution principle)是一条针对对象继承关系提出的设计原则。它以芭芭拉·利斯科夫(Barbara Liskov)的名字命名。1987年,芭芭拉在一次名为“数据的抽象与层次”的演讲中首次提出这条原则;1994年,芭芭拉与另一位女性计算机科学家周以真(Jeannette Marie Wing)合作发表论文,正式提出了这条面向对象设计原则

花园的景昕,公众号:景昕的花园[5+1]里氏替换原则(一)

[5+1]接口隔离原则(一)

[5+1]接口隔离原则(二)

一般我们会说,接口隔离原则是指:把庞大而臃肿的接口拆分成更小、更具体的接口。
不过,这并不是接口隔离原则的定义。实际上,接口隔离原则的定义其实是这样的……客户端不应被迫依赖它们压根用不上的接口;或者反过来说,客户端应该只依赖它们要用的接口。

花园的景昕,公众号:景昕的花园[5+1]接口隔离原则(一)

[5+1]依赖倒置原则(一)

[5+1]依赖倒置原则(二)

在Java世界里谈到依赖倒置原则,相信90%的人都会立即想起SpringIOC;还有9%的人会想起“面向接口编程”。最多只有1%的人能想起依赖倒置原则的真正定义
花园的景昕,公众号:景昕的花园[5+1]依赖倒置原则(一)

[5+1]迪米特法则(一)
[5+1]迪米特法则(二)

迪米特法则可以用一句话概括:Only talk to your friends。

 “只和你的朋友说话”,这是1987年的表述。2003/2004年左右,Karl Liebertherr对迪米特法则做了一次升级:由“Only talk to your friends”升级为了“Only talk to your friends who share your concerns”——“只和与你同忧同乐的朋友说话”。

花园的景昕,公众号:景昕的花园[5+1]迪米特法则

《常用设计模式-策略模式》

策略模式把属于同一类别的不同行为封装为某种“策略抽象”,而把这些行为统一为这个抽象下的某个“策略实现”。这样,我们就可以很灵活地决定在哪种场景下使用哪种“策略”了。

花园的景昕,公众号:景昕的花园常用设计模式-策略模式

《常用设计模式-工厂模式(一)》

《常用设计模式-工厂模式(二)》

工厂模式的定义其实很简单:提供一个独立组件,用以根据不同条件选择并构建不同实例。这个组件就是“工厂”。

花园的景昕,公众号:景昕的花园常用设计模式——工厂模式(一)


《常用设计模式-注册器模式(一)》

《常用设计模式-注册器模式(二)》

注册器模式提供了一个注册器,一组相同类型的实例可以被注册到注册器上并由后者进行保存。调用方则可以通过注册器来取用这些实例。
花园的景昕,公众号:景昕的花园常用设计模式-注册器模式(一)

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多