skip to content
天地同游

设计模式

/ 22 min read

Updated:
Table of Contents

多态

多态是同一个行为(single interface)具有多个不同表现形式或形态(different type)的能力

In object-oriented programming, polymorphism is the provision of a single interface to entities of different types.[2] The concept is borrowed from a principle in biology where an organism or species can have many different forms or stages.

The most commonly recognized major forms of polymorphism are:

  • Ad hoc polymorphism: defines a common interface for an arbitrary set of individually specified types.
  • Parametric polymorphism: not specifying concrete types and instead use abstract symbols that can substitute for any type.
  • Subtyping (also called subtype polymorphism or inclusion polymorphism): when a name denotes instances of many different classes related by some common superclass.

设计模式的精髓在于对面向对象编程特性之一多态的灵活应用,而多态正是面向对象编程的本质所在

  • 子类实现父类或者接口的抽象方法,程序使用抽象父类或者接口编程,运行期注入不同的子类,程序就表现出不同的形态. 这是多态的一种表现形式——子类多态.

  • 多态的核心思想是允许使用统一的接口来处理不同类型的数据或对象。重载正是实现了这一点:它允许使用同一个方法名来处理不同类型或数量的参数.

  • 泛型之所以被视为多态的一种形式,是因为它提供了一种机制,允许单一的代码结构(类、接口或方法)以统一的方式处理多种不同的类型。这完全符合多态的核心理念 —— 为不同类型提供统一的接口。泛型通过类型参数化实现了这一点,使得同一段代码可以适用于多种类型,同时保持类型安全性和代码的可重用性

抽象类 vs 接口

  • 抽象类是一种自下而上的设计思路,先是有了重复的代码,再进行提取抽象,所以抽象类的目的也是为了解决代码的复用问题,跟子类是is-a的关系
  • 接口更像是自上而下的设计,先是归纳出能够提供的功能,再实现细节。接口为了解决可扩展性的问题,使得代码的迭代更容易符合开闭原则,一个接口代表了某个特定的功能,跟其实现类是has-a的关系

组合 vs 继承

如果类之间的继承结构稳定,层次比较浅,关系不复杂,我们就可以使用继承。反之应该尽量使用组合

  • VO是一种DTO,主要作为接口的数据传承载体,将数据发送到其它系统,从功能上看不应包含业务逻辑,所以通常设计成贫血模型

鉴权功能实现

  • 调用方进行接口请求的时候,将 URL、AppID、密码、时间戳拼接在一起,通过加密算法生成 token,并且将 token、AppID、时间戳拼接在 URL 中,一并发送到微服务端
  • 微服务端在接收到调用方的接口请求之后,从请求中拆解出 token、AppID、时间戳
  • 微服务端首先检查传递过来的时间戳跟当前时间,是否在 token 失效时间窗口内。如果已经超过失效时间,那就算接口调用鉴权失败,拒绝接口调用请求
  • 如果 token 验证没有过期失效,微服务端再从自己的存储中,取出 AppID 对应的密码,通过同样的 token 生成算法,生成另外一个 token,与调用方传递过来的 token 进行匹配;如果一致,则鉴权成功,允许接口调用,否则就拒绝接口调用

控制反转 vs 依赖注入

  • 实际上,控制反转是一个比较笼统的设计思想,并不是一种具体的实现方法,一般用来指导框架层面的设计。这里所说的“控制”指的是对程序执行流程的控制,而“反转”指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程通过框架来控制。流程的控制权从程序员“反转”给了框架

  • 依赖注入和控制反转恰恰相反,它是一种具体的编码技巧。我们不通过 new 的方式在类内部创建依赖类的对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类来使用

单元测试

  • 当我们要测试的功能包含了对外部接口的依赖,例如为了获取某些外部数据,可以通过继承该接口/类重写特定方法的方式屏蔽外部环境的影响——多态在单元测试中的应用
  • 封装:屏蔽实现细节,也有利于单元测试,在面对需要外部依赖的情况下,可以把对外部的依赖封装起来。因为封装起来之后再写单元测试时就可以直接提供封装部分的返回结果

装饰器模式

  • 装饰器类是对功能的增强,而策略模式附加的是跟原始类无关的功能,通常是非业务相关的功能
  • 装饰器体现多态性,代理体现封装性
  • 装饰器之间可以灵活组装,代理用来隐藏对象的实现和控制访问

门面模式

  • 解决分布式事务问题:如果某个需要事务性保证的场景,是通过分别调用2个不同系统的接口完成的,我们可以通过门面模式将2个接口封装到一个接口内,在这一个接口内实现事务

