深入理解软件设计模式

Tech 导读
本文从设计模式与编程语言的关系,设计模式与架构模式的区别,设计原则和设计模式的关系等几个维度进行了分析和解答。关于设计模式应该如何学习和应用的问题,给出了学习意见和实践心得。为了让设计模式更加的直观和立体,本文主要通过场景化的案例,以设计模式的方式给出解决方案。冰冻三尺非一日之寒,滴水石穿非一日之功,希望本文能够为你带来帮助。
导读
本文从设计模式与编程语言的关系,设计模式与架构模式的区别,设计原则和设计模式的关系等几个维度进行了分析和解答。关于设计模式应该如何学习和应用的问题,给出了学习意见和实践心得。为了让设计模式更加的直观和立体,本文主要通过场景化的案例,以设计模式的方式给出解决方案。冰冻三尺非一日之寒,滴水石穿非一日之功,希望本文能够为你带来帮助。
设计模式与编程语言
1.1 什么是设计模式
1.2 什么是编程范式
要探讨设计模式和编程语言的关系,还得从编程范式谈起。编程范式一词最早来自 Robert Floyd 在1979年图灵奖的颁奖演说,是程序员看待程序的观点,代表了程序设计者认为程序应该如何被构建和执行的看法,与软件建模方式和架构风格有紧密关系。
当前主流的编程范式有三种:
1. 结构化编程(structured programming)
2. 面向对象编程(object-oriented programming)
3. 函数式编程(functional programming)
这几种编程范式之间的关系如下:
1. 起初是非结构化编程,指令(goto指令)可以随便跳转,数据可以随便引用。后来有了结构化编程,人们把 goto 语句去掉了,约束了指令的方向性,过程之间是单向的,但数据却是可以全局访问的;
2. 后来面向对象编程的时候,人们干脆将数据与其紧密耦合的方法放在一个逻辑边界内,约束了数据的作用域,靠关系来查找;
3. 到函数式编程的时候,人们约束了数据的可变性,通过一系列函数的组合来描述数据,从源到目标映射规则的编排,中间它是无状态的;
编程范式是抽象的,编程语言是具体的。编程范式是编程语言背后的思想,要通过编程语言来体现。C 语言的主流编程范式是结构化编程,而 Java 语言的主流编程范式是面向对象编程,后来 Java8 开始支持 Lambda 表达式,将函数式编程范式的内容融合进来,同时新诞生的语言一开始就支持多范式,比如 Scala,Go 和 Rust 等。
从结构化编程到面向对象编程,再到函数式编程,抽象程度越来越高(离图灵机模型越来越远),与领域问题的距离越来越近。直观地来讲,就是解决现实问题的效率提升了,灵活性和执行效率随之有所下降。
设计模式无论用什么语言实现都是可以的,然而由于语言的各自差异化特点,不是每种语言都完美或统一实现各种设计模式。比如 Java 里面有策略模式,那是因为 Java8 之前不支持方法传递,不能把一个方法当作参数传给别人,所以有了策略模式。而 JavaScript 等语言可以直接传函数,就根本没必要造一个策略模式出来。
1.3 什么是多态特性
面向对象编程语言有三大特性:封装、继承和多态。
1. 封装即信息隐藏或数据保护,“数据结构”通过暴露有限的访问接口,授权外部仅能通过"数据结构"提供的方法(函数)来访问其内部的数据;
2. 继承的好处是可以实现代码复用,但不应过度使用,如果继承的层次过深就会导致代码可读性和可维护性变差。因此建议少用继承而多用组合模式;
3. 多态可以分为变量的多态,方法的多态,类的多态。通常强调的是类的多态,多态的实现是指子类可以替换父类,在实际代码运行过程中调用子类的方法实现;
多态可以说是面向对象中最重要的一个特性,是解决项目中紧偶合的问题,提高代码的可扩展性和可复用性的核心,是很多设计模式、设计原则、编程技巧的代码实现基础。
多态比较直观的理解就是去完成某个动作,当不同的对象去完成时会产生出不同的状态,其作用范围可以是方法的参数和方法的返回类型。
多态这种特性也需要编程语言提供特殊的语法机制来实现,Java 中多态可以通过"子类继承父类+子类重写父类方法+父类引用指向子类对象"的方式实现,还可以通过"接口语法"的方式实现。C++中则使用 virtual(虚函数)关键字来实现。像一些动态语言如 Python 也可以通过 duck-typing 的语法实现,另外 Go 语言中的"隐藏式接口"也算是 duck-typing。
Python 语言,实现多态示例如下
class MyFile:def write(self):print('I write a message into file.')class MyDB:def write(self):print('I write data into db. ')def doIt(writer):writer.write()def demo():myFile= MyFile()myDB = MyDB()doIt(myFile)doIt(myDB )
设计模式与架构模式
2.1了解架构模式
对给定上下文的软件架构中常见问题的一种通用的可复用的解决方案,可以为设计大型软件系统的各个方面提供相应的指导。它不仅显示了软件需求和软件结构之间的对应关系,而且指定了整个软件系统的组织和拓扑结构,提供了一些设计决策的基本原理,常见的架构设计模式如下:
电子商务web应用程序; 移动App; | ||
基于IPC的应用程序; | ||
进程内多线程调度; | ||
网络传输中的代理软件; | ||
多媒体协议; | ||
注册中心; | ||
GUI 应用程序; | ||
车辆识别及追踪; | ||
(Interpreter pattern) | 用于描述通信协议的语言 |
2.2 了解设计模式
在1995年,有四位编程界的前辈合著了一本书,书名叫做《Design Patterns: Elements of Reusable Object-Oriented Software》,翻译过来就是《设计模式:可复用面向对象软件的基础》,书里面总共收录了23种设计模式。这本书是软件研发领域重要的里程碑,合著此书的四位作者,被业内称为GoF(Gang of Four),因此这本书也被人称为GoF设计模式。
单例模式 (Singleton) | ||
2.3 小结
· 架构模式更像是宏观战略层面的设计,设计模式则更像是战略目标拆解出来的具体任务的实现方案;
• 实现一种软件架构,不同组成部分可能用到不同的设计模式,某个部分也可能可以采用不同的设计模式来实现。
应用实践指南
3.1适用场景
不使用设计模式也能实现业务诉求,系统也能够正常运行,为什么要使用设计模式呢?
是的,相当一部分场景是不需要进行设计模式的引入的,比如:业务逻辑简单,业务演进方向不明朗,或者就是一个不需要经常迭代的功能点。但当遇到了复杂问题设计的时候,就需要借助前人的经验了,而设计模式就是前人沉淀总结的各种常见问题的解决方案。
那么多种设计模式,难道需要全部系统地学习实现一遍,都要闭着眼睛就能写出来吗?其实不用,这就跟排序算法一样,只需要记住每种算法的适用范围和场景就可以了,在有需要的时候,再去深入研究就可以了。以下总结了各种设计模式对应的适用场景:
3.2 场景案例
为了让读者对设计模式有个更加直观立体的感知,接下来以实际案例为大家展现一下设计模式在实际场景的应用。案例包含了创建型,结构型,行为型各种模式类型里常用的设计模式,比如:
• 用工厂模式隔离业务实现;
• 用策略模式消解业务流程分支;
• 用模板方法模式提取业务分支公共流程;
• 用建造者模式简化入参对象的构建难度;
• 用代理模式横向扩展通用能力(日志,异常处理);
• 用职责链模式对请求进行敏感词,防刷校验;
• 用命令模式让指令拥有了记忆。
工厂模式
首先,这三个人是如何成为和尚的呢?
每一个和尚的来历都不尽相同,但在当下喝不上水,这件事情上,都显得不重要。重要的是,只要凑足三个和尚,就会没水喝。那么寺庙如何招收和尚?这里就可以用到工厂模式的思想。
// 贫困潦倒产生的和尚过程:1.大山里;2.闹饥荒;3.要吃饭;一号和尚 = HeShangFactoty.getOneHeshang("贫困潦倒型");// 走投无路产生的和尚过程:1.生性耿直;2.打死恶霸;3.要赎罪;二号和尚 = HeShangFactoty.getOneHeshang("走投无路型");// 天选之子产生的和尚过程:1.敏而好学;2.佛学感兴趣;3.要广大佛法;三号和尚 = HeShangFactoty.getOneHeshang("天选之子型");
以上示例想体现的是工厂模式能将复杂的对象创建和使用进行了分离设计。下面就以和尚吃水这件事情,用程序的方式详细展现工厂模式的实现思路。按照和尚的人数,分别有挑,抬,等三种实现方式。以下为基础代码实现:
public interface Waterable {Water getWater();}public class TiaoShui implements Waterable{public Water getWater(){System.out.println("先到山下去!");return "水是挑上来的!";}}public class TaiShui implements Waterable{public Water getWater(){System.out.println("先到山下去!");return "水是抬上来的!";}}public class DengShui implements Waterable{public Water getWater(){System.out.println("就坐在原地!");return "水是等不来的!";}}
具体使用
public class Factory {/*** 按照和尚数量生成取水对象** @param heShangNum 和尚数量* @return*/public static Waterable getWaterable(Integer heShangNum) {switch (heShangNum) {case 1:return new TiaoShui();case 2:return new TaiShui();case 3:return new DengShui();default:throw new RuntimeException("庙小,装不下那么多和尚!");}}}
策略模式
按照不同的条件(人数),分别有几种获取水的方法:挑,抬,等。可以通过策略模式来实现,前面的实现方式其实就是策略模式和工厂模式的结合。我们再看一下策略模式的具体使用方式如下:
/*** 通过入参和尚人数,就可以动态改变Waterable.getWater()的取水模式* @param heShangNum* @return*/public void getWater(Integer heShangNum) {Waterable waterable = Factory.getWaterable(heShangNum);Water water = waterable.getWater();// 取水}
1. 输入参数1:挑水模式的实现(对应Tiaoshui实现类);
2. 输入参数2:抬水模式的实现(对应Taishui实现类);
3. 输入参数3:等不到水模式的实现(对应Dengshui实现类)。
通过和尚人数,就可以动态获得对应的取水实现,即所谓的通过策略实现业务,对于使用方来说(主流程),无需关注取水的具体实现(解耦:业务流程稳定性的设计体现),新增取水方式时,只需要新增一个类实现就可以了,存量的实现和主流程都不会受到影响。
模板方法
细化取水过程,取水过程一般需要三步:
public interface Waterable {Water getWater();}public abstract class AbstractWaterable implements Waterable {@Overridepublic Water getWater() {takeTool();toRiver();return moveWater();}/*** 拿起工具*/protected abstract String takeTool();/*** 到河边去*/protected String toRiver() {System.out.println("走过去!");return "步行";}/*** 将水带回来** @return*/protected abstract Water moveWater();}
个性化场景实现
public class TiaoShui extends AbstractWaterable {@Overrideprotected String takeTool() {return "扁担";}@Overrideprotected Water moveWater() {return "挑水";}}public class Taishui extends AbstractWaterable{@Overrideprotected String takeTool() {return "木棍";}@Overrideprotected Water moveWater() {return "抬水";}}public class DengShui extends AbstractWaterable{@Overrideprotected String takeTool() {return "意念";}@Overrideprotected String toRiver() {return "一动不动";}@Overrideprotected Water moveWater() {return "无水";}}
/*** 和尚取水:实现一个和尚挑水喝,两个和尚抬水喝,三个和尚等水喝*/public void fetchWater(){//for (int heShangNum = 1; heShangNum < 4; heShangNum++) {Waterable waterable = Factory.getWaterable(heShangNum);Water water = waterable.getWater();}}
模板方法讲的是流程标准定义和能力复用。示例中,定义了取水的三个阶段,选择工具,出行方式,搬运方式。单看出行方式中,【挑水】和【抬水】复用了模板方法里的通用实现,【等水】则个性化的重写了出行方式。
public class ToolBox {private final String bianDan;private final String muTong;private final String muGun;private ToolBox(TiaoBuilder builder){this.bianDan=builder.bianDan;this.muTong=builder.muTong;this.muGun = null;}private ToolBox(TaiBuilder builder){this.bianDan = null;this.muTong = null;this.muGun=builder.muGun;}private ToolBox(DengBuilder builder){this.bianDan = null;this.muTong = null;this.muGun=null;}public static class TiaoBuilder{private String bianDan;private String muTong;public TiaoBuilder setBianDan(String bianDan) {this.bianDan = bianDan;return this;}public TiaoBuilder setMuTong(String muTong) {this.muTong = muTong;return this;}public ToolBox build(){return new ToolBox(this);}}public static class TaiBuilder{private String muGun;private String muTong;public TaiBuilder setMuGun(String muGun) {this.muGun = muGun;return this;}public TaiBuilder setMuTong(String muTong) {this.muTong = muTong;return this;}public ToolBox build(){return new ToolBox(this);}}public static class DengBuilder{public ToolBox build(){return new ToolBox(this);}}//省略getter方法}
ToolBox oneHeShangToolBox = new ToolBox.TiaoBuilder().setMuTong("小号木桶").setBianDan("小号扁担").build();ToolBox twoHeShangToolBox = new ToolBox.TaiBuilder().setMuTong("大号木桶").setMuGun("长号木棍").build();ToolBox threeHeShangToolBox = new ToolBox.DengBuilder().build();
代理模式
为了鼓励多劳多得,庙里取水开始采用积分机制,每取回来一桶水就要敲一下木鱼,打一次卡。这里就可以采用代理模式。代理分为静态代理和动态代理,为了简单起见,这里就用静态代理来举例。
public class WaterableProxy implements Waterable{/*** 被代理的原始对象*/private Waterable waterable;public WaterableProxy(Waterable waterable) {this.waterable = waterable;}@Overridepublic Water getWater() {Water water = waterable.getWater();// 增强的新功能,不管是挑水,抬水,等水,只有带回来水,就可以if(water != "无水"){System.out.println("我敲一下木鱼,打一次卡!");}return water;}}
具体使用
public void doProxy(){Waterable waterable = new Taishui();WaterableProxy proxyWaterable = new WaterableProxy(waterable);proxyWaterable.getWater();}
public interface SecureFilter {void filter(Map context);}public class PetSecure implements SecureFilter{@Overridepublic void filter(Map context) {if(context.containsKey("宠物")){throw new RuntimeException("请出去:禁止携带宠物进入!");}}}public class WearSecure implements SecureFilter{@Overridepublic void filter(Map context) {if(context.containsKey("光膀子")){throw new RuntimeException("请出去:有伤风化者!");}}}public class OtherSecure implements SecureFilter{@Overridepublic void filter(Map context) {if(context.containsKey("大声喧哗")){throw new RuntimeException("请出去:佛门乃清净之地!");}}}
/*** 安检责任链实现*/class SecureChain implements SecureFilter{// 注入PetSecure,WearSecure,OtherSecure等过滤器private List<SecureFilter> secureFilterList;/*** 进入寺庙,进行安检逻辑* @param context*/@Overridepublic void filter(Map context) {// 进行安检流程for (SecureFilter secureFilter : secureFilterList) {secureFilter.filter(context);}System.out.println("佛祖保佑,安检通过!");}}
责任链模式一般和过滤器模式组合一起使用,即创建一个链条,经过这个链条处理的所有对象和数据分别进行依次加工,每个环节负责处理不同的业务,环节间彼此独立解耦,同时可以复用。这种设计的巧妙之处在于可以链式调用,不同的过滤方式可以灵活的排序和组合。既可以使用单个过滤器进行处理,也可以直接添加一条责任链。
2.二号和尚扫庭院;
public class Command implements Serializable {// 做早餐,打扫,敲钟等指令标识private OrderTypeEnum order;// 正向执行OR逆向回滚private Integer direction;// 省略get和set方法}// 指令动作执行器,每种指令对应一个实现public interface OrderHandler {/*** 执行逻辑** @param callContext* @return*/PipeResult execute(CallContext callContext);/*** 支持的命令类型:做早餐,打扫,敲钟等命令标识** @return*/OrderTypeEnum getOrderType();}// 指令类型管理器public interface PipelineCmd {/*** 指令行定义** @return*/Command getCommand();/*** 执行逻辑** @param pipeContext* @return*/PipeResult execute(PipeContext pipeContext);/*** 如果可以撤消指令,则此方法应返回true ,否则返回false** @return*/default boolean isReversible() {return true;}}// 指令执行器管理器public interface CmdHandler {/*** 业务执行** @param callContext* @return*/PipeResult execute(CallContext callContext);/*** 业务回滚(只回滚当前指令)** @param callContext* @return*/PipeResult rollback(CallContext callContext);/*** 全部回滚** @param pipeContext* @return*/PipeResult rollbackAll(PipeContext pipeContext);}
命令实现
public class ZhuChiCmd implements PipelineCmd {private Command command;private transient OrderHandler orderHandler;public StepCmd(Command command, OrderHandler orderHandler) {this.command = command;this.orderHandler= orderHandler;}@Overridepublic PipeResult execute(PipeContext pipeContext) {return orderHandler.execute(new CallContext(command, pipeContext));}// 省略get和set方法}public class Breakfast implements OrderHandler {/*** 执行逻辑** @param callContext* @return*/PipeResult execute(CallContext callContext){System.out.println("做早餐啦!");}/*** 支持的指令类型:做早餐,打扫,敲钟等指令标识** @return*/OrderTypeEnum getOrderType(){return OrderTypeEnum.BREAKFAST;}}public class Clean implements OrderHandler {/*** 执行逻辑** @param callContext* @return*/PipeResult execute(CallContext callContext){System.out.println("打扫庭院啦!");}/*** 支持的指令类型:做早餐,打扫,敲钟等命令标识** @return*/OrderTypeEnum getOrderType(){return OrderTypeEnum.CLEAN;}}public class Ring implements OrderHandler {/*** 执行逻辑** @param callContext* @return*/PipeResult execute(CallContext callContext){System.out.println("敲钟啦!");}/*** 支持的命令类型:做早餐,打扫,敲钟等指令标识** @return*/OrderTypeEnum getOrderType(){return OrderTypeEnum.Ring;}}public class CmdFactory {private List<OrderHandler> orderHandlerList;/*** 获取指定指令条件的指令对象** @param command* @return*/public PipelineCmd getPipelineCmd(Command command) {for (OrderHandler orderHandler : orderHandlerList) {OrderTypeEnum orderTypeEnum = orderHandler.getOrderType();if (orderTypeEnum.equals(command.getOrder())) {return new ZhuChiCmd(command, orderHandler);}}throw new RuntimeException("对不起主持:没有多余的和尚来执行新命令了!");}/*** 获取给定指令的回滚操作指令对象** @param command* @return*/public PipelineCmd getRollbackPipelineCmd(Command command) {Command rollbackCommand = getRollbackCommand(command);return getPipelineCmd(rollbackCommand);}}
public class CmdHandlerImpl implements CmdHandler {private CmdFactory cmdFactory;@Overridepublic PipeResult execute(CallContext callContext) {PipelineCmd pipelineCmd = cmdFactory.getPipelineCmd(callContext.getCommand());PipeResult pipeResult = pipelineCmd.execute(callContext.getPipeContext());return pipeResult;}@Overridepublic PipeResult rollback(CallContext callContext) {Command rollbackCommand = cmdFactory.getRollbackCommand(callContext.getCommand());if (rollbackCommand == null) {return new PipeResult("不需要回滚");}PipelineCmd pipelineCmd = cmdFactory.getPipelineCmd(rollbackCommand);if (!pipelineCmd.isReversible()) {return new PipeResult("不支持回滚");}PipeResult pipeResult = pipelineCmd.execute(callContext.getPipeContext());return pipeResult;}@Overridepublic PipeResult rollbackAll(PipeContext pipeContext) {// 命令执行备忘录模式对象,这里不再展开Caretaker<Command> caretaker = pipeContext.getCaretaker();// 拿到上一步执行命令,依次循环回滚Command command = caretaker.pop();while (command != null) {PipelineCmd pipelineCmd = cmdFactory.getRollbackPipelineCmd(command);if (pipelineCmd != null) {pipelineCmd.execute(pipeContext);}command = caretaker.pop();}return new PipeResult();}}
3.3 实践心得
设计原则
具体包含单一职责原则SRP、开闭原则OCP、里氏替换原则LSP、依赖倒置原则DIP、接口隔离原则ISP、最少知识原则LKP等很多种,其核心还是围绕着低耦合,高复用,高内聚,易扩展,易维护展开的。
模式与原则
1.设计原则是指导思想,设计模式是实现手段之一;
2. 设计原则在实际开发中并不能做到完全遵守,往往是打破一些原则,遵守一些原则,来实现设计的合理性;(成本,性能)
3. 设计模式往往是问题解决方案的骨架,有时候可以当做开发规范和任务拆分执行落地的技术手段;
4. 一个设计模式,往往不仅仅采用一种设计原则,而是一些设计原则的整合;
5. 设计模式不是一成不变的,可以根据问题场景,输出新的模式;
6. 一个复杂场景问题,有时候需要多种设计模式的组合;
7. 学设计模式,死记硬背是没用的,要从实践中习得;
8. 避免设计过度,使简单的问题复杂化。一定要牢记简洁原则,设计模式是为了使设计简单,而不是更复杂。
当然,为了让设计模式更加的直观和立体,也花了大量篇幅在应用实践案例上面,主要是通过场景化的案例,以设计模式的方式给出解决方案,其中部分场景为了方便理解,将问题做了简化处理,但这不影响对于设计模式要解决的问题类型的理解。冰冻三尺非一日之寒,滴水石穿非一日之功,希望本文能够为你带来帮助。
求分享
求点赞
求在看
▪功能支撑:会员成长体系、等级计算策略、权益体系、营销底层能力支持
▪用户活跃:会员关怀、用户触达、活跃活动、业务线交叉获客、拉新促活