设计模式——个人理解

懵懂的女人 提交于 2019-12-03 21:15:22

写在前面

设计模式是基于固定设计场景的一套解决方案,目的是为了完成开闭原则,或者说面向后续的需求变更,以成本最低的改动和测试完成新功能。同时设计模式也是一种程序员之间的交流语言,可以省去很多沟通成本。

在工作的过程中,从最初不知如何使用设计模式,到中间刻意使用设计模式,到今天稍有好转的正确的使用设计模式,踩了许多坑,也有过度设计的时候,为了避免今后在犯同样的错误,所以准备将自己在项目实践的过程中使用过的设计模式记录下来。

设计模式中,有一些实战里用过,有的没用过,没用过的应该是对这个设计方法没有领悟透彻,所以不知道该怎么用。

目前先按照自己的理解记录下来,有更深的领悟会更新。

创建型设计模式

1、工厂方法模式(FactoryMethod)

工厂方法模式的实质是“定义一个创建对象的接口,但让实现这个接口的类来决定实例化哪个类。工厂方法让类的实例化推迟到子类中进行。”

工厂方法模式,是在抽象类中定义创建对象的方法,调用声明创建过程(抽象方法,但创建对象的过程的实现是在实现类中定义。

也就是说,在创建对象时,创建步骤是固定的,但各个步骤的实现是多种多样的,这种情况下可以使用工厂方法。

例如:创建一个电脑主机,需要安装CPU,内存,硬盘,显卡。这个安装步骤是固定的,但是由不同的工厂实现类去实现这几个步骤。

这种父类声明,子类实现的方式有很多,把这种方法用于创建对象,就叫工厂方法,如果把这种方法用于一套算法的实现,就叫模版方法。

总结一下个人理解,目标是创建一个对象。创建对象有很多步骤,步骤的顺序是固定的,但是步骤的实现是多种多样,这种情况下在父类定义步骤顺序,在子类完成步骤实现。

2、抽象工厂模式(AbstractFactory)

抽象工厂模式提供了一种方式,可以将一组具有同一主题的单独的工厂封装起来。

我理解的抽象工厂,是指要创建的产品的纬度更复杂,例如产品士兵,勋章,帽子,衣服,鞋子,这些产品有指定纬度的组合:

  勋章 帽子 衣服 鞋子
见习士兵 见习 见习 见习 见习
正式士兵 正式 正式 正式 正式
高级士兵 高级 高级 高级 高级

这种情况,可以简历三个工厂(见习士兵工厂、正式士兵工厂、高级士兵工厂),每个工厂生产士兵类型对应的勋章,帽子,衣服和鞋子。

抽象工厂用于业务复杂的对象创建,每当产品线有变化,就需要创建或修改工厂。

例如:添加武器这个产品

  勋章 帽子 衣服 鞋子 武器
见习士兵 见习 见习 见习 见习 见习
正式士兵 正式 正式 正式 正式 正式
高级士兵 高级 高级 高级 高级 高级

就需要修改三个工厂,加入武器创建功能。

例如:添加一个士兵类型

  勋章 帽子 衣服 鞋子 武器
见习士兵 见习 见习 见习 见习 见习
正式士兵 正式 正式 正式 正式 正式
高级士兵 高级 高级 高级 高级 高级
将军 将军 将军 将军 将军 将军

则需要新建一个将军的工厂。

3、单例模式(Singleton)

单例对象的类必须保证只有一个实例存在。

单例模式很常用,自己写不是很复杂,如果使用Spring框架,默认情况下Bean都是单例模式。

在单例模式下的类,要注意线程安全问题。

单例模式的实现方式如下表:

名称 懒加载 线程安全 实现难度 备注
懒汉式A型 简单 判断如果是null就new一个
懒汉式B型 简单 方法上加synchronized,有可能线程安全
饿汉式 简单 类加载时就会初始化
双检锁 中等 判断null,加锁,在判断null
内部类 中等  
枚举 简单 JDK1.5后支持

4、建造者模式(Builder)

建造者模式,也叫生成器模式,是一种对象构建模式。它可以将复杂对象的建造过程抽象出来(抽象类别),使这个抽象过程的不同实现方法可以构造出不同表现(属性)的对象。

可以理解为,建造者着重于一步步构造一个复杂对象,并且有些构造的步骤是可以省略的。

例如:定义一个HttpResult对象,它有响应的code,message,body等属性,那么使用建造者模式创建一个HttpResult应该可以是:

return new HttpResult.Builder<T>(DefaultHttpResultStatus.SUCCESS).msg(msg).body(body).build();

先创建一个建造者,然后一步一步构造对象的各个部分,最后一次性创建对象。

5、原型模式(Prototype)

通过“复制”一个已经存在的实例来返回新的实例,而不是新建实例。被复制的实例就是我们所称的“原型”,这个原型是可定制的。

用于创建复杂的或者耗时的实例,因为这种情况下,复制一个已经存在的实例使程序运行更高效;

网上有一个例子是说发送邮件,发送1000万封,如果用new来创建,每次都要创建一个邮件对象,然后赋值属性。

如果用原型模式,通过一个对象,克隆出一个新对象,只修改两个对象中不同的部分就可以了。

原型模式在Java中大多通过Object对象的clone方法实现,这里要注意的是clone对象是浅克隆,如果要深克隆,最好用序列化的方式做。

结构型设计模式

6、适配器模式(Adapter)

有时候也称包装样式或者包装(wrapper)。将一个类的接口转接成用户所期待的。一个适配使得因接口不兼容而不能在一起工作的类能在一起工作,做法是将类自己的接口包裹在一个已存在的类中。

在不改变被适配的对象的代码的前提下,加入需要的功能。适配器模式常用于软件生命周期的维护阶段,大多是因为已经开发的功能不想再改动,或者没有源码可以修改了。

适配器大致上有两种实现方法:继承方式和委托方式。

继承方式,适配器实现被适配的接口,提供被适配对象的所有功能。

委托方式,如果被适配器没有接口,那么可以在适配器对象中创建一个被适配器对象,完成它的功能。

7、桥接模式(Bridge)

桥接模式是设计模式中最复杂的模式之一,它把事物对象和其具体行为、具体特征分离开来,使它们可以各自独立的变化。

概念上,桥接模式要把对象的具体行为和具体特性分离开,要理解桥接模式,首先要理解什么是具体行为和具体特征。说其复杂,主要就是因为它有两个纬度,添加了一个纬度,就增加了一倍的复杂性。

在书中把具体行为和具体特征换了一种比较好理解的描述方式,叫类的功能层次和类的实现层次。

层次通常是指继承或者实现。

功能层次怎么理解呢?比如有一个用户管理类(customerService),有CURD的功能,我们想在这个用户管理类上扩展一个新功能,如批量导入,做法是可以创建一个新的Service类,继承customerService,在这个新类中开发批量导入的功能,这种方式就是功能层次的扩展。

实现层次怎么理解呢?以批量导入这个功能为例,我们定义一个导入类(importService),对于excel导入有一个实现,json导入有一个实现,可以通过实现接口或者继承抽象类来完成。这种方式就是实现层次的扩展。

个人理解,桥接模式就是把功能层次和实现层次分离开来,然后通过一个方式链接到一起。

以上面的customerService为例,功能扩展是一套功能层级,对于导入则单独管理一套导入的实现层次,然后功能类引用导入类完成业务。

注意是找到功能扩展和实现扩展的那个点,这个点是否合理,就要根据经验和对需求的理解了。

8、组合模式(Composite)

组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。

组合模式从结构上看,适用于实现类似树的结构的对象关系。

应用该模式的目的封装复杂的类型关系,假如树有3层,每层都是不同的类型,那么使用组合模式设计,调用者就可以不需要知道这三层究竟是什么类型了。

这么说比较抽象,举个例子来说就是操作系统的文件管理,简单说文件管理分为具体文件和文件夹,对我们使用操作系统的人来说,这俩个对象都是一个文件。

在比如说,公司的组织机构,大一点的机构可以从各个工作中心往下划分,一直小组,例如技术中心—产品研发部—架构部—核心架构师小组,这些广义上讲都是部门,在平时工作中都会问你在哪一个部门(这就是封装了复杂性),没有人会问你在哪个中心的哪个部门下的哪个小组。

总的来说,如果要使用组合模式,可以在下列场景中使用:

  • 需要完成有层次结构的需求,但想通过一种方式忽略整体与部分的差异(在某些时刻不区分子节点与父节点),可以一致地对待它们;
  • 让调用者能够忽略不同对象层次的变化,调用者无须关心对象层次结构的细节。

组合模式虽然能够非常好地处理层次结构,也使得客户端程序变得简单。

9、装饰器模式(Decorator)

装饰器模式,是OOP领域中,一种动态地往一个类中添加新的行为的设计模式。就功能而言,修饰模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

从装饰器模式的定义和类图中,可以得出一个结论,装饰模式是给类添加新行为的一种设计模式,但是为什么说比生成子类更灵活?具体该怎么做?

举个网上描述装饰器模式的例子 —— 咖啡厅。

比如有5种咖啡,每种咖啡有有4种做法,如果不用装饰器模式,而用类的实现的方式就会有5 * 4 = 20个类,每个类代表一种做法的咖啡。这种情况被称之为类爆炸。

如果使用装饰器模式,那么定义一个咖啡的父类,5个咖啡类,一个装饰器父类,4个做法类,那么只有1 + 5 + 1 + 4 = 11个类。而且各种做法之间还可以组合。

Beverage beverage = new Mocha(new Milk(new CoffeeB()));

装饰器模式也是一种包装方法(wapper),也就是说装饰器与被装饰对象都有同一个父类,这里跟适配器模式的做法是一样的。但是两者之前的区别,从行为意义上来讲:

适配器模式,是将一个接口转换为一个新接口,被适配后的对象,意义上不再是那个对象。

装饰器模式,被装饰的对象从意义上,还是原来的对象。

10、门面模式(Facade)

为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。

门面模式也叫外观模式。为了降低调用者的调用复杂度,提供一个facade对象,封装一个业务的一系列调用。

门面模式算是比较简单的模式之一,我们在开发中也大量的使用到了这个模式,比如微服务架构下,消费者编排调用多个提供者完成业务,那么这个编排的过程就是门面模式封装调用的过程。

比如家里有许多智能家居,电灯、音响等等,在独处时要把灯光调暗,放一些轻柔的音乐,音量不要太大。在开派对时需要把灯光调成霓虹模式,然后放一些比较动感的音乐。每次切换模式又要调整灯光,又要调整音响,很麻烦。

这时就可以提供一个门面模式,定义一个独处场景,一个派对场景,或者其他场景,然后把要做的事情封装到具体的场景里就可以了。

11、享元模式(Flyweight)

它使用共享物件,用来尽可能减少内存使用量以及分享资讯给尽可能多的相似物件;它适合用于当大量物件只是重复因而导致无法令人接受的使用大量内存。通常物件中的部分状态是可以分享。常见做法是把它们放在外部数据结构,当需要使用时再将它们传递给享元。

享元模式的核心细想是:如果在一个系统中存在多个相同的对象,那么只需要共享一份对象的拷贝,而不必为每一次使用都创建新的对象。

享元模式想做到的是大量细颗粒对象的重用,以节省内存资源。比如:表情包,一些素材,字符等等。最终效果,就是实现一个对象池。

同时它也是少数几个以提高系统性能为目的的模式之一。

基本做法就是提供享元对象的父类和实现类,在提供一个享元工厂,工厂内使用容器来保存已经存在的对象,如果容器内不存在则new一个放进去。

这个模式和单例模式很像,区别是从使用场景上来定义的,首先享元模式是结构性,单例模式是创建型,其次享元模式维护了多个对象,单例模式是针对与一个对象。

总的来说,享元模式是一个使用门槛低,使用频率高的模式。

12、代理模式(Proxy)

个类别可以作为其它东西的接口。

代理模式是常见的设计模式之一。

所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。
为什么要采用这种间接的形式来调用对象呢?一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。

代理模式的做法跟适配器、装饰者类似:

适配器:适配器,被适配者,继承一个父类;

装饰者:装饰器,被装饰者,继承一个父类;

代理(静态代理):代理者,被代理者,继承一个父类;

Java中的代理还分三种:

静态代理:在不修改目标对象的功能前提下,对目标功能扩展;

动态代理:在运行期,通过反射机制创建一个实现了一组给定接口的新类;

Cglib代理:在内存中构建一个子类对象从而实现对目标对象功能的扩展;

三种代理模式如何实现,就不在这里展开了。但从做法上感觉和装饰器模式类似,它们之间的区别在目标定位上。

装饰器:是想给对象添加新行为;

代理器:是想控制访问;

基于这个定位,如果你想在不改变目标对象的前提下添加新行为,就用装饰器模式。

如果你想在不改变目标对象的前提下,控制访问,就使用代理模式;

行为型设计模式

13、责任链模式(Chain of Responsibility)

使多个对象都有机会处理请求,从而避免了请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

举个例子,普通员工请假要有部门领导审批,中心总监审批,老板审批。不同员工请假就是一个请求,请求会先到部门领导审批类,如果不通过就驳回,如果通过了就向下传递请求。

从代码层面,责任链模式减少了嵌套if,或者if...elseif...else,或者switch这种语法。

但从设计角度,责任链最大的优点就在于它弱化了发出请求的人和处理请求的人之间的关系。

在使用责任链之前,先把责任链的调用顺序配置好,然后将请求发送到责任链即可。

14、命令模式(Command)

尝试以对象来代表实际行动。命令对象可以把行动(action) 及其参数封装起来。

命令模式把一个请求或者操作封装到一个对象中。命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能。

命令模式用来封装请求,命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象。

每一个命令都是一个操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口。

简单来说命令模式就是字面意思,如果在业务中有一些请求是以命令来描述场景的,这时可以考虑使用命令模式。

例如:一些智能设备的远程控制命令,就可以用命令模式去设计,比如开灯,关灯,设置空调风速之类的,还可以对每个命令添加撤回接口。

15、解释器模式(Interpreter)

给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

这个模式使用的场景不多,常用于处理表达式的业务,这个模式聚焦点很明确。

例如有一个需求,要做一个大楼耗电费用的公式,公式是各楼层的用电量相加,乘以电费,这种时候就可以用解释器模式。在或者做一个简单的加减乘除计算器,也可以用解释器模式。

解释器模式通常有一个抽象类,一些终结解释器和一些非终结解释器。非终结解释器可以接收其他解释器,并可以递归调用。

具体的Demo可以在网上搜索RPN的解释器模式实现。

16、迭代器模式(Iterator)

是一种最简单也最常见的设计模式。它可以让用户透过特定的接口巡访容器中的每一个元素而不用了解底层的实现。

这个模式太常见了,在Java中我们经常使用迭代器。

迭代器是为了封装对象内部的数据集合的遍历工作,如果你不想把对象内部的数据结构暴露给调用者,则可以使用这种设计模式。

你也可以继承Java提供的Iterator。另外,自己做的迭代器模式也可以选择从后向前遍历等方式。

17、中介者模式(Mediator)

定义中介者对象,该对象封装了系统中对象间的交互方式。 由于它可以在运行时改变程序的行为。

中介者模式使用的场景是,将网状的调用关系通过中介者模式解耦,然后通过终结者来编排调用。

适用于多个对象之间调用关系很复杂的时候,多大是网状或星状结构,这种情况下使用中介者,可以让各个对象之间解耦,大家都只于中介者交互。

做法是提供一个中介者的父类和实现类,把具体要进行业务操作的对象注册进来,业务操作的对象也叫同事类。同时,在每个同事类中也保存着中介者。客户端消费时,进行双向注册,因为同事类中保存着中介者,所以由主动发起业务的同事类,触发中介者调用。

总而言之,在发生复杂的对象调用关系时,可以考虑使用中介者模式。

18、备忘录模式(Memento)

在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。

备忘录模式的实现很简单:发起人可以创建备忘录和更新备忘录,备忘录提供状态管理功能,管理者可以控制备忘录的内容。

说起来比较绕,直接看代码:

        Originator originator = new Originator();
        originator.setState("状态1");
        System.out.println("初始状态:"+originator.getState());

        Caretaker caretaker = new Caretaker();
        caretaker.setMemento(originator.createMemento());

        originator.setState("状态2");
        System.out.println("改变后状态:"+originator.getState());
        
        originator.restoreMemento(caretaker.getMemento());
        System.out.println("恢复后状态:"+originator.getState());
初始状态:状态1
改变后状态:状态2
恢复后状态:状态1

总而言之,当你想管理一个对象的状态,并提供回滚等操作时,可以选择该模式。

19、观察者模式(Observer)

一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

观察者模式使用的场景是,当你有一个业务发生时,想批量通知多处(多个对象),则可以使用该模式。

举个例子,汽车会想服务器上传当前位置,服务端接收到当前位置进行一些报警判断,比如围栏报警,移动报警等等,这个把报警注册到报警主题,然后位置数据上报后统一通知。

做法是提供主题父类,主题实现类,观察者类和观察者类的实现。将观察者注册到主题中,当主题发生业务时,循环调用观察者的方法。

这种做法的好处是可以快速上线下线观察者,面向后续的需求变更做到快速实现。

20、状态模式(State)

创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。

状态模式很简单,使用场景是把对象在各种状态下的操作封装到状态对象中。

例如,一个用户在登录的时,由于用户的状态状态不同,登录的场景不同,假如用户是正常状态,则可以登录,但如果用户是冻结状态,则登录时提示冻结,如果用户是黑名单状态,登录的时候会提示在账号被禁用等等。

做法就是把不同状态下的操作封装到状态对象中。

状态模式与策略不是在做法上差不多,但是应用场景不同,状态模式是处理不同状态的业务,策略模式是处理不同的算法策略。是语意上的区别。

21、策略模式(Strategy)

指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。

策略模式很简单,使用场景是把一些有不同实现的算法(或者业务逻辑)封装成接口。

例如用户注册的时候给用户加密,那么可以定义一个接口叫加密策略,然后实现这个接口用作加密。

策略模式的场景就是封装算法,后续算法变更的时候不会影响到很大的范围。

22、模板方法模式(TemplateMethod)

决定这些抽象方法的执行顺序,这些抽象方法的实作由子类别负责,并且子类别不允许覆写模板方法。

模板方法非常简单,也特别常用。它能将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中,并且通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为。

跟工厂方法的做法基本相似,只不过场景不同,工厂方法是用来创建对象,而模板方法是用来执行算法。

23、访问者模式(Visitor)

是一种将算法与对象结构分离的设计模式。

访问者模式可能是行为类模式中最复杂的一种模式了,掌握它对理解设计模式会更进一步。

如果系统的数据结构是比较稳定的,但其操作(算法)是易于变化的,那么使用访问者模式是个不错的选择;如果数据结构是易于变化的,则不适合使用访问者模式。

访问者模式一共有五种角色:

(1) Vistor(抽象访问者):为该对象结构中具体元素角色声明一个访问操作接口。

(2) ConcreteVisitor(具体访问者):每个具体访问者都实现了Vistor中定义的操作。

(3) Element(抽象元素):定义了一个accept操作,以Visitor作为参数。

(4) ConcreteElement(具体元素):实现了Element中的accept()方法,调用Vistor的访问方法以便完成对一个元素的操作。

(5) ObjectStructure(对象结构):可以是组合模式,也可以是集合;能够枚举它包含的元素;提供一个接口,允许Vistor访问它的元素。

访问者模式的基本做法是,软件系统中拥有一个由许多对象构成的、比较稳定的对象结构,这些对象的类都拥有一个 accept 方法用来接受访问者对象的访问。访问者是一个接口,它拥有一个 visit 方法,这个方法对访问到的对象结构中不同类型的元素做出不同的处理。在对象结构的一次访问过程中,我们遍历整个对象结构,对每一个元素都实施 accept 方法,在每一个元素的 accept 方法中会调用访问者的 visit 方法,从而使访问者得以处理对象结构的每一个元素,我们可以针对对象结构设计不同的访问者类来完成不同的操作,达到区别对待的效果。 

从另一个角度看,对象结构对象中保存着Element对象的集合,并提供一个接收Vistor的入口,随时调用这个Element的accept操作,而Element的accept方法的参数是Vistor对象,这次是方法参数级别的关联,在方法中调用Vistor对象的visit对象,再把自己当作参数传递给visit方法。最后在visit方法中对传递过来的Element对象进行操作。

说起来比较繁琐,看一下代码块:

// 数据结构
public class Car {
    private List<Visitable> visit = new ArrayList<>();

    public void addVisit(Visitable visitable) {
        visit.add(visitable);
    }

    // 传入visitor
    public void show(Visitor visitor) {
        for (Visitable visitable : visit) {
            visitable.accept(visitor);
        }
    }
}

这里car是一个数据结构,里面有一个visit集合保存着Element,然后提供一个show方法循环list,调用其accept方法,把visitor穿进去。

    public static void main(String[] args) {
        // 数据结构
        Car car = new Car();
        car.addVisit(new Body());   // 添加Element
        car.addVisit(new Engine()); // 添加Element
        // 创建Visitor
        Visitor print = new PrintCar();
        car.show(print);
        Visitor check = new CheckCar();
        car.show(check);
    }

Client测先注册Element到数据结构,然后实例化不同的访问者,调用show方法。

至于Element和Visitor如上图所示。

总而言之,使用访问者模式的场景是,把现在已经稳定的数据结构,和对数据结构的操作进行解耦,就可以使用访问者模式。

以后的操作变更只修改访问者类就好,不需要在对数据结构的类进行修改了,最终要求其符合开闭原则。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!