组合模式

组合模式的设计思路,与其说是一种设计模式,不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成树这种数据结构,业务需求可以通过在树上的递归遍历算法来实现

设计模式要干的事情就是解耦。创建型模式是将创建和使用代码解耦,结构型模式将不同功能代码解耦,行为型将不同的行为解耦

建造者模式

  • 场景: 当我们要创建的对象属性与属性之间有逻辑判断时,直接在构造器中肯定无法完成,set方法也不够,因为你不知道所有属性什么时候才能全部设置完成,只能在所有属性的值都设置完成后完整的判断一次,即build()方法, 如下例子中, build方法统一做逻辑判断

    public Calendar build() {
    if (locale == null) {
    locale = Locale.getDefault();
    }
    if (type == null) {
    if (locale.getCountry() == "TH"
    && locale.getLanguage() == "th") {
    type = "buddhist";
    } else {
    type = "gregory";
    }
    }
    cal.setLenient(lenient);
    if (firstDayOfWeek != 0) {
    cal.setFirstDayOfWeek(firstDayOfWeek);
    cal.setMinimalDaysInFirstWeek(minimalDaysInFirstWeek);
    }
    if (isInstantSet()) {
    cal.setTimeInMillis(instant);
    cal.complete();
    return cal;
    }
    return cal;
    }
  • 构造器的set方法的返回值都是Builder本身, 这样有利于链式调用, 即setX().setY(), 我们在set属性的时候也可以对传入的参数进行校验

  • Builder是一个静态内部类, 且需要把外部类(即需要被创建类)的属性重复声明一遍

  • 为了避免构造函数的参数列表过长,不同的构造函数过多,一般是使用Builder或者通过无参构造函数创建对象

适配器模式

  • 为每一个目标类新建对应的适配器类,所有的适配器类都实现一个共同的接口,再需要使用目标类时就可以用对应的适配器类,而这些适配器类面向接口编程,可以利用多态的特性使得代码更具备扩展性
  • 所以适配器类是对现有结构的改善,如果从一开始设计的时候就能够让各个实现类都是实现一个相同的接口,那就很容易扩展了,也就没有适配器模式的用武之地。或者我们需要的功能本身就依赖不同的第三方,那么我们也可以使用适配器为每一种第三方实现适配

策略模式

定义一系列算法,把它们封装起来,并且使它们可以互相替换,使得算法可以独立于客户端的变化而变化.例如我们每一种频道的节目需要不同的处理逻辑,则定义各自的算法

  • 基于接口
  • 每个客户端都需要知道具体的策略实现才好选择
  • Comparator就是一个策略模式的例子.每个实现Comparator接口的都是一个策略
  • 如果每个策略类都是无状态的,就没必要在每次使用的时候都重新创建一个新的对象
  • 策略模式除了避免if-else的分支判断逻辑,主要的还是解耦策略的定义,创建和使用

代理模式

控制对某个对象的访问,例如反向代理,虽然不是设计模式,也表达了代理的思想,为了保护后端服务器避免直接被访问到.

模版方法模式

定义一个操作中算法的骨架,而将一些步骤延迟到子类中实现,使得子类可以不改变整体算法的结构而自定义个别算法的实现细节

  • 模板方法除了可以把具有特定处理步骤的逻辑抽象出来的好处,还能起到统一集中管理某些关键操作的作用.例如对于Spring的JDBC Template,能够确保尽可能地将资源的获取和释放操作放到一起,以一种统一而集中的方式来处理资源的获取和释放,避免将这种容器出现问题的操作分散到代码中的各个地方,作为第三方包的提供方也希望将这种容易造成问题的处理由自己完成
  • 模板方法类一定要是抽象类吗?模板方法通常把相同的行为提取到模板方法类中,某些实现细节下沉到各个具体类,例如在电视猫做节目处理时从收到消息到提取数据等流程是固定的,但是不同节目类型(电影,电视剧)的提取细节是不同的.此时就把这些细节下沉到电影类,电视剧类,共同的逻辑提取到抽象类.这里的模板方法类以抽象类的形式存在是没问题的,而JDBC Template以非抽象类的形式存在可以看作是模板方法的一个变种,其本身定位是一个工具类,需要直接被拿来使用,因此把抽象方法改成了callback.为什么这里是callback,首先template是一个工具类需要被客户端调用,而客户端在调用之前需要准备好callback的实现,当在客户端使用template时,template里抽象出来的那部分又反过来调用了客户端实现好了callback的逻辑.总结下来,如果一个公共类或者工具类本身的功能提供出去同时部分只能抽象,那么抽象的那部分就符合callback的定义,你在定义接口名字的时候通常应该以Callback结尾.
    • 扩展:以工具类 +callback的形式进行抽象是有好处的,工具类本身可以更简洁方便的被使用,如果是抽象类则需要新建一个子类,对于比较小的功能再特意建一个类反而不够简洁,在前一种形式下只要在客户端调用的类里实现一个callback接口即可
