写于2019-11-13
理解设计模式前的前堤:
1、模式的应用目标是把可维护性作为很重要指标的程序,像一次性的Demo程序就不需要多高的可维护性;
2、意识到并认可面向接口编程的好处,不认可请回看1;
3、设计模式的本质是解耦,解耦的根本手段是分层,分的层越多,关系越不直观;
4、不用设计模式完全不影响实现需求,只是写的代码多了,重构多了,写着写着,也就进入了设计模式的“窠臼”。
行为型模式
模板方法模式
模板方法也是用得很多的一种模式,尤其是在重构系统时。当你在两个以上相同的类,代码几乎一模一样,只有少许区别时,就要考虑是否用模板方法模式进行重构。父类定好算法步骤,通常将变化的部分封装成abstract方法,由子类实现该abstract方法。从另一个角度来看模板方法,其实它是对面向对象多态的补充,试想一下,多态是父类(接口)定好接口,子类分别实现之,可现实情况并不会总是如此简单,有些是可复用的部分,让每个子类各自重复实现这些可复用的代码明显是有问题的,重构方法就是把这部分可复用的代码前置到父类去写一份即可,那重构后不就是模板方法的应用了么?缺点就是父类调用子类方法,会增加调试的复杂性。
策略模式
既然是策略,那就是对调用方暴露的,即调用方需要了解有哪些策略。面向接口编程使得策略之间可以互相替换,变化部分通过策略类封装起来,也就意味着一个事情可以有多种实现方案,在使用if...else进行判断选择不同方案的情况下可以考虑使用策略模式来替换。策略类的使用非常广泛,比如Log4J清理策略的选择、线程池的拒绝策略选择,还有各种第三方组件在初始化时各种内部策略的选择,不同序列化协议的策略选择,甚至内部的一段计算逻辑算法也可以封装成不同策略。策略模式的本质还是在于对修改关闭,对扩展开放,有新策略则只需要增加一个策略类并使用即可,那如果我的策略数量很稳定,条件也是很固定的情况下是否还有必要用策略模式呢?比如说,我的策略是基于性别的(男、女、不详三种)基于月份的(1-12月),这种情况可以参考简单工厂与工厂模式的区别,使用类似简单工厂的方案,一个大方法内根据性别或月份采取相应策略,而不需要建立若干策略类,还需要对调用方暴露策略,一来对调用方更友好,二来减少复杂度。另外千万不要为了模式而模式。
责任链模式
责任链模式我们其实并不陌生,想想在web编程中的filter、Netty的handler都使用了该模式。责任链模式解耦了发送者和接收者,使多个对象都有机会处理该请求。那什么情况下适合使用该模式呢?一是存在流水线处理的情况,每一步链式处理相当于流水线上一个节点,完成一部分操作,另一种情况是存在“踢皮球”的情况,可能我不处理,给下家处理,下家也可能又给他的下家处理。总而言之就是需要链式处理的情况下适合应用责任链模式。该模式带来的坏处是:经过链式处理,性能必然有所损耗,尤其是链较长的时候。
观察者模式
观察者模式定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新。观察者模式经常会被拿来和发布订阅模式来做比较,首先,发布订阅模式并不是一种正式的设计模式,只是因为分布式的流行,消息中间件、Redis等的普及使用,导致了发布订阅模式被广为使用;其次,发布订阅模式到底算不算是观察者模式?对此问题也是争论不休,但不可否认的是,它达到了观察者模式同样的效果,即:当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新,甚至它比观察者模式更彻底地解耦了消息的发布者和接收者,使得双方可以完全不需要知道对方的存在。为什么要用观察者模式呢?比如在注册奖励并加积分的场景中,如果不采用模式,以面向过程的方式来编写代码的话,整个过程是这样的:第一步、注册逻辑;第二步、执行奖励逻辑;第三步、增加积分。如果仅以完成需求的角度来看上述处理方式没任何问题。然而需求是不断变化的,可扩展(修改)性是我们不得不面对的问题,如果现在新运营需求是注册时增加积分、同时送优惠券。那你的处理方式大概率就变成了把第二步执行奖励逻辑改成赠送优惠券逻辑,这就违反了开闭原则了。如果采用观察者模式,你只需要加一个优惠券赠送对象,并订阅到用户注册这个Subject,整个过程并没有修改到用户注册这个逻辑。如果采用消息队列的发布订阅模式,则更为简单,只需要在用户注册完成后往消息队列里pub一个消息,对于关注用户注册事件的对象可自行订阅该消息队列并进行处理。
状态模式
状态模式将对象的状态与对应的操作实现成子类,比如说有三个操作,分别是注册、授信、支用,这三个操作对应的状态分别是:未注册、已注册、已授信。用传统的if...else判断代码大致结构是:在注册的时候判断用户是否未注册,否则给出相应提示;在授信的时候判断用户是否已注册,否则给出相应提示;在支用的时候判断用户是否已授信,否则给出相应提示。试想一下,如果新加一种状态,则注册、授信、支用的逻辑都要做相应修改。而状态模式重构后则会生成未注册、已注册、已授信三个状态类,每个状态类都实现了注册、授信、支用这三个方法,由调用方指定用户对应的状态,这样新增状态时,只需新增对应的状态子类,无需修改现有代码逻辑了。状态模式的结构与策略模式很相似,策略模式是由调用方指定对应的策略,状态模式是由调用方指定对应的状态并可进行状态切换,策略模式更带有普通性。状态模式适用在存在多个状态判断的情况,如果代码中存在3层以上的if...else或switch分支对某字段状态值进行条件判断,则可考虑用状态模式进行重构。
迭代器模式
分离了集合对象的遍历行为,抽象出一个迭代器类来负责,无须暴露该对象的内部表示。Java中的Collection都实现了Iterator,日常业务编码中需要实现自定义集合类的地方确实不多,即使有,也是对Java Collection的组合,也可以通过稍微暴露自定义类内部的Java Collection以提供更方便的访问,如for语法的支持,所以日常编程中对这种模式的应用并不多。但是,由于JDK类库的支持,我们对这种使用方式太司空见惯了,以致以差点忘记了这也是一种设计模式。好处是不需要暴露该对象的内部表示,新增集合类时只需要增加一个新集合迭代类,另外还能提供一致的访问接口。坏处是集合类和迭代类会成对增加。
中介者模式
中介是一个古老的角色,生活中也是随处可见。中介者模式类似现实中的中介,它的核心是将网状耦合关系转变为星状耦合关系,从而减少对象与对象间的直接耦合。我们最常用的MVC模式中的C就是中介者类,好处是解耦了M和V,使它们可以各自独立的变化。比如聊天室,如果没有一个中介者(可建模为聊天室),那用户间的关系就是典型的网状关系,反映到代码中,就是每个用户要持有其它同个聊天室用户的引用,这样用户与用户之间是紧耦合关系,而引入了中介者,则解耦了这种紧耦合。用户只需要发消息给中介者(聊天室)它再将消息发给目标用户。网状转为星状的过程中可能存在两种问题,第一种问题是中介者选得过小,极端情况下每两个对象之间有一个中介者,那不同中介者之间又可能产生了网状关联,关系反而变得更加复杂。另一种问题是中介者选得过大,这种情况下,中介者的职责过多,造成了接口数量大量增长。所以中介者模式通常在是在建模时辩证地使用并注意搭配其他设计模式。
解释器模式
通俗点说解释器模式就是用来解析一串文本的,比如自己实现表达式计算器就可以用解释器模式实现。由于各种动态脚本和SQL的应用,日常业务编码中及少用到需要自定义“语法”的地方,所以该模式实际使用得极少。优点是扩展性高,增加新的解析类不需要修改旧的解析代码。 这个核心思想在于构建语法树,进行运算的时候,进行递归调用。
备忘录模式
所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。备忘录模式和我们在现实里面的备忘十分相似,就是用来实现备忘或存档恢复的一种设计模式。设想一个场景:你有一个对象Originator,需要支持该对象的某些字段(非全部)恢复到某个时间点上的状态。用备忘录模式是这样实现的:由于对象Originator只有部分字段是需要支持恢复的,也为了不破坏它的封装,需要引入一个备忘录 Memento类,该对象负责存储Originator类所有需要存储的字段,根据单一职责原则,最后再引入Caretaker(管理者)类负责保存备忘录的工作。
命令模式
命令模式解耦了调用方和接受方,使得他们只依赖于命令Command,从而可以独立变化,新增命令也可以很容易加入到系统中,方便实现命令队列,也方便了实现撤销和恢复操作。命令模式是将接收者要做的事情,抽象成命令,通过命令来解耦。在具体的命令中,用命令所引用的接收者实例进行相应的操作。命令的发送者负责发送各种命令。同一个发送者可以对应多个不同接收者,不同接收者可以对应多个不同的发送者。我们常用的Struts中的action就是用了命令模式,工作流引擎Activiti中整个流程的发起、审批、流转、变量设置都是使用了命令模式完成。那我们在日常编程中怎么应用这个模式呢?调用方对应不特定的接受方或者有一方还待定时,就不能用传统的方法一方引用并调用另一方的方法了,此时可考虑使用命令模式解耦。另外,当需要实现命令队列提供撤销和恢复操作时,也可以考虑使用该模式。
访问者模式
访问者,如其名,就是给一个对象做访问用的。这就很奇怪了:访问一个对象不是用this吗?访问对象的内部不是用方法吗?
那用this.methodX()不就可以了吗?为什么还要单独加一个访问者类呢?想象一下,如果一个类的结构(属性和基础方法)是稳定的,但访问方法是易变化的,那把易变化的部分抽取到访问者类,当业务变更的时候是不是只需要修改访问者类就行了呢?是不是达到“动静分离”的效果了呢?迪米特法则说,一个对象应当对其他对象尽可能少的了解,那访问者类( Visitor)是不是对被访问者类(Element)知道的太多了?如果扣细节的话这样做确实是违反了迪米特法则,但是如果把Element和 Visitor看作是整体呢?当Element的种类固定时, Visitor的 Visit访问方法的重载数量一般跟Element的种类数量相同,而Element也定式的有accept(接受访问)方法。访问者模式的适应场景较苛刻:Element数量与结构较稳定,而访问方法/算法又易变时可以考虑使用访问者模式。