最近在学习《设计模式》,学校里没有开过这门课,也没听老师怎么提起过,并不了解它是作什么用的,想着以后用到了再学。因为马上要找工作了,想着准备准备,否则到时面试官让你写个设计模式,你说没学过,岂不很尴尬。但看过之后,我只能说相见恨晚。
我曾一度怀疑自己不适合编程,因为如果要我实现稍复杂的业务功能,根本不知道该怎么写或者布局哪些类和方法,看别人的代码脑子也会乱掉。现在明白了,很大程度上是因为我对“设计模式”太无知,不知道编程原来都是有“套路”的。不掌握这些套路,在程序员这条路上会走的很艰难。
因为自己本身搞的是Android开发,最终目的是可以将设计模式与Android各方面(开发、系统源码解读)结合起来,不过由于目前经验有限,所以这些工作还得一步步来,遇到的时候就做一个总结。
这篇文章主要介绍常见的几大模式,日后会逐渐更新添加,然后对每一个模式的要点做一个梳理,并根据模式思想,写一个代码示例。这里不会对每一个模式进行详细介绍,因为这些内容看书是最系统、最易理解和掌握的。我的博文可以作为刚入门的读者的一个辅助,重点是理清思路,能够用代码表示出来,有不正确的地方,还望批评指正。
单例模式
定义
确保一个类只有一个实例,而且自行实例化,并提供一个全局访问点。
1)确保只有一个实例:这个是单例模式所要实现的目的,没什么可说的,从它的名字就可以看出。
2)自行实例化:实例化只能通过new来实现,自行实例化就是只能在类内部进行new操作,否则没有别的办法来确保单例。
3)提供一个全局的访问点:因为我们是在类内部进行的实例化,如果其内部再不提供访问点,那这个类就没法使用了呀。
代码示例
单例模式的实现方式有很多种,比如懒汉模式、饿汉模式、DCL模式、静态内部类模式等。每一种方法都能够实现上面我们所定义的内容,那怎么还会有这么多的方法呢?区别就在于,在一些性能、安全等细节上,它们的处理是不同的,有的有考虑性能,有的则完全没有考虑。主要有这么几个细节问题:
1)线程安全:在多线程的情况下,要确保进行实例化的方法只有一个线程在执行;
2)延迟实例化:意思就是说当我们要用到这个类的时候,再进行实例化操作,而不是像静态成员那样,在类加载的时候就进行了实例化操作,如果永远不去使用,会造成内存的浪费。
3)性能开销要小:这个主要是针对synchronized关键字来说的,如果一个方法被声明为synchronized的,它的性能消耗是要大于普通方法的,如果要该方法频繁调用的话,性能消耗会比较大。
所以基于以上原则,才出现了不同的方法来实现单例模式,有的遵守了上面的原则,有的则没有。我下面使用“静态内部类”的方式来举例实现单例模式,因为它比较好的照顾到了上面三个原则。
工厂方法模式
定义
定义一个创建对象的接口,但由子类决定要实例化哪一个类。
单从定义来看,这个定义只是陈述了一个事实:由子类决定要实例化哪一个类。但并没有体现出这个模式想达成的目的是什么,下面,我们先来用代码示例,然后进行一个总结。
代码示例
工厂模式包含如下四个角色:
- Product:抽象产品
- ConcreateProduct:具体产品
- Factory:抽象工厂
- ConcreateFactory:具体工厂
总结
结合上面的UML图和代码可以看出,使用工厂方法模式:
1)用户只需要关心所需产品对应的工厂,无需关心创建细节,所有的细节都封装在了具体工厂类内部,甚至无需知道具体产品类的类名,实现了将客户从具体产品类中解耦。
2)在系统中加入新产品时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了。这样就符合了开闭原则,即:对修改关闭,对扩展开放。
观察者模式
定义
定义了对象之间一对多的依赖,每当一个对象改变状态时,则所有依赖于它的对象都会得到通知并被自动更新。
一对多:这个“一”就是被观察者,“多”就是指有多个观察者。当这一个被观察者的状态改变时,则所有的观察者都会得到通知并做出响应。
代码示例
观察者模式包含如下四类角色:
- Subject:抽象主题,也就是被观察(Observable)的角色。
- ConcreateSunject:具体主题,也就是具体被观察者
- Observer:抽象观察者,是观察者的抽象类
- ConcreateObserver:具体观察者
|
|
总结
1)观察者模式的一个重要作用就是解耦,将观察者和被观察者解耦,使得它们之间依赖性更小,甚至做到毫无依赖;
2)观察者模式可以实现表示层和数据逻辑层的分离,并提供了稳定的消息更新传递机制,使得可以有各种各样不同的表示层作为具体的观察者角色。
3)很明显,观察者模式符合“开闭原则”。
装饰者模式
定义
动态的给一个对象添加一些额外的职责。就增加功能来说,装饰者模式相比生成子类更为灵活。
从定义可以看出,装饰者模式的目的就是:在不需要创造更多子类的情况下,将对象的功能加以扩展。
代码示例
装饰者模式包含如下四类角色:
- Component:抽象组件。
- ConcreteComponent:组件具体实现类。
- Decorator:抽象装饰者,其内部一定要有一个指向组件对象的引用。
- ConcreteDecorator:装饰者具体实现类。
总结
1)装饰者模式和继承的目的都是要扩展对象的功能,但是装饰者模式有更高的灵活性;
2)通过使用不同的具体装饰类以及这些装饰类的排列组合,可以创造出很多不同行为的组合,得到功能更为强大的对象。
命令模式
定义
将一个请求封装成对象,从而让用户使用不同的请求、队列或者日志来参数化其它对象。命令模式也支持可撤销的操作。
这个定义我只读懂了第一句话,后面的没太看懂,不过对这个模式大体是如何工作的有了一个简单的了解。为了弄明白这个模式,下面通过一个例子来解释命令模式的工作流程,当一提到这个模式的时候,脑海中就会立刻浮现出如下的画面,当一想到这个方面,你就会立刻回忆起命令模式是怎么回事。(这个例子来自《Head First设计模式》,有书的同学直接看书好了)没书的同学,请听我下面的简单介绍,故事如下:
故事发生在一家餐厅……
角色介绍:
- 顾客
- 服务员
- 订单
- 厨师
有以上四个角色,一家餐厅就可以很好的运营了,我们想象一下有一位顾客来餐厅吃饭的场景:顾客来吃饭,他知道自己想吃的是什么,并将其写在了订单上——>之后,服务员拿走了订单,并通知厨师准备饭菜——>厨师拿到订单,开始做菜——>菜做好了………
好了,故事讲完了,这就是命令模式的大体的工作流程。不过你也许会疑惑,故事里好像没有怎么体现出“命令”这回事?一开始我也稍有疑惑,心想没看出来谁发布了命令啊,难道是顾客命令服务员?还是服务员命令厨师去做饭?
后来懂了,“命令”在这里是一个名词,是指一系列需要做的动作,而不是一个动词,不是谁命令谁的意思。这就好懂了,那回到上面的故事,订单就是命令。我们下面进入到这个模式:
代码示例
命令模式包含如下四个角色:
- Command:抽象命令类
- ConcreteCommand:具体命令类(订单)
- Invoker:调用者(服务员)
- Receiver:接收者(厨师)
- Client:客户类(顾客)
|
|
总结
命令模式将发出请求的对象和执行请求的对象解耦,降低了系统的耦合度。而且,新的命令也可以很容易的加入到系统中。
适配器模式
定义
适配器模式把一个类的接口变成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作。
说到底,适配器是将两个不兼容的类融合在一起,它有点像粘合剂,将不同的东西通过一种转换使得它们能够协作起来。
代码示例
适配器模式有两种:类适配器和对象适配器。因为对象适配器更为灵活和实用,所以这里只讲对象适配器。适配器模式包含如下三个角色:
- Target:目标角色
- Adaptee:需要适配的接口
- Adapter:适配器
这里有一点需要注意,也是对象适配器与类适配的区别:对象适配器不是使用继承关系连接到Adaptee类,而是使用组合的形式。
下面用电源接口来举例写代码,具体我就不介绍了,大家一看代码就明白,很简单:
总结
1)Adapter通常应用于进行不兼容的类型转换的场景;
2)还有一种就是输入有无数种情况,但是输出类型是统一的。如果你是搞Android开发,RecyclerView或者ListView中就是这种使用场景。
模版方法模式
定义
在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
看到这个模式的定义就感觉特别亲切,它的使用场景无处不在。用四个字概括就是流程封装。也就是把某个固定的流程封装到一个final函数中,并且让子类能够定制这个流程中的某些甚至所有的步骤。这就要求父类提取提取共用的代码,提升代码的复用率。
代码示例
模版方法模式包含如下三个角色:
- AbsTemplate:抽象类,定义了一套算法框架
- ConcreteImplA:具体实现类A
- ConcreteImplB:具体实现类B
下面写一个示例代码,示例取自《Head First设计模式》:
迭代器模式
定义
提供一种方法顺序访问一个容器对象中的各个元素,而又不需要暴露暴露该对象的内部表示。
根据定义,该模式的应用场景也就是用在遍历一个容器的对象时。例如遍历java中的各种集合类,如List、Map等。Android的源码中也为我们提供了迭代器遍历数据,最为典型的例子就是数据库查询使用Cursor,当我们使用SQLiteDatabase的query()方法查询数据时,会返回一个Cursor游标对象,该游标对象实质就是一个具体的迭代器,我们可以使用它来遍历数据库查询所得的结果集。
代码示例
前面的几种设计模式都提供了代码示例,不过迭代器这个模式对开发者来说几乎不会自己去实现一个迭代器,面试时面试官应该也不会让你写个迭代器模式吧,所以这里就不再给出代码示例,不过UML图还是要有的,如下:
策略模式
定义
策略模式定义了一系列算法,并将每一个算法封装起来,而且使它们还可以互相替换。此模式让算法的变化独立于使用算法的客户。
这是该模式的官方定义,但说实话,这个定义不像其它的一些模式,有些一看就明白是怎么回事,但这个不太好懂,下面我用大白话一解释就明白了是怎么回事,其实很简答,只不过用一句话解释不清,需要啰嗦一点:
完成一项任务,往往可以有多种不同的方式,每一种方式称为一个策略,我们可以根据环境或者条件的不同选择不同的策略来完成该项任务。
例如:有许多算法可以实现某一功能,如查找、排序等,一种常用的方法是硬编码(Hard Coding)在一个类中,如需要提供多种查找算法,可以将这些算法写到一个类中,在该类中提供多个方法,每一个方法对应一个具体的查找算法;当然也可以将这些查找算法封装在一个统一的方法中,通过if…else…等条件判断语句来进行选择。这两种实现方法我们都可以称之为硬编码,如果需要增加一种新的查找算法,需要修改封装算法类的源代码;更换查找算法,也需要修改客户端调用代码。在这个算法类中封装了大量查找算法,该类代码将较复杂,维护较为困难。
为了解决这些问题,可以定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法,在这里,每一个封装算法的类我们都可以称之为策略(Strategy),为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。
代码示例
策略模式中包含如下角色:
- Strategy:策略的抽象
- ConcreteStrategy:具体的策略实现
- Context:用来操作策略的上下文环境
下面以乘坐交通工具的场景为例来写代码:我们出门选择乘坐不同各种交通工具可以作为一种策略,例如可以乘出租或者公交车,每一种交通都有不同的计费方式,选择策略不同,计费方式自然不同。我们就以此为例,看如何运用策略模式来实现:选择不同的交通工具,实现各自的计费。
状态模式
定义
允许对象在内部状态改变时改变它的行为,对象看起来好像修改了它的类。
状态模式通常会拿来和策略模式比较,因为它俩的结构几乎完全一样,UML图也完全一样,区别在于它俩的“意图”。
刚开始我在学状态模式的时候,也是拿它和策略模式作比较,后来发现完全没必要这样。它俩完全是为了适应不同的运用场景,设计意图也不同,只是最后发现它俩的模式结构基本一样,除此之外没有任何关系。所以在学状态模式的时候,个人认为无需结合策略模式,理解各自的运用场景就好,下面就说说状态模式的使用场景:
在很多情况下,一个对象的行为取决于一个或多个动态变化的属性,这样的属性叫做状态,这样的对象叫做有状态的对象。当一个这样的对象与外部事件产生互动时,其内部状态就会改变,从而使得系统的行为也随之发生变化。
如果我们按照常规思路来做的话,代码中必定包含大量的 if-else 语句进行状态的判断,当有新的状态时,就得在类中加入新的 if-else ,这必定会导致这个类变得臃肿,而且不好维护,那这时就可以使用状态模式了。
状态模式将每一个条件分支放入一个独立的类中,这使得你可以根据对象自身的情况将对象的状态作为一个对象,这一对象不依赖于其它的对象而独立变化,这样就通过多态来去除过多的、重复的 if-else 等分支。
如果还不理解,看完下面的例子就应该明白了。
代码示例
状态模式中包含的角色:
- Strategy:抽象状态类或接口
- ConcreteStrategy:具体状态类
- Context:用来操作状态的上下文环境
下面就以电视遥控器为例来演示状态模式的实现。我们知道电视有关机和开机的状态,在开机状态下,可以切换频道、调整音量、关机等,而在关机状态下,这些操作都是无效的,只能执行开机的操作。下面我们就来实现:
代理模式
定义
为其他对象提供一个代理(类)以控制对这个对象的访问。
定义很简短也很好理解,一看就明白,没有什么需要解释的。
代码示例
代理模式包含如下四个角色:
- Subject:抽象主题类
- RealSubject:真实主题类
- ProxySubject:代理类
- Client:客户类
Builder模式
定义
确保一个类只有一个实例,而且自行实例化,并提供一个全局访问点。
1)确保只有一个实例:这个是单例模式所要实现的目的,没什么可说的,从它的名字就可以看出。
2)自行实例化:实例化只能通过new来实现,自行实例化就是只能在类内部进行new操作,否则没有别的办法来确保单例。
3)提供一个全局的访问点:因为我们是在类内部进行的实例化,如果其内部再不提供访问点,那这个类就没法使用了呀。
代码示例
单例模式的实现方式有很多种,比如懒汉模式、饿汉模式、DCL模式、静态内部类模式等。每一种方法都能够实现上面我们所定义的内容,那怎么还会有这么多的方法呢?区别就在于,在一些性能、安全等细节上,它们的处理是不同的,有的有考虑性能,有的则完全没有考虑。主要有这么几个细节问题:
1)线程安全:在多线程的情况下,要确保进行实例化的方法只有一个线程在执行;
2)延迟实例化:意思就是说当我们要用到这个类的时候,再进行实例化操作,而不是像静态成员那样,在类加载的时候就进行了实例化操作,如果永远不去使用,会造成内存的浪费。
3)性能开销要小:这个主要是针对synchronized关键字来说的,如果一个方法被声明为synchronized的,它的性能消耗是要大于普通方法的,如果要该方法频繁调用的话,性能消耗会比较大。
所以基于以上原则,才出现了不同的方法来实现单例模式,有的遵守了上面的原则,有的则没有。我下面使用“静态内部类”的方式来举例实现单例模式,因为它比较好的照顾到了上面三个原则。