Callback

A调用B的某个function f, 同时A会提供给B一个A自己实现的函数f’, 当B执行f时会返过来调用A的函数f’, 即为回调

  • 同步回调: 在函数返回之前执行回调函数. JdbcTemplate是同步回调, 同步回调更像是模板模式, 主要是解决了代码的复用性问题
    • 异步回调: 在函数返回之后执行回调函数. 我们通常注册的各种Listener就是异步回调, 更像是观察者模式

工厂方法

把对象的创建移动到一个工厂类中使得代码责任分明更容易维护-单一责任原则

把对象的使用和创建分开,新建新的类型对象时也完全不用调整调用类的代码-开闭原则

切券功能中使用了StrategyFactory来创建具体的切券策略,连续切券策略还是累计切券策略,而这些策略又都实现了ICutSecurityStrategy接口,使得在调用入口处通过StrategyFactory获得ICutSecurityStrategy类型的对象,直接调用ICutSecurityStrategy.cut()就可以了,完全不用考虑运行时是哪个切券策略在执行.

开闭原则也不用完全不改代码,可以把改动转移到不重要的类中,保证核心代码块是符合开闭原则的.所以很多策略中都会新建一个类出来(工厂类、策略类),以满足开闭原则

复杂项目开发

  • 简单清晰,可读性好,是任何大型软件开发要遵循的首要原则。只要可读性好,即便扩展性不好也是可以改得动的,否则看都看不懂就很难维护了
  • 尽量避免过度设计,过早优化,在扩展性和可读性有冲突时首选可读性

函数式编程

  • 面向对象:以类,对象作为组织代码的单元,及其体现的封装,继承,多态的特性
  • 面向过程:以函数作为组织代码的单元,数据与方法分离

接口隔离原则

其它

所有的策略都是为了达到面向对象的封装,SOLID设计原则.各种设计模式都是面向对象的3大特点---继承,多态,封装的应用

基于接口实现,使得功能变得更容易扩展,具体的实现方式发生改变只需要写一个新的实现类,业务流程没有太大的变动,只需要把原来的实现类替换成新的实现类就可以,最大限度地满足了开闭原则

  • 如果功能的实现从一开始就没有遵循基于接口实现的原则,也就很难在迭代的过程中应用上设计模式了。例如策略模式,如果当前的代码不是基于接口实现的,想要通过策略模式去实现非业务逻辑的补充也要修改到原来的类,让它变成基于接口实现的方式,就已经打破了开闭原则

  • 设计没有最优解,只有权衡和取舍

  • 单元测试记录下来某个功能点所表现出来的状态,当我们回过头再去优化重构的时候,这些重构后的代码跑单元测试应该表现出同样的状态

  • try catch要跟对应的代码在同一个线程中才能catch到异常,如果try中的代码是提交到线程池的一个任务那么该任务的异常会被吞掉

  • 很多设计模式都有接口,就是参照了基于接口而非实现编程的设计思想

  • 如果开发的复杂度很高,可以对某些异常的处理在工程上做妥协,交给业务系统或者人工处理

  • 设计模式虽然会让类的数量变多,但是总的代码行数应该更少

设计模式的精髓是对多态的使用

这是摘抄的李智慧的一篇文章中的副标题,深有同感,很多设计模式的精髓之处就是多态的应用,我在几次设计的过程中没有刻意匹配某个设计模式,而是根据实际需要抽象出一个接口,让原本的类依赖于接口而不是特定的类,对某些公用的不可修改的类,使用一个Wrapper类包装一下,以实现增强

精通设计模式,就是忘了设计模式. 因为在实际开发中,一个完整的场景往往是多个模式的组合,只要你根据SOLID的思想和复用的思想去重构代码,不用严格的遵循某个模式的定义,能实现可维护,满足开闭原则,易于持续迭代就够了.策略模式往往都不是单纯的策略模式,因为具体的策略之间可能存在重叠的部分,需要提取到父类中,这样就可能在父类中形成模版模式

设计模式的要点

  • 分离变化和不变
  • 针对接口编程,而不是实现
  • 优先使用对象组合,而不是类继承
  • 松耦合
  • 单一职责