如何通俗理解设计模式及其思想 _,Android 自学资料
Gson 本身是线程安全的,它可以被多个线程同时使用,因此,我更倾向于通过下面的方式获取 Gson 的实例:
public class Gsons {
private static class Holder {private static final Gson INSTANCE = new Gson();}
public static Gson getInstance() {return Holder.INSTANCE;}}
不仅是 Gson, 除此之外还有比如网络请求的相关管理类(Retrofit 对象,ServiceManager 等),Android 系统提供的各种 XXXManager(NotificationManager)等等,这些通过单例的方式去管理它,能够让你业务设计的更加严谨。
2.Builder 的链式调用
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。
Android 开发者一定很熟悉它,因为我们创建 AlertDialog 的时候,链式调用的 API 实在是赏心悦目:
new AlertDialog.Builder(this).setTitle("标题").setMessage("内容").setNegativeButton("取消", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {//...}}).setPositiveButton("确定", new DialogInterface.OnClickListener() {@Overridepublic void onClick(DialogInterface dialog, int which) {//....}}).create().show();
此外,稍微细心的同学会发现,其实 JDK 中,StringBuilder 和 StringBuffer 的源码中的 append()方法也是 Builder 模式的体现:
public StringBuilder append(String str) {super.append(str); // 调用基类的 append 方法 return this;}
// 基类的 append 方法 public AbstractStringBuilder append(String str) {if (str == null) str = "null";int len = str.length();ensureCapacityInternal(count + len);str.getChars(0, len, value, count);count += len;return this; // 返回构建对象}
除了赏心悦目的代码之外,我更关注 Builder 模式的使用场景:
当我们面临着一个复杂对象的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。
很好,我把 这个学习网站 关于 Builder 模式的适用场景复制下了下来,我曾经在学习它的时候尝试去理解它的叙述,所得出的结论是 ——上文的定义非常严谨,但是我看不懂。
我们参考 AlertDialog,对于一个 Dialog 而言,它的基本构成是复杂的(有标题,内容,按钮及其对应的事件等等属性),但是在实际需求中,不同的界面,我们需要展示给用户的 Dialog 是不一样的(标题不一样,内容不一样,点击事件也不一样),这些各个部分都是在不断剧烈的变化,但是他们组合起来是相对稳定的(就是一个 Dialog 弹出展示在界面上)。
在这种情况下,我们可以尝试使用 Builder 模式,和普通的构造器生成对象不同,如果没有需求,我们可以忽略配置某些属性——对于 Dialog,我可以不去定义 title,也可以不去定义取消按钮的点击事件,他们内部都有默认的处理;此外,对于 API 的设计来讲,Builder 模式更利于去扩展新的功能或者属性。
Builder 模式在我们开发中非常常见,除上述案例之外,Android 流行的图片加载库,以及 Notification 通知的实例化等等,都能看到 Builder 的身影。
上文说到,Builder 模式对于对象的创建提供了非常赏心悦目的 API,我理解了 Builder 模式的思想和实现方式之后,便尝试给自己的一些工具类加一些这样的设计。
很快,我遇到了一个问题,那就是——这样写太 TM 累了!
3.避免过度设计
关于过度设计的定义,请参考 什么是软件开发中的过度设计? 的解释,我认为讲解的非常风趣且易懂。
从我个人的角度而言,我遇到了问题,我尝试给一些工具改为 Builder 实现,结果是,我添加了很多很多代码,但是效果平平。
不仅如此,这样的设计给我的工具带来了更多的复杂度,本来一个构造器 new 一下能解决的问题,非要很多行代码链式配置,这种设计,做了还不如不做。
这样的结果,让我对网络上一位前辈的总结非常赞同,那就是:
设计模式的一个重要的作用是代码复用,最终的目的是提升效率。 所以,一个模式是否适合或必要,只要看看它是否能减少我们的工作,提升我们的工作效率。
那么,如何避免过度设计,我的经验告诉我,写代码之前多思考,考虑不同实现方式所需的成本,保证代码的不断迭代和调整。
即使如此,在开发的过程中,过度设计仍然是难以避免的情况,只有依靠经验的积累和不断的总结思考,慢慢调整和丰富自己的个人经验了。
4. 单一职责原则与依赖注入
对于单例模式,我似乎也会遇到过度设计这种情况——每个对象的单例都需要再写一个类去封装,似乎也太麻烦了。
实际上这并非过度设计,因为这种设计是必要的,它能够节省性能的开销,但是对象的创建和管理依然是对开发者一个不可小觑的工作量。
此外,还需要考量的是,对于一个复杂的单例对象,它可能有很多的状态和依赖,这意味着,单例类的职责很有可能很重,这在一定程度上违背了单一职责原则:
一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
单一职责原则告诉我们:一个类不能太“累”! 一个类的职责越重(这往往从构造器所需要的依赖就能体现出来),它被复用的可能性就越小。
在了解了单例模式的优点和缺点后,我们可以有选择的使用单例模式,对于依赖过于复杂的对象的单例,我们更需要仔细考量。
对于复杂的依赖管理,依赖注入库(比如Dagger)是一个可以考虑的解决方案(慎重),对于单例模式的实现,你只需要在 Module 中对应的依赖 Provider 上添加一个 @Singleton 注解,编译器会在编译期间为您自动生成对应的单例模式代码。
不能否认,这个工具需要相对较高的学习成本,但是学会了依赖注入工具并理解了 IOC(控制反转)和 DI(依赖注入)的思想之后,它将成为你开发过程中无往不胜的利器。
5.开闭原则
开闭原则:一个软件应对扩展开放、对修改关闭,用 head first 中的话说就是:代码应该如晚霞中 的莲花一样关闭(免于改变),如晨曦中的莲花一样开放(能够扩展).
建造者模式(Builder)便是开闭原则的完全体现,它将对象的构建和调用隔离开来,不同的使用者都可以通过自由的构建对象,然后使用它。
6.小结
创建型模式是最容易入门的,因为该类型的模式,更经常暴露在开发者面前,但是它们并不简单,我们除了知道这些模式的使用方式,更应该去思考什么时候用,用哪个,甚至是组合使用它们——它们有些互斥,有些也可以互补,这需要我们去研究更经典的一些代码,并自己作出尝试。
不只是创建型,接下来的结构型和行为型的设计模式,本文也不会去一一阐述其目录下所有的设计模式。
结构型模式
1.定义
首先阐述书中结构型模式的定义:
结构型模式涉及到如何组合类和对象以获得更大的结构。结构型类模式采用继承机制来组合接口或实现。
在学习之初,对我个人而言,阅读《设计模式:可复用面向对象软件的基础》 的内容宛如诵读天书,书中对每种设计模式都进行了详细的讲解,但是我看完之后,很快就忘掉了,亦或是对看起来非常相似的两种设计模式感到疑惑——书中的讲解细致入微,但是太抽象了。
最终(也就是现在),我个人对于结构型模式的理解是,通过将不同类或对象的组合,采用继承或者组合接口,或者组合一些对象,以实现新的功能。
用一句话陈述,就是对不同职责的对象(以对象/抽象类/接口的形式)之间组合调度的实现方式。
2.并非所有对象的组合都是结构型模式
实际上,并非所有对对象的组合都属于结构型模式,构型模式的意义在于,对一些对象的组合,以实现新功能的方式—— 通过运行时,通过改变组合的关系,这种灵活性产生不同的效果,这种机制,普通的对象组合是不可能实现的。
接下来我将通过阐述数种不同的结构型模式在实际开发中的应用,逐步加深对上文叙述的理解。
3.RecyclerView:适配器模式
RecyclerView 是 Android 日常开发中实现列表的首选方案,站在我的角度来看,我还没想明白一个问题,RecyclerView 是如何实现列表的?
我可以回答说,通过实现 RecyclerView.Adapter 就能实现列表呀!
事实上,是这样的,但是这引发了另外一个问题,Adapter 和 RecyclerView 之间的关系是什么,为啥实现了 Adapter 就能实现 RecyclerView 呢?
思考现实中的一个问题,我有一台笔记本电脑,我的屋子里也有一个电源,我如何给我的笔记本充电?
不假思索,我们用笔记本的充电器连接电源和笔记本就行了,实际上,充电器更官方的叫法应该叫做电源适配器(Adapter)。对于笔记本电脑和电源来讲,它们并没有直接的关系,但是通过 Adapter 适配器,它们就能产生新的功能——电源给笔记本充电。
RecyclerView 和数据的展示也是一样,数据对象和 RecyclerView 并没有直接的关系,但是我如果想要将数据展示在 RecyclerView 上,通过给 RecyclerView 配置一个适配器(Adapter)以连接数据源,就可以了。
现在我们来看 Adapter 模式的定义:
使原本由于接口不兼容而不能一起工作的那些类可以一起工作。
现在我们理解了适配器模式的应用场景,但是我想抛出一个问题:
为啥我要实现一个 Adapter,设计之初,为什么不能直接设置 RecyclerView 呢?
比如说,我既然有了数据源,为什么设计之初,不能让 RecyclerView 通过这样直接配置呢:
mRecyclerView.setDataAndShow
(datas);
我的理解是,如果把 RecyclerView 比喻为屋子里的电源插口,电源不知道它将要连接什么设备(同样,RecyclerView 也不可能知道它要展示什么样的数据,怎么展示),而不同的设备的接口也可能不一样,但是只要为设备配置一个对应的适配器,两个不相关的接口就能一起工作。
RecyclerView 的设计者将实现对开发者隐藏,并通过 Adapter 对开发者暴露其接口,开发者通过配置数据源(设备)和对应的适配器(充电器),就能实现列表的展示(充电)。
4.Retrofit:外观模式与动态代理
说到迪米特法则(也叫最少知识原则),这个应该很好理解,就是降低各模块之间的耦合:
迪米特法则:一个软件实体应当尽可能少地与其他实体发生作用。
我的学习过程中,让我感受到设计模式的组合之美的第一个库就是Retrofit,对于网络请求,你只需要配置一个接口:
public interface BlogService {
@GET("blog/{id}")Call<ResponseBody> getBlog(@Path("id") int id);}
// 使用方式// 1.初始化配置 Retrofit 对象 Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost:4567/").addConverterFactory(GsonConverterFactory.create()).build();// 2.实例化 BlogService 接口 BlogService service = retrofit.create(BlogService.class);
Retrofit 的源码中,通过组合,将各种设计模式应用在一起,构成了整个框架,保证了我们常说的高内聚,低耦合,堪称设计模式学习案例的典范,如下图(图片参考感谢这篇文章):
在分析整个框架的时候,我们首先从 API 的使用方式入手,我们可以看到,在配置 Retrofit 的时候,库采用了外观模式作为 Retrofit 的门面。
有朋友说了,在我看来,Retrofit 的初始化,不应该是 Builder 模式吗,为什么你说它是外观模式呢?
我们首先看一下《设计模式:可复用面向对象软件的基础》一书对于外观模式的定义:
为子系统中的一组接口提供一个一致的界面,外观模式定义一个高层接口,这个接口使得这一子系统更容易使用。
我的解读是,对于网络请求库的 Retrofit,它内部有着很多不同的组件,包括数据的序列化,线程的调度,不同的适配器等,这一系列复杂的子系统,对于网络请求来讲,都是不可或缺的且关系复杂的,那么,通过将它们都交给 Retrofit 对象去配置和调度(当然,Retrofit 对象的创建是通过 Builder 模式实现的),对于 API 的调用者来说,使用配置起来简单方便,这符合外观模式 的定义。
简单理解了外观模式的思想,接下来我们来看一下动态代理,对于最初接触 Retrofit 的我来说,我最难以理解的是我只配置了一个接口,Retrofit 是如何帮我把 Service 对象创建出来的呢?
// 2.实例化 BlogService 接口 BlogService service = retrofit.create(BlogService.class);
实际上,并没有 BlogService 这个对象的创建,service 只不过是在 jvm 运行时动态生成的一个 proxy 对象,这个 proxy 对象的意义是:
为其他对象提供一种代理以控制对这个对象的访问。
我想通过 BlogService 进行网络请求,Retrofit 就会通过动态代理实现一个 proxy 对象代理 BlogService 的行为,当我调用它的某个方法请求网络时,实际上是这个 proxy 对象通过解析你的注解和方法的参数,通过一系列的逻辑包装成一个网络请求的 OkHttpCall 对象,并请求网络。
现在我明白了,怪不得我无论怎么给 Service 的接口和方法命名,Retrofit 都会动态生成代理对象并在调用其方法时进行解析,对于复杂多变的网络请求来讲,这种实现的方式非常合适。
5.里氏替换原则
在优秀的源码中,我们经常可以看到,很多功能的实现,都是依赖其接口进行的,这里我们首先要理解面向对象中最重要的基本原则之一里氏替换原则:
任何基类可以出现的地方,子类一定可以出现。
里氏代换原则是对开闭原则的补充。实现开闭原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
向上转型是 Java 的基础,我们经常也用到,实际上,在进行设计的时候,尽量从抽象类继承,而不是从具体类继承。同时,保证在软件系统中,把父类都替换成它的子类,程序的行为没有变化,就足够了。
6.小结
通过上述案例,我们简单理解了几种结构型设计模式的概念和思想,总结一下:
在解决了对象的创建问题之后,对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点,因为如何设计对象的结构、继承和依赖关系会影响到后续程序的维护性、代码的健壮性、耦合性等。所以也有多种结构型模式可供开发人员选择使用。
提高类之间的协作效率——行为型模式
1.定义
我们先看书中对行为型模式比较严谨的定义:
行为模式涉及到算法和对象间职责的分配,行为模式不仅描述对象或类的模式,还描述它们之间的通信模式。这些模式刻划了在运行时难以跟踪的复杂的控制流,将你的注意力从控制流转移到对象间的联系方式上来。
依然是有点难以理解,我们先举两个例子:
2.OkHttp:Intercepter 和职责链模式
在 Okhttp 中, Intercepter 就是典型的职责链模式的体现.它可以设置任意数量的 Intercepter 来对网络请求及其响应做任何中间处理——设置缓存, Https 的证书验证, 统一对请求加密/防串改, 打印自定义 Log, 过滤请求等。
new OkHttpClient.Builder().addNetworkInterceptor(interceptor1).addNetworkInterceptor(interceptor2).addNetworkInterceptor(interceptor3)
职责链模式的定义为:
让多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将他们连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。
以现实为例,职责链模式之一就是网络连接,七层或五层的网络连接模型如下:
网络请求发出,经过应用层->传输层->网络层->连接层->物理层
收到响应后,物理层->连接层->网络层->传输层->应用层
在请求经过各层时,由每层轮流处理.每层都可以对请求或响应进行处理.并可以中断链接,以自身为终点返回响应。
3.RxJava:观察者模式
Android 开发中,点击事件的监听是很经典观察者模式的体现:
button.setOnClickListener(v -> {// do something})
对设置 OnClickListener 来说,View 是被观察者,OnClickListener 是观察者,两者通过 setOnClickListener()方法达成注册(订阅)关系。订阅之后,当用户点击按钮,View 就会将点击事件发送给已经注册的 OnClickListener。
同样,对于可以和 Retrofit 配套的 RxJava 来讲,它是也通过观察者模式来实现的。
// 被观察者 Observable observable = Observable.just("Hello", "Hi", "Aloha")
// 观察者 Observer<String> observer = new Observer<String>() {@Overridepublic void onNext(String s) {Log.d(tag, "Item: " + s);}
@Overridepublic void onCompleted() {Log.d(tag, "Completed!");}
@Overridepublic void onError(Throwable e) {
评论