【设计模式】【第九章】【设计模式小结】
项目代码:https://gitee.com/java_wxid/java_wxid/tree/master/demo/design-demo
设计模式的原则
单一职责原则:一个类只负责一个功能领域中的相应职责
开闭原则:一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展
里氏代换原则 (Liskov Substitution Principle, LSP):所有引用基类(父类)的地方必须能透明地使用其子类的对象
依赖倒置: 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象,其核心思想是:要面向接口编程,不要面向实现编程
接口隔离原则:使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
迪米特法则:一个软件实体应当尽可能少地与其他实体发生相互作用
策略模式 + 工厂模式 + 门面模式
项目需求概述:用户付款模块设计。要求支持“支付宝付款 0”、“微信付款 1”、“银行卡付款 2” 三种方式。并且提供良好的扩展性,封装性。尽可能为上层调用模块提供最简洁的调用方式。
访问方式: HTTP POST 请求
PostBody入参: 包含用户account,支付类型,支付金额,所购产品。
策略模式(Strategy Pattern)是一种比较简单的模式,也叫做政策模式。
● Context封装角色
它也叫做上下文角色,起承上启下封装作用,屏蔽高层模块对策略、算法的直接访问,封装可能存在的变化。
● Strategy抽象策略角色。
● ConcreteStrategy具体策略角色
策略模式的优点
● 算法可以自由切换
这是策略模式本身定义的,只要实现抽象策略,它就成为策略家族的一个成员,通过封装角色对其进行封装,保证对外提供“可自由切换”的策略。
● 避免使用多重条件判断 (避免的调用层的)
如果没有策略模式,我们想想看会是什么样子?一个策略家族有5个策略算法,一会要使用A策略,一会要使用B策略,怎么设计呢?使用多重的条件语句?多重条件语句不易维护,而且出错的概率大大增强。使用策略模式后,可以由其他模块决定采用何种策略,策略家族对外提供的访问接口就是封装类,简化了操作,同时避免了条件语句判断。
● 扩展性良好
这甚至都不用说是它的优点,因为它太明显了。在现有的系统中增加一个策略太容易了,只要实现接口就可以了,其他都不用修改,类似于一个可反复拆卸的插件,这大大地符合了OCP原则。
策略模式的缺点
● 策略类数量增多
每一个策略都是一个类,复用的可能性很小,类数量增多。
● 所有的策略类都需要对外暴露
上层模块必须知道有哪些策略,然后才能决定使用哪一个策略,这与迪米特法则是相违背的,我只是想使用了一个策略,我凭什么就要了解这个策略呢?那要你的封装类还有什么意义?
门面模式
门面模式(Facade Pattern)也叫做外观模式,是一种比较常用的封装模式。门面模式注重“统一的对象”,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生,其通用类图
类图就这么简单,但是它代表的意义可是异常复杂,Subsystem Classes是子系统所有类的简称,它可能代表一个类,也可能代表几十个对象的集合。甭管多少对象,我们把这些对象全部圈入子系统的范畴。
再简单地说,门面对象是外界访问子系统内部的唯一通道,不管子系统内部是多么杂乱无章,只要有门面对象在,就可以做到“金玉其外”。
门面模式有如下优点。
● 减少系统的相互依赖
想想看,如果我们不使用门面模式,外界访问直接深入到子系统内部,相互之间是一种强耦合关系,你死我就死,你活我才能活,这样的强依赖是系统设计所不能接受的,门面模式的出现就很好地解决了该问题,所有的依赖都是对门面对象的依赖,与子系统无关。
● 提高了灵活性
依赖减少了,灵活性自然提高了。不管子系统内部如何变化,只要不影响到门面对象,任你自由活动。
● 提高安全性
想让你访问子系统的哪些业务就开通哪些逻辑,不在门面上开通的方法,你休想访问到。
门面模式的缺点
●门面模式最大的缺点就是不符合开闭原则,对修改关闭,对扩展开放,看看我们那个门面对象吧,它可是重中之重,一旦在系统投产后发现有一个小错误,你怎么解决?完全遵从开闭原则,根本没办法解决。继承?覆写?都顶不上用,唯一能做的一件事就是修改门面角色的代码,这个风险相当大,这就需要大家在设计的时候慎之又慎,多思考几遍才会有好收获。
在工厂方法模式中,抽象产品类Product负责定义产品的共性,实现对事物最抽象的定义;Creator为抽象创建类,也就是抽象工厂,具体如何创建产品类是由具体的实现工厂ConcreteCreator完成的。工厂方法模式的变种较多。
责任链模式
项目需求:付款完成后的投放业务,投放业务就是要在这些资源位中展示符合当前用户的资源。在支付成功页筛选活动 banner 和推荐商品。
要求:
- 允许运营人员配置需要展示的资源,以及对资源进行过滤的规则。2. 资源的过滤规则相对灵活多变,这里体现为三点:
过滤规则大部分可重用,但也会有扩展和变更。
不同资源位的过滤规则和过滤顺序是不同的。
同一个资源位由于业务所处的不同阶段,过滤规则可能不同。 - 允许规则实时增减和顺序调整。(使用责任链的核心原因)
过滤规则:
- 用户个人资质是否满足投放业务;
- 用户所在城市是否在业务投放城市;
- 用户近期所购买的产品是否符合业务投放人群;
- 新用户首次购买投放指定业务。
模式定义:使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
责任链模式的核心在“链”上,“链”是由多个处理者ConcreteHandler组成的。
责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。
责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长,环节比较多的时候。
//定义一个抽象的handle
public abstract class Handler {
private Handler nextHandler; //指向下一个处理者
private int level; //处理者能够处理的级别。代表一些扩展的可能性
public Handler(int level) {
this.level = level;
}
public void setNextHandler(Handler handler) {
this.nextHandler = handler;
}
// 抽象方法,子类实现
public abstract void echo(Request request);
}
//客户端实现
class Client {
public static void main(String[] args) {
HandleRuleA handleRuleA = new HandleRuleA(1);
HandleRuleB handleRuleB = new HandleRuleB(2);
handleRuleA.setNextHandler(handleRuleB); //这是重点,将handleA和handleB串起来
handleRuleA.echo(new Request());
}
}
单例模式的优点
● 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
● 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决(在Java EE中采用单例模式时需要注意JVM垃圾回收机制)。
● 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
● 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
单例模式的缺点(其实不算缺点,是个特点)
● 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。单例模式为什么不能增加接口呢?因为接口对单例模式是没有任何意义的,它要求“自行实例化”,并且提供单一实例、接口或抽象类是不可能被实例化的。当然,在特殊情况下,单例模式可以实现接口、被继承等,需要在系统开发中根据环境判断。
单例模式-饿汉式
单例模式-懒汉式
单例模式-双重检查锁
状态模式 + 观察者模式(监听器模式)
项目需求概述:用户从开始下订单,到支付完成,再到物流部进行发货,最终用户确认收货,整个流程涉及到很多订单状态,需要通过代码对订单状态进行管理。除此之外,用户或者物流部门每一次触发的不同操作都有可能改变订单状态。如:用户创建订单操作 导致 订单状态为 待支付状态; 用户支付操作 导致 订单状态为待发货状态;物流部门发货操作 导致 订单状态变为待收货状态;用户确认收货操作 导致 订单状态变为 订单完成状态。
开发任务: 设计整体结算发货及收货的流程。用户创建订单 --> 支付订单 --> 发货–>收货–>订单完成。
要求:
创建订单成功后,订单状态初始化为 待支付。
订单状态包括: 待支付;待发货;待收货;订单完成。(状态模式)
触发订单状态变化的操作:支付订单;发货;确认收货 (观察者模式)
状态模式
● State——抽象状态角色(Enum 完成)
接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。
● ConcreteState——具体状态角色 (状态变化 Enum)
每一个具体状态必须完成两个职责:本状态的行为管理以及趋向状态处理,通俗地说,就是本状态下要做的事情,以及本状态如何过渡到其他状态。
● Context——环境角色 (用户触发 – 》 controller + service)
定义客户端需要的接口,并且负责具体状态的切换。
如果结合到我们的需求上,我们需要定义四个方法在State里,craete,pay,send,receive
定义四个 具体的状态子类。 createS,payS,sendS,receiveS。每一
个子类都需要 Override我们的四个方法 (craete,pay,send,receive)
这四个方法我们需要分别进行实现。
观察者模式
观察者模式(Observer Pattern)也叫做发布订阅模式(Publish/subscribe)
● Subject被观察者
定义被观察者必须实现的职责,它必须能够动态地增加、取消观察者。它一般是抽象类或者是实现类,仅仅完成作为被观察者必须实现的职责:管理观察者并通知观察者。
● ConcreteSubject具体的被观察者
定义被观察者自己的业务逻辑,同时定义对哪些事件进行通知。
====================================================
● Observer观察者
观察者接收到消息后,即进行update(更新方法)操作,对接收到的信息进行处理。
● ConcreteObserver具体的观察者
部分商品支付完成更新平台币、红包发放等后续业务。装饰者模式 + 享元模式(优化对象的频繁创建问题)
项目需求:部分推销商品付款完成后,需要平台对当前用户的平台币进行更新,如淘宝的淘金币,京东商城的京豆等。
要求:
- 平台币的更新和红包发放业务为附属功能,不能影响主支付业务逻辑。
- 平台币的更新和红包发放业务只对部分推广商品有效,且依赖于商品的属性变更。商品属性变更,如需取消平台币更新和发放红包业务,不可修改代码。要做到在线实时热变更。
- 调用层无需关心该商品是否需要更新平台币或者发放红包。做到与上层调用者的完全解耦。
- 该逻辑为支付的附属业务,随着支付完成立即触发。但不可影响支付主逻辑。
装饰者模式
● Component抽象构件
Component是一个接口或者是抽象类,就是定义我们最核心的对象,也就是最原始的对象
● ConcreteComponent 具体构件
ConcreteComponent是最核心、最原始、最基本的接口或抽象类的实现,你要装饰的就是它。
● Decorator装饰角色
一般是一个抽象类,做什么用呢?实现接口或者抽象方法,它里面可不一定有抽象的方法呀,在它的属性里必然有一个private变量指向Component抽象构件。
● 具体装饰角色
ConcreteDecoratorA和ConcreteDecoratorB是两个具体的装饰类,你要把你最核心的、最原始的、最基本的东西装饰成其他东西。
享元模式(享元工厂模式)
享元模式(Flyweight Pattern)是池技术的重要实现方式,使用共享对象可有效地支持大量的细粒度的对象。
要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。
● 内部状态
内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变
● 外部状态
外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态
一般情况下,我们会将可变部分和不可变部分放到一起。因为我们最终享元的是一整个对象,既然是一整个对象,我们能够控制共享部分和不可共享部分,那么我们何必新增一个额外的类呢?
申请电子发票(企业+个人)
建造者 + 原型(JDK自己有了)
项目需求:用户支付完成后,部分企业用户或个人账户需要开电子增值税发票,实现该功能。
要求:
-
由于电子增值税发票所需内容较多,而且随国家政策可能会有所修改,发票内容的添加删除及后台对象的组装尽量做到灵活。
-
发票开具不是高并发访问接口且无法缓存,尽量保证发票创建的性能。
建造者模式
建造者模式(Builder Pattern)也叫做生成器模式。将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
● Product产品类 (电子发票 个人+企业)
● Builder抽象建造者 (创建产品的抽象类)
规范产品的组建,一般是由子类实现
● ConcreteBuilder具体建造者 (真实创建发票的实现类 个人+企业)
实现抽象类定义的所有方法,并且返回一个组建好的对象。
● Director导演类 (调用端,1.直接用service 层作为导演类; 2 如果你想要对这个建造模块进行一个非常好的封装,那么就单独创建一个导演类。然后service 调用这个导演类即可)
负责安排已有模块的顺序,然后告诉Builder开始建造。 对于导演类,要看我们项目的需求复杂程度,如果service层面只有创建的逻辑,那么就无需导演类了,如果还有其他很多相关的逻辑,就做一个导演类。
原型模式
原型模式(Prototype Pattern)的简单程度仅次于单例模式。
原型模式的核心是一个clone方法,通过该方法进行对象的拷贝,Java提供了一个Cloneable接口来标示这个对象是可拷贝的
记录审计日志: 模板方法模式
项目需求:公司 财务、审计、法务部门需要记录用户关键核心日志,防止日后未知的纠纷。
要求:
-
该日志记录不是普通的日志记录,不同的日志记录内容可能不一样
-
可以灵活增加日志种类和处理;
-
核心日志包括: 登录; 订单创建; 订单支付。其中订单创建需要有相关产品信息;订单支付需要有相关产品信息以及支付方式和支付金额。
-
日志组装完成后,将日志信息发送到 queue中,会由数据处理部门进行处理。
模板方法模式
模板方法模式确实非常简单,仅仅使用了Java的继承机制,但它是一个应用非常广泛的模式。其中,AbstractClass叫做抽象模板,它的方法分为两类:
● 基本方法
基本方法也叫做基本操作,是由子类实现的方法,并且在模板方法被调用。
● 模板方法
可以有一个或几个,一般是一个具体方法,也就是一个框架,实现对基本方法的调度,完成固定的逻辑。
注意 为了防止恶意的操作,一般模板方法都加上final关键字,不允许被覆写。
ElasticSearch 数据查询 - Scroll: 迭代器模式
项目需求:数据从mysql迁移至Es,Es 数据查询的默认 fetchSize 最大为 10000. 如果查询超过 10000 条数据,需要通过 scroll形式进行查询。
要求:
-
出于安全问题考虑,查询需要直连 ES-ip:9200, 不可使用第三方Jar 包。
-
由于目前项目的查询方式是基于mysql的,为了减少改动,暂时使用SQL 语句查询,ES-IP:9200/_sql
-
我们需要将结果以 stream的形式进行返回。(避免我们的内存占用过大以及瞬时的网络带宽问题。)
迭代器模式
● Iterator抽象迭代器
抽象迭代器负责定义访问和遍历元素的接口
● ConcreteIterator具体迭代器
具体迭代器角色要实现迭代器接口,完成容器元素的遍历。
● Aggregate抽象容器
容器角色负责提供创建具体迭代器角色的接口,必然提供一个类似createIterator()这样的方法,在Java中一般是iterator()方法。
● Concrete Aggregate具体容器
具体容器实现容器接口定义的方法
多种类第三方账号登录 - 桥接模式
项目需求:为了简化用户的登录注册流程,允许用户直接使用经过授权的第三方账号登录网站。
要求:
- 遵循开闭原则:可以新增抽象部分和实现部分,且它们之间不会互相影响。
2.遵循单一职责原则:抽象部分专注于处理高层逻辑,实现部分处理平台细节。
第三方账号登录原理 简述
自建账号体系的注册和登录,对用户来讲,过程很繁琐,结果 很多用户并不想注册你开发的网站或 APP, 所以用户量增长缓慢。此时可考虑用第三方账号登录,比如微信登录和 QQ 登录。
当用户点击第三方登录时,会跳转到第三方登录 SDK 内部;用户输入第三方登录用户名或密码,有些第三方登录平台,可以直接调用已经登录的账号,例如:QQ;完成第三方平台登录的;登录完成后,第三方平台,或者 SDK 会回调我们的应用,在回调的信息里面,可以拿到用户在第三方平台的 OpenId,以及昵称,头像等信息。
桥接模式
桥接模式,是一个比较简单的模式。
● Abstraction——抽象化角色
它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。
● Implementor——实现化角色
它是接口或者抽象类,定义角色必需的行为和属性。
● RefinedAbstraction——修正抽象化角色
它引用实现化角色对抽象化角色进行修正。
● ConcreteImplementor——具体实现化角色
它实现接口或抽象类定义的方法和属性。
桥梁模式的优点
● 抽象和实现分离
● 优秀的扩充能力
● 实现细节对客户透明
多种类第三方账号登录 - 适配器模式(不推荐在项目中使用适配器)
项目需求:为了简化用户的登录注册流程,允许用户直接使用经过授权的第三方账号登录网站。
要求:
- 遵循开闭原则:可以新增抽象部分和实现部分,且它们之间不会互相影响。
2.遵循单一职责原则:抽象部分专注于处理高层逻辑,实现部分处理平台细节。
适配器模式
● Target目标角色
该角色定义把其他类转换为何种接口,也就是我们的期望接口,例子中的IUserInfo接口就是目标角色。
● Adaptee源角色
你想把谁转换成目标角色,这个“谁”就是源角色,它是已经存在的、运行良好的类或对象,经过适配器角色的包装,它会成为一个崭新、靓丽的角色。
● Adapter适配器角色
只有当项目中没有时间做新的接口的时候,才临时使用适配器。适配器这个东西就是为补救应急而生的。
比如说,我们有一个接口,传入两个参数,name,age。现在这个接口发生了改变,需要传 3 个参数,name,age,sex。这个时候我们做一个适配器。Adapter里边多加一个参数,多组装一个新的bean。
商品多级分类目录 - 组合模式 + 访问者模式
项目需求:目前商城中有很多商品目录,且层级很多。为了对层级目录进行管理,需要满足对层级目录的增加和删除。
要求:
- 层及目录是保存在 DB 中的,项目一旦初始化,需要将层及目录设置为超热点缓存。
2.支持在线对层级目录的增删。
-
前端获取一次层及目录后,每隔24小时对层级目录进行后台重新获取。
-
层及目录更新需要先更新 redis 缓存,再更新DB。后台层及目录缓存应该为永不过期缓存。
组合模式
● Component抽象构件角色
定义参加组合对象的共有方法和属性
● Leaf叶子构件
叶子对象,其下再也没有其他的分支,也就是遍历的最小单位。
● Composite树枝构件
树枝对象,它的作用是组合树枝节点和叶子节点形成一个树形结构。
访问者模式
表示一个作用于某对象结构中的各个(层级啊)元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
● Visitor——抽象访问者 (ItemVisitor)
抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。
● ConcreteVisitor——具体访问者
它影响访问者访问到一个类后该怎么干,要做什么事情。
● Element——抽象元素 (AbstractProductItem)
接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。
● ConcreteElement——具体元素 (ProductItem)
实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。
● ObjectStruture——结构对象(ProductItem)
代理模式 与 装饰器模式
对于两个模式,首先要说的是,装饰模式就是代理模式的一个特殊应用,两者的共同点是都具有相同的接口,不同点则是代理模式着重对代理过程的控制,而装饰模式则是对类的功能进行加强或减弱,它着重类的功能变化
中介者模式 与 门面模式
门面模式为复杂的子系统提供一个统一的访问界面,它定义的是一个高层接口,该接口使得子系统更加容易使用,避免外部模块深入到子系统内部而产生与子系统内部细节耦合的问题。中介者模式使用一个中介对象来封装一系列同事对象的交互行为,它使各对象之间不再显式地引用,从而使其耦合松散,建立一个可扩展的应用架构。
命令模式 与 策略模式
命令模式和策略模式的类图确实很相似,只是命令模式多了一个接收者(Receiver)角色。
策略模式的意图是封装算法,它认为“算法”已经是一个完整的、不可拆分的原子业务;
而命令模式则是对动作的解耦,把一个动作的执行分为执行对象(接收者角色)、执行行为(命令角色),让两者相互独立而不相互影响。
备忘录模式
备忘录模式就是一个对象的备份模式,提供了一种程序数据的备份方法
● 需要保存和恢复数据的相关状态场景。
● 提供一个可回滚(rollback)的操作
● 需要监控的副本场景中。
● 数据库连接的事务管理就是用的备忘录模式