开始学习设计模式——迭代器模式和适配器模式
开始看结城浩的《图解设计模式》~(对日本人写的书总有种莫名其妙的亲近感,或许是因为他们的语言比较……轻松?)
这个老师似乎也写过 Java 多线程相关的书籍,先观望一个。
什么是设计模式(Design Pattern)?我认为,我们在不断地编写代码的过程中,在不断地 debug,重构,思考中,经常会发现一些“套路”,发现这样那样编写能让代码容易维护,容易复用,写起来优雅 etc… 对这些“套路”进行系统的分析,归纳出来概念,形式,确定它的适用范围……这样所诞生的就是设计模式,它归根结底是从丰富的实践经验中抽象出来的,又反作用于我们的实践,帮助我们编写更加有可复用性,可维护性,高内聚低耦合的代码。就这方面(!类比只是类比,而不是论证,只负责提供一种感性经验罢了)来说,设计模式与具体的编程的关系,就如哲学同具体科学的关系一样。
设计模式用来表现内部组件如何被组装在一起。一场话剧能由无数届演员来演,可是剧情(剧中人与人的关系)总是变动较少的。
其实我觉得粗糙地说,
for (int i = 0; i < arr.length; i++)
这样的也算是设计模式 w
面向对象是一种设计模式,面向过程也是一种设计模式,Lisp 语言的宏编程也是一种设计模式,这是毫无疑问的。
设计模式的目标有二——复用和易维护。应时时刻刻记住这两点。
不过也正如学习哲学一样,如果没有积累丰富的感性经验(无论是对日常生活的,还是对科学的),上来就整抽象的东西容易钻到牛角尖里,无法联系实际看问题。希望这本书能够提供足够的实例让我积累感性经验。
一个非常非常非常非常需要注意的地方是,此书基于 Java 2,所以没有泛型(因为这点,或许书中很多代码需要重构),没有函数式特性,没有 foreach 的语法糖……随便翻了几页,还觉得它有时候讲的不全面……应当批判地学习,同时应当多联系 Java 的源码。
先开个头,学一学它讲解的最早的两个设计模式——Iterator 迭代器模式和 Adapter 适配器模式。
Iterator 模式——迭代器
Iterator 模式归根结底是为(以各种方式)遍历各种不同的数据结构提供一套相同的接口,它满足这样的形式——
1 |
|
迭代器的优点在于,它抽象了对对象的遍历操作,让对各种不同集合(容器)对象的遍历的操作都变得统一。如果没有这种设计模式的话,该怎么办呢?那对每种数据结构,都需要根据它的具体实现(!)来确定对它的遍历方式,比如对于数组,我们就用for (int i = 0; i < arr.length; i++)
,对于链表,我们就for (Node i = head; i != null; i = i.next)
,那对于树,对于堆,对于图呢?对于哈希表的拉链实现和数组——红黑树实现呢?对这样的形式该如何实现?显然,必须要根据其内部数据结构(和需要迭代的方式)来决定实现。迭代器其实也可以说是对这里的循环变量 i 抽象化了。
因此就可以使用迭代器模式。根据书中的一个例子来说——
在这里,Aggregate 表示一种集合(容器)类型接口,它只提供 iterator 接口。Iterator 接口就是迭代器接口,hasNext 判断是否还能继续迭代,next 方法则是获取一个元素,并让迭代器指针后移指向下一个元素。BookShelf 是为了实验而创建的数据结构,它实现 Aggregate 接口,内部存储 Book 对象(以数组形式),last 表示下一个要插入的元素的位置(同时也是已插入的元素的数量)。它的 iterator 方法返回一个对它自身的迭代器。需要注意的是,是迭代器包含类,而不是类包含迭代器,因为一个类可以同时存在多个迭代器进行迭代。
上源码。
1 |
|
然后是最重要的迭代器代码——
1 |
|
它的使用如下——
1 |
|
一个原则是,对于声明类型,能用基类就用基类,能用接口就用接口。
同时,迭代器也允许使用不同的迭代方式,只需要更改 next 和 hasNext 方法即可,比如跳跃着遍历,反向遍历……还能在迭代器内部暂存信息并不暴露给外界,比如遍历树的时候在迭代器里放个栈,队列之类的。这显然是符合抽象的。
问题:Java 中 iterable 是什么?它和迭代器有什么关系?
Adapter 模式——适配器
所谓适配器模式,就是将别人的不能直接复用的代码拿来进行一些包装,让它能够被复用。这里帮助别人的代码适配我们的需要的设计模式就叫适配器(Adapter),也叫包装器(Wrapper),因为它不直接使用别人的代码,而是进行一些包装后使用。就像笔记本的适配器把 220 伏的交流电转成 12 伏的直流电一样。Adapter 的意思是“使……相互适合的东西”。
适配器模式中需要四个角色——Client,Target,Adapter,Adaptee,它们的关系是——Client(笔记本电脑)使用 Adapter(电源适配器),将 Adaptee(220 伏交流电)转换成它所需要的 Target(12 伏直流电)。其中 Client 是调用适配器的对象,Adapter 是适配器,Adaptee 是别人的需要进行包装的代码(因此不应直接修改),Target 是 Client 所需的方法。
适配器模式有两种实现——
- 类适配器模式(使用继承的适配器)
- 对象适配器模式(使用委托的适配器)
此书给出的类适配器模式我觉得有一些漏洞,出现了抽象泄露的问题(虽然作者不可能没有意识到这个问题,因为习题 1 就涉及到了这个问题)。我认为,在设计自己的类库(就比如这里实现的适配器,对别人的类库进行包装,其实可以认为这里的 Target,Adapter,Adaptee 是自己设计出来的类库(其中 Adaptee 是“别人的代码”,在设计适配器的时候不应该对 Adaptee 进行任何改动),Client 是使用该类库的客户端)的时候,应该认为用户是“愚蠢”的,否则面向对象的封装原则就被破坏了。
这张图非常好地表现了适配器的功能和(对象适配器模式的)形式。
类适配器实例
所谓类适配器模式,就是编写 Adapter 继承 Adaptee,实现所需要的接口(Target)。
本书提供了这样一个实例——
首先有一个 Banner 类,这里假设是别人已经编写好的,要复用它。它提供了两个功能——将字符串用括号包围或用*包围,它的实现是非常简单的。
1 |
|
我们的需求(也就是 Target)是 Print 接口,Print 接口假设有两个语法,使用成对括号包围字符串规定字符串变细,使用**包围则是规定字符串加粗。
1 |
|
PrintBanner 担任 Adapter 的角色,它要提供 Target(因此就要实现 Print 接口),同时继承 Adaptee 以复用其代码。
1 |
|
然后是 Main,Main 担任 Client 的角色,它使用 Adapter 提供的 Print 接口。
1 |
|
可见,Adaptee 的操作被包装(wrap)起来了,用户(Client)通过使用 Adapter 提供的接口(Target)来间接访问 Adaptee。
但我认为这里的抽象有一些问题——如何保证用户必然使用 Print 接口来作为声明类型?如果用户偏偏不按需求来,使用 PrintBanner 作为声明类型,进行这样的操作怎么办?
1 |
|
这在语法上并无问题,但是这显然破坏了封装!用户不应该能通过任何途径来直接访问到 Adaptee。在这个示例中不会出问题,但若是联想上面的笔记本电源适配器的例子,就相当与是说用户可以直接把 220V 的交流电连接在笔记本电脑(而不是转换成 12V 直流电之后)上了,这不就出了事?我想也没有语法能够屏蔽父类的方法,只能重写或将父类方法改成 protected,但这也是不好的,我们很多时候不能更改别人的代码。
因此我认为类适配器有这样的缺陷——类库设计者必须与使用者做约定,要求使用者必须使用 Target 作为声明类型,才能正常使用。而且如果想要复用的是 final 类,则是不能使用的。
对象适配器
对象适配器容易描述。它是指 Adapter 包含一个 Adaptee 实例,通过委托(也就是将方法的执行交给另一个类(实例)执行)的方式让 Client 访问 Adaptee。
该方法相对于类适配器只需要更改 PrintBanner 类。
1 |
|
这样的一个好处是,不再需要和 Client 做额外约定了,无论 Client 使用 Print 还是 PrintBanner 作为声明类型,都不会出问题。
这一段是自己的想法,可能有错误。
我认为对象适配器模式中适配器的角色更加符合其概念。一般来说,Adapter 提供的功能应当是 Adaptee 的子集,也就是说,Adapter 能实现的方法,Adaptee 应当能实现,Adapter 不能实现的方法,Adaptee 也可能可以实现。比如,有一个库,能够把各种图片格式相互转换,比如 png, jpg, tiff, psd, pdf 等,但这时我只需要将 png 转换成其它格式,我就据此编写一个适配器,使用 Adaptee 功能的子集。而且使用委托的对象适配器模式,其结构也更加符合之前对适配器的感性理解。
然后考虑类适配器,类适配器使用继承来复用代码。而继承,虽然就概念来说,子类是基类的属概念,但是就功能,具体程度来说,子类是比基类更加具体的,在基类的基础上提供更多功能。因此这就和上面我所认为的 Adapter 同 Adaptee 提供的功能的关系相悖了。就上面的电源适配器的例子来说,就像是给 220V 交流电的电源加了个组件,让它能够转换成 12V 直流电,这个电源原本输出 220V 交流电的能力是没有改变的。这时就必须和用户做约定(Target),要求用户使用该 12V 的直流电。但是用户也有权力不遵守约定……
对象适配器的 UML 类图如图。
学会看 UML 图还是比较方便的。
使用适配器模式的目的是对现有的类进行适配,让其可复用,在出现 bug 的时候,也能够容易确定 bug 的位置出于 Adapter 中(假如 Adaptee 经过充分测试的话)。如果要去修改已经充分测试的类,则必须重新进行测试。
适配器模式也能够方便地让版本适配,从而让新旧版本兼容,让能够同时维护新版本和旧版本变得简单。
习题
java.util.Properties 可以像这样管理键值对——
1 |
|
它提供了两个方法,帮助从流(这个需要去学习!)中取出属性(property)或将属性写入流中。要求使用 Adapter 适配器模式,编写一个将属性保存在文件中的 FileProperties 类。
扮演 Target 角色的 FileIO 接口如下——
1 |
|
扮演 Adapter 角色的 FileProperties 类如下——
1 |
|
Client 角色如下——
1 |
|
这本书将设计模式中各个组成部分称为“角色”相当精妙,而且有趣。感觉遇到了一个好的开始。
等回家后第一时间去学 Spring……其它的都缓缓。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!