大家好,欢迎关注极客架构师,极客架构师,专注架构师成长,我是码农老吴。
上期我们介绍了狗厂百万级订单量的号卡业务,并开发出了没有使用设计模式的第一版代码,错过的朋友可以先看一下上期内容,ntent="mp" data-source="innerlink" href="https://www.toutiao.com/i7073240408452694559/?group_id=7073240408452694559" rel="noopener noreferrer noopener noreferrer" target="_blank">架构师基本功之设计模式-狗厂百万级订单量的号卡业务-第1期
基本思路本期我们针对第一版的代码,分析它存在的问题,并使用模板方法模式进行重构(上期你猜对了吗?没猜对没关系,后面还有几个设计模式,还会用到这个案例),然后我们先看看在《Design Patterns: Elements of Reusable Object-Oriented Software》这本设计模式开山之作中,四位前辈对模板方法模式的定义,接着采用我自己总结的REIS分析模型,对模板方法模式进行解读,最后我们给出模板方法模式的通用UML类图和代码。
第一版代码存在的问题从上面的截图,我们可以看出,第一版代码存在的主要问题如下:
两个订单类,北京移动(BjMobileOrderService)和山西联通(SxUnicomOrderService),
创建订单方法(createOrder()),这个方法的代码几乎完全相同,或者说完全相同。
锁定号码(lockMobile()),这个方法也是重复的,因为调用的都是公司自己的锁定号码服务,所以这个方法是不区分运营商的。
当项目中的大段代码发生重复时,就要引起我们的警惕了,意味着从技术角度,有可能需要进行重构。
模板方法模式-重构后的代码UML类图如上图所示,第二版代码新增了一个AbstractOrderService类,从名字上可以看出来,它是一个抽象类。北京移动(BjMobileOrderService)和山西联通(SxUnicomOrderService)这两个类,都继承了这个抽象类。
执行结果执行结果,两个版本的代码,完全相同。
代码OrderInfo实体类没有任何变化
package com.geekarchitect.patterns.demo002;import lombok.Data;@Datapublic class OrderInfo { public String memberID; public String mobileNumber; public String memberName; public String memberIdentityCard; public String operatorName; public String operatorID;}
IOrderService接口没有任何变化
package com.geekarchitect.patterns.demo002;public interface IOrderSerivce { boolean identityVerification(OrderInfo orderInfo); boolean fiveCardonePerson(OrderInfo orderInfo); void lockMobile(OrderInfo orderInfo); boolean sendOrder(OrderInfo orderInfo); boolean createOrder(OrderInfo orderInfo); }
新增AbstractOrderService抽象类,是我们讲解的重点:
1,执行IOrderService接口
2,把createOrder()方法,从原来的两个运营商的订单子类中,移动到了父类中,并且添加了final修饰符(为什么?)
3,把lockMobile()方法,从原来的两个运营商的订单类中,移动到了父类中,也添加了final修饰符。
4,实现了identityVerification()方法,里面的代码,调用的是国家工信部提供的实名认证服务(这个方法为什么没有添加final修饰符?)。
package com.geekarchitect.patterns.demo002;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public abstract class AbstractOrderService implements IOrderSerivce{ private static final Logger lOG = LoggerFactory.getLogger(AbstractOrderService.class); @Override public boolean identityVerification(OrderInfo orderInfo) { lOG.info("第一步:实名认证,调用国家工信部提供的认证服务"); return true; } @Override public final void lockMobile(OrderInfo orderInfo) { lOG.info("第三步:调用公司锁定号码服务"); } @Override public final boolean createOrder(OrderInfo orderInfo) { boolean result = identityVerification(orderInfo); if (!result) { lOG.info("实名认证失败"); return result; } result = fiveCardonePerson(orderInfo); if (!result) { lOG.info("一证五号验证失败"); return result; } lockMobile(orderInfo); if (!result) { lOG.info("号码锁定失败"); return result; } result = sendOrder(orderInfo); if (!result) { lOG.info("发送订单信息失败"); return result; } lOG.info("提交订单成功"); return result; }}
BjMobileOrderService和上一版比较,少了两个方法,createOrder()方法和lockMobile()方法,因为这两个方法,都被父类实现了。
package com.geekarchitect.patterns.demo002;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class BjMobileOrderService extends AbstractOrderService { private static final Logger lOG = LoggerFactory.getLogger(BjMobileOrderService.class); @Override public boolean identityVerification(OrderInfo orderInfo) { lOG.info("第一步:实名认证,调用{}Boss系统", orderInfo.getOperatorName()); return true; } @Override public boolean fiveCardonePerson(OrderInfo orderInfo) { lOG.info("第二步:一证五号验证,调用{}Boss系统", orderInfo.getOperatorName()); return true; } @Override public boolean sendOrder(OrderInfo orderInfo) { lOG.info("第四步:发送订单信息给{}Boss系统", orderInfo.getOperatorName()); return true; }}
SxUnicomOrderService类不仅少了createOrder()方法和lockMobile()方法,还少了一个identityVerification()方法,因为山西联通不提供自己的实名认证服务,所以只能使用工信部的了。
package com.geekarchitect.patterns.demo002;import org.slf4j.Logger;import org.slf4j.LoggerFactory;public class SxUnicomOrderService extends AbstractOrderService { private static final Logger lOG = LoggerFactory.getLogger(SxUnicomOrderService.class); @Override public boolean fiveCardonePerson(OrderInfo orderInfo) { lOG.info("第二步:一证五号验证,调用{}Boss系统", orderInfo.getOperatorName()); return true; } @Override public boolean sendOrder(OrderInfo orderInfo) { lOG.info("第四步:发送订单信息给{}Boss系统", orderInfo.getOperatorName()); return true; }}
TestOrderService 测试类
package com.geekarchitect.patterns.demo002;public class TestOrderService { public void bjMobileTest(){ IOrderSerivce orderSerivce=new BjMobileOrderService(); OrderInfo orderInfo=new OrderInfo(); orderInfo.setMemberID("1"); orderInfo.setMemberName("张三"); orderInfo.setMobileNumber("18702222387"); orderInfo.setMemberIdentityCard("142724000000003310"); orderInfo.setOperatorName("北京移动"); orderSerivce.createOrder(orderInfo); } public void sxUnicomTest(){ IOrderSerivce orderSerivce=new SxUnicomOrderService(); OrderInfo orderInfo=new OrderInfo(); orderInfo.setMemberID("2"); orderInfo.setMemberName("李四"); orderInfo.setMobileNumber("18702222000"); orderInfo.setMemberIdentityCard("142724000000003311"); orderInfo.setOperatorName("山西联通"); orderSerivce.createOrder(orderInfo); } public static void main(String[] args) { TestOrderService testOrderService=new TestOrderService(); testOrderService.bjMobileTest(); //testOrderService.sxUnicomTest(); }}
山西联通的测试结果大家可以看到,实名认证调用的是工信部的接口服务。
重构代码总结从上面的代码可以看出,我们只是新增了一个抽象父类AbstractOrderService,将两个子类中重复的方法createOrder()和lockMobile(),上移到了父类中。好像没什么吗,看不到有什么设计模式吗。
这就是我要给大家分享的,一个使用频率非常高,难度又非常低的设计模式,它就是模板方法模式。可是看起来好像不需要知道这个设计模式,就可以编出这个代码,是的,这个设计模式,如果你精通面向对象的编程思想,特别是对继承了解的比较透,就会自然而然的,不知不觉的使用这个模式,这个模式使用频率之高,高到大家可能都意识不到它的存在。但是当你熟悉了这个设计模式,你的代码将会更加专业,接下来,我们就看看模板方法模式的庐山真面目。
模板方法(Template Method Pattern)模式定义Define the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclassed redefine certain steps of an algorithm without changing the algorithm's structure.
—— Gof《Design Patterns: Elements of Reusable Object-Oriented Software》
中文解释如下:
定义一个 *** 作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构,即可重定义该算法的某些特定步骤。
这个定义里面我们要注意的关键词是算法的骨架、算法的某些特定步骤。
算法的骨架大家不要被这里的”算法“吓唬住,它和我们平时说的数据结构和算法里面的算法不是一个概念,不是什么高深的东西,广义上讲,类的方法里面的任何代码,你都可以理解为一种算法,这是广义上的算法。
算法的骨架,从业务角度,可以理解为一个业务需要一些固定的步骤才能完成,从代码角度可以理解为方法里面的代码,有固定的模式或者流程。
算法的骨架定义在什么地方,这个定义里面说的是”一个 *** 作中“,说得比较含糊,不精确,一般意义上来说,是定义在父类的方法里面,但是这里没有说死,是怕将来有别的可能性。定义算法骨架的方法,就是模板方法。
算法的某些特定步骤算法的骨架,决定了算法的大部分步骤是固定的,但是算法的某些特定步骤,需要扩展,需要个性化,这个定义里面,称之为重定义,被谁扩展,被谁个性化,被谁重定义呢,按照现在的面向对象的基本模式,一般是通过子类来完成,也可以通过其他方式。子类重定义的这些方法,我们可以称之为个性化方法(在原著里面,把这类方法,称为primitive operation 原语 *** 作,说起来不像中国话,所以我斗胆重新取了个名字,四位前辈不要生气,群殴我哦,为什么叫个性化方法,因为模板的对立面,好像就是个性化,大家有没有更好的名字,可以留言或者私信哦)。
概括起来讲,就是在父类定义模板方法,模板方法中包含了算法的骨架,并在其中调用个性化方法,个性化方法可以在父类或者父类的接口中定义,而个性化方法的实现由子类来完成,或者父类自己也可以实现一个基础的(这种方法又被称为hook operations钩子方法)方法,子类根据情况,可以直接使用,也可以直接覆盖,自己实现钩子方法。
号卡案例中的父类AbstractOrderService里面的identityVerification()就是一个钩子方法。
REIS模型分析模板方法模式REIS模型是我总结的分析设计模式的一种方法论,主要包括场景(scene),角色(role),交互(interaction),效果(effect)四个要素,后面我会专门分享一下这套方法论。
场景(Scene)场景,也就是我们在什么情况下,遇到了什么问题,需要使用某个设计模式。
对于模板方法模式,它的场景可以从业务和技术两个角度看。
从业务角度看,当业务中出现步骤,流程等字眼时,且这些流程有一定的稳定性时,可能需要使用模板方法模式。
从技术角度看,在面向对象开发中,当父类中的某个方法,被不同的子类实现时,大部分代码发生重复,则可能需要使用模板方法模式。
我们的号卡办理案例,不论从业务角度,还是从第一版的代码角度看,都符合这种场景。
角色(Role)角色,一般为设计模式出现的类,或者对象。每种角色有自己的职责。
在模板方法模式中,包含两种角色,模板角色,个性化角色。
模板角色:负责定义并实现模板方法,定义或者实现个性化方法或者钩子方法。
个性化角色:继承模板角色,并实现模板角色中定义的个性化方法或者钩子方法。
号卡案例中的父类AbstractOrderService,在这里就是模板角色,而两个子类,BjMobileOrderService,SxUnicomOrderService 则是个性化角色。
交互(interaction)交互,是指设计模式中,各种角色是如何交互的,一般用UML中的序列图,活动图来表示。简单的说就是角色之间是如何配合,完成设计模式的使命的。对于23种基本设计模式,因为他们是元设计模式,所以交互都比较简单,后面我们要讲的业务型设计模式,如多线程相关设计模式,异步消息相关设计模式等,交互就比较复杂,MVC,ORM这些都属于业务型设计模式。
模板方法模式中,模板角色和个性化角色之间的交互比较简单,就是个性化角色继承了模板角色。
效果(effect)效果,使用该设计模式之后,达到了什么效果,有何意义,当然,也可以说说它的缺点,或者风险。
从我们前面的案例可以看出,模板方法模式达到了以下效果。
1,消除了重复代码:本来在各个子类中,需要重复出现的代码,被提取到了父类角色中。
2,明确了职责,算法的骨架由模板角色的模板方法来实现,算法可扩展部分,由个性化角色实现。
3,通过新增个性化角色,实现算法的可扩展性。
最佳实践模板方法,建议定义了final,否则可能会被子类角色重写,也就是意味着算法的骨架,可能会被子类重写,这违背了设计的初衷。
个性化方法,一般定义在模板角色的接口中,如果在模板角色中定义,可以定义为钩子方法。这样个性化角色,就可以根据情况,选择是否需要重定义钩子方法了。
下面我们看看模板方法模式的通用UML类图和代码。大家注意,我讲的和普通设计模式书籍上的,是不太一样的,看仔细了。
UML类图通用代码IRoleService,模板角色执行的接口,定义了模板方法,个性化方法,钩子方法。面向接口编程思路要深入骨髓,定义接口是一切工作的起点。
package com.geekarchitect.patterns.demo003;public interface IRoleService { void templateMethod(); void individuationMethod1(); void individuationMethod2(); void hookMethod();}
AbstractTemplateRoleService抽象类,模板角色对应的类
1,实现了模板方法,这是模板角色的主要任务,一般需要添加final修饰符
2,实现了钩子方法,钩子方法将来还可能被子类覆盖,所以不能添加final修饰符。
package com.geekarchitect.patterns.demo003;public abstract class AbstractTemplateRoleService implements IRoleService{ @Override public final void templateMethod() { individuationMethod1(); individuationMethod2(); hookMethod(); } @Override public void hookMethod() { }}
IndividuationRoleService1 个性化角色对应的类
1,继承了模板角色对应的抽象类。
2,实现了所有的个性化方法。
package com.geekarchitect.patterns.demo003;public class IndividuationRoleService1 extends AbstractTemplateRoleService { @Override public void individuationMethod1() { } @Override public void individuationMethod2() { }}
IndividuationRoleService2 个性化角色对应的类
1,继承了模板角色对应的抽象类。
2,实现了所有的个性化方法。
3,覆盖了父类提供的钩子方法
package com.geekarchitect.patterns.demo003;public class IndividuationRoleService2 extends AbstractTemplateRoleService { @Override public void individuationMethod1() { } @Override public void individuationMethod2() { } @Override public void hookMethod() { //super.hookMethod(); }}
至此,模板方法模式我们讲完了,后面我除了继续讲解其他设计模式之外,还会接着开发一些模板方法模式的相关案例,还会带领大家看看在知名的开源软件中,模板方法模式是如何落地的,以及关于模板方法模式的面试策略,有些内容写文章表现力不足,会通过视频的方式进行分享。
由于这是我分享的《架构师基本功之设计模式》系列的第一个设计模式,很多东西还在完善中,大家如果有什么意见或者建议,欢迎留言或者私信我,我会积极改进,让我们一起为中国的软件开发事业,尽一份力。
本期我们就分享到这里,关注我,我将持续分享更多架构师的相关文章和视频,我们下期见。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)