浅谈设计模式 - 模板方法(十) 前言: 模板方法模式在JAVA当中最为熟知的就是spring
的template
对象,模板方法和策略这两个模式需要小心的区分,关于模板方法模式只需要重点记忆一句话:模板方法的模式定义了算法的骨架 。同时针对模板方法的的一项设计原则好莱坞原则 也是对 依赖倒转 原则一种很好的补充和扩展。
文章目的:
了解模板方法,同时了解模板方法是如何体现好莱坞原则的。
模板方法与策略模式的对比,以及模板方法灵活运用钩子函数。
模板方法的简单案例,以及在spring框架当中的具体体现。
什么是模板方法 基本定义 定义:在一个方法当中定义了算法的骨架,而将具体的实现延迟到子类当中。模板方法在不改变算法结构的情况下,重构算法的某些步骤。
从现实看模板方法 我们都知道现代各式各样的蛋糕都是使用模具做成的,而同一个形状的蛋糕却可以使用不同的配料,此时模具便是模板方法的骨架,通过定义具体的配料细节对应了“算法”的细节。
钩子函数 钩子函数是一种编程上比较常用的技巧,在框架设计当中十分常见,什么是钩子呢?从个人的理解来看,钩子像是可以延迟定义的匿名函数,钩子可以“勾”住某个算法的中间过程,让外部环境可以干涉内部算法实现的同时,又能让内部的函数进行自由控制钩子的使用。
钩子函数一般实现方式为抽象类或者不做任何动作的函数。
钩子函数在脚本语言里面经常被用作回调函数。包括java的许多框架也用钩子让用户可以干涉一些算法的细节。但是需要注意的是,钩子这个东西很容易破坏代码的可阅读性,所以不建议经常使用这种函数,可以用组合以及其他的设计模式对于结构进行优化。
模板方法的结构图 下面是模板方法的结构图,模板方法对比其他设计模式应该算是最简单的一个结构图了,比较容易理解:
模板方法的模式定义了算法的骨架,那么什么是定义算法的骨架,从下面的图表很好的看到,父类定义为抽象类,定义模板的算法方法和抽象的算法细节。
这里要实现算法需要由子类实现具体的算法业务
模板方法的优缺点 优点:
模板方法可以让算法的细节掩盖在子类,同时抽取公共的算法,提高代码复用程度
模板方法可以让修改控制在子类,而父类方法不需要进行改动,符合开放关闭原则。
缺点:
模板方法类的改动对于所有的算法实现子类都会产生影响,同时模板父类改动违背“开放-关闭”原则
模板方法由于利用钩子控制父类方法,会导致反向控制代码,对于代码的阅读不是十分友好。
模板方法与好莱坞原则 什么是好莱坞原则?
首先需要了解一下什么是好莱坞原则:**让我们调用你们,而不是让你们调用我**。
和依赖倒转原则有什么关联?
好莱坞原则更像是对于依赖倒转的一种扩展技巧。依赖倒转更加关注的是如何在设计中避免面向实现编程,而好莱坞则是将实现的调用在低层的结构进行隐藏。
为什么不建议低层组件调用高层组件?
为了防止环形依赖,在高层组件里面调用了抽象方法,而抽象方法又调用高层组件的方法。
策略模式和模板方法对比 策略模式和模板方法模式的对比
策略是定义一整个算法,使用组合的形式实现不同的对象切换
模板方法的是定义一个超类,在超类中通过高层调用底层实现的具体方法的实现,来实现方法的延迟功能
案例 这次的案例以个人小时候做过的一件事情举例,以前外婆兼职从厂里拿来一堆玩具零件的成品,而工作就是把成品进行“反转”(就是把做好的玩具翻面),还非常清楚的记得大概是一分钱一个,靠着帮忙那时候还拿了一些零花钱,每天放学做完作业之后就是帮外婆做“兼职”。这种重复性劳动,在代码的构建很容易想到模板方法的模式,由于各种玩具的形状不同,所以翻面的方式以及效率和速度都不同,我们将重复劳动的部分定义为顶层的模板,而具体的玩具构建细节,需要根据不同的玩具进行不同的操作,下面定义这个工作的大致流程:
下面是根据结构图绘制一个基本的代码:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 public abstract class TemplateWorkFlow { public void productToy () { takeToy(); reverseToy(); putBasket(); } public final void putBasket () { System.out.println("把玩具放到玩具篮" ); } public void takeToy () { System.out.println("拿起玩具" ); } public abstract void reverseToy () ; } public class AntlersToyWorkFlow extends TemplateWorkFlow { @Override public void reverseToy () { System.out.println("把主干翻面" ); System.out.println("把鹿角的分叉翻页" ); } } public class ChristmasHatWorkFlow extends TemplateWorkFlow { @Override public void reverseToy () { System.out.println("圣诞帽反转" ); System.out.println("圣诞帽帽子顶部的小秋顶出去" ); } } public class Main { public static void main (String[] args) { TemplateWorkFlow templateWorkFlow = new ChristmasHatWorkFlow(); TemplateWorkFlow templateWorkFlow1 = new AntlersToyWorkFlow(); templateWorkFlow.productToy(); templateWorkFlow1.productToy(); } }
如果不使用设计模式,他大致的设计代码如下,可以看到很多方法都干了相似的事情,这些方法可能本质上只是一两行代码甚至只是取名不一样,当然现代的编译器都很“聪明”,会发现重复的点,所以最最基本的要求,是编写出编译器都无法发现的重复代码,当然仅仅凭借这一点显然要求有点低
下面看下不使用模板方法的代码:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class ChristmasHatWorkFlow { public void productToy () { takeToy(); reverseToy(); putBasket(); } public final void putBasket () { System.out.println("把玩具放到玩具篮" ); } public void takeToy () { System.out.println("拿起玩具" ); } public void reverseToy () { System.out.println("圣诞帽反转" ); System.out.println("圣诞帽帽子顶部的小秋顶出去" ); } } public class AntlersToyWorkFlow { public void productToy () { takeToy(); reverseToy(); putBasket(); } public final void putBasket () { System.out.println("把玩具放到玩具篮" ); } public void takeToy () { System.out.println("拿起玩具" ); } public void reverseToy () { System.out.println("把主干翻面" ); System.out.println("把鹿角的分叉翻页" ); } } public class Main { public static void main (String[] args) { AntlersToyWorkFlow antlersToyWorkFlow = new AntlersToyWorkFlow(); antlersToyWorkFlow.productToy(); ChristmasHatWorkFlow christmasHatWorkFlow = new ChristmasHatWorkFlow(); christmasHatWorkFlow.productToy(); } }
spring当中的模板方法 spring最为典型的案例便是Tempalte
框架,但是需要注意spring多数情况下并没有使用经典的模板方法结构,而是使用了CallBack函数
的形式,避开了继承结构的同时,每个类可以单独实现自己的具体功能:
我们看一下RedisTempalte
当中的StringRedisTemplate
,这里调用父类的afterPropertiesSet()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class StringRedisTemplate extends RedisTemplate <String , String > { public StringRedisTemplate (RedisConnectionFactory connectionFactory) { this (); setConnectionFactory(connectionFactory); afterPropertiesSet(); } protected RedisConnection preProcessConnection (RedisConnection connection, boolean existingConnection) { return new DefaultStringRedisConnection(connection); } }
父类同样继承的手段,在如下方法当中调用了afterProperteisSet()
,通过super引用父类的方法:
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 32 33 34 35 36 37 38 39 40 41 42 43 @Override public void afterPropertiesSet () { super .afterPropertiesSet(); boolean defaultUsed = false ; if (defaultSerializer == null ) { defaultSerializer = new JdkSerializationRedisSerializer( classLoader != null ? classLoader : this .getClass().getClassLoader()); } if (enableDefaultSerializer) { if (keySerializer == null ) { keySerializer = defaultSerializer; defaultUsed = true ; } if (valueSerializer == null ) { valueSerializer = defaultSerializer; defaultUsed = true ; } if (hashKeySerializer == null ) { hashKeySerializer = defaultSerializer; defaultUsed = true ; } if (hashValueSerializer == null ) { hashValueSerializer = defaultSerializer; defaultUsed = true ; } } if (enableDefaultSerializer && defaultUsed) { Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized" ); } if (scriptExecutor == null ) { this .scriptExecutor = new DefaultScriptExecutor<>(this ); } initialized = true ; }
下面引用父类RedisAccesor
类当中的afterPropertiesSet()
1 2 3 public void afterPropertiesSet () { Assert.state(getConnectionFactory() != null , "RedisConnectionFactory is required" ); }
通过定义getConnectionFactory()
方法,子类可以自由的配置连接工厂,也可以直接沿用父类的默认实现。
总结 模板方法是一个比较重要的设计模式,他可以从结构上帮助程序员构建一个良好的抽象概念,同时模板方法提供的钩子函数,通过定义抽象方法延迟到子类实现这一技巧非常符合“开放-关闭”原则,灵活运用模板方法模式有利于构建更加灵活的软件骨架,同时可以定义各种多变的算法体系。但是需要注意的是传统的模板方法这种继承的结构 并不推崇 ,因为我们都知道继承对于所有子类都会产生影响。
另外模板方法这个模式对于阅读代码的体验不是很好,经常需要各个类之间不断切换,有时候甚至会莫名其妙为什么突然跑到另一个方法里面,模板方法有时候比较影响阅读体验。
另外设计模式最大的目的就是 减少重复代码 以及 用最小的代价进行扩展 ,个人认为如果代码符合这两个点基本就是一个好代码,然而这个点确实日常工作最难实现的。