[TOC]
示例
合并菜单(C#实现)
需求说明
某连锁餐厅有一家早餐店和一家晚餐店,现需要将早餐店和晚餐店合并,由于早餐和晚餐其数据结构不同,现在需要一个统一的菜单,即菜单项结构相同
数据结构说明
合并后的菜单单项
1 | public class MenuItem |
早餐类,结构是ArrayList
1 | public class BreakfastMenu |
晚餐类,结构是数组
1 | public class DinnerMenu |
原有的客户端实现
1 | //不使用迭代器 |
上面的遍历的算法是一样的,因为早餐和晚餐的数据结构的不同导致了代码不能复用
如果以后还要将一个午餐厅餐单合并到菜单中(数据结构和早餐晚餐都不同),又要修改代码,可以使用泛型解决该问题。但是这里使用的是迭代器设计模式
使用迭代器模式实现
定义一个迭代器接口
1 | interface ITerator |
我们希望的是能通过迭代器实现下面的操作
1 | while (iterator.HasNext()) |
创建早晚餐菜单的迭代器
1 | public class BreakfastIterator : ITerator |
1 | public class DinnerIterator : ITerator |
定义一个菜单接口,来创建迭代器,返回迭代器接口
1 | interface IMenu |
在早餐和晚餐的菜单中实现这个菜单接口
1 | public class BreakfastMenu : IMenu |
1 | public class DinnerMenu : IMenu |
注意在 BreakfastMenu
和DinnerMenu
类中去掉了 GetMenuItems()
方法.因为在改造前的实现中我们需要GetMenuItems()
提供聚集数据 ,但是正是因为两个类的GetMenuItems()
返回的数据结构不同,造成了代码不能复用
改造后BreakfastMenu
和DinnerMenu
类只是单纯的存放聚集数据,我们用更抽象的ITerator
的来返回单个数据,避免 数据的不一致,ITerator
类的Next()
方法 接管了原来GetMenuItems()
方法的 功能
客户端调用
1 | static void Main(string[] args) |
迭代器模式主要是聚合对象创建迭代器,借助单一职责的原则,从而实现客户端可以对聚合的各种对象实现相同的操作,达到代码复用的效果。
类图对比
上面代码的类图
迭代器模式的类图
.NET中的迭代器模式
.NET1.1
在上面的例子中,我们没有使用任何.NET
的特性
实际要在.NET内部已经定义了实现Iterator模式
需要的聚集接口和迭代器接口,其中IEnumerator
扮演的就是迭代器接口的角色,也就是上文中的ITerator
:
1 | //System.Collections.IEnumerator |
属性Current
返回当前集合中的元素,Reset()
方法恢复初始化指向的位置,MoveNext()
方法返回值true
表示迭代器成功前进到集合中的下一个元素,返回值false
表示已经位于集合的末尾
IEnumerable
则扮演的就是抽象聚集接口的角色,相当于上文中的IMenu
,只有一个GetEnumerator()
方法,如果集合对象需要具备跌代遍历的功能,就必须实现该接口。
1 | public interface IEnumerable |
我们试着使用.NET1.1
的方式来改写合并菜单的例子,因为有了系统接口,我们可以少写两个接口,使用系统接口
迭代器:
1 | public class BreakfastIterator : IEnumerator |
这里为了方便迭代,_position
的初始值设置成了-1,因为MoveNext
函数同时完成了移动指针和判断是否能继续移动的功能
聚集数据:
1 | public class BreakfastMenu : IEnumerable |
客户端调用
1 | class Program |
可以看出上面使用迭代器模式实现时使用的迭代器类没有了,while (iterator.HasNext()) {...}
也没有了,foreach
究竟使用了什么魔法呢?
看一下这段对应的IL代码
IL代码
比较难看懂,重点关注红线的部分,所以大概等价于这么回事(只分析breakfastMenu
部分)
1 | BreakfastMenu breakfastMenu = new BreakfastMenu (); |
看出来了么?.NET
帮我们在底层实现了调用接口方法并迭代,foreach只是语法糖
不过,尽管我们少写了很多代码,这里的实现和不用特性的代码相比,在代码结构上仍然高度相似,并且我们把大部分的精力都花在了实现迭代器和可迭代的类上面.到了NET2.0
,由于有了yield return
关键字,实现起来将更加的简单优雅.
.NET2.0
有了yield return
可以这么实现聚集数据:
注意GetEnumerator
方法中使用了yield return
1 | public class BreakfastMenu : IEnumerable |
客户端调用不变,注意因为是固定数组所以要判一下空
1 | class Program |
这次我们连迭代器接口都省略了!
那yield
又是个啥?
只看这一段
1 | public IEnumerator GetEnumerator() |
对应的IL代码
有点看不懂了,不过还是发现了一点蛛丝马迹,红线的部分,状态机!
是的编译器在幕后构建了一个状态机,隐藏了复杂性.原理就是记录下程序状态,下次迭代从状态处开始,这样来实现MoveNext
作为理解原理,到这里已经足够了.如果要知道yield
的具体实现,必须要去研究CLI
的内容
详细看一下这一段状态机是怎么实现的
this.<>1__state
代表内部的状态,下面记做statethis.<>4__this
代表当前类的实例this.<i>E5__1
代表当前迭代的下标,下面记做index- this.<>2__current 代表当前的迭代结果item
state的初始状态为0,state为0时给index初始值0
index>=menuItems.Count
时返回false
其余情况给state设为1,返回true
state的初始状态为1时++index
还是很好理解的
可见 C#一直在努力改进语法糖,让我们能更方便的写代码.但是其实,内部实现了迭代器模式
实现自己的迭代器(js和ruby实现)
需求:判断两个已经排序的数组里的元素是否完全相同
1.实现一个each
函数
each函数接受两个参数,第一个为被循环的数组,第二个为循环中每一步后将触发的回调函数1
2
3
4
5
6
7
8
9
10
11
12var each = function(arr, callback) {
for (var i = 0; i < arr.length; i++) {
//把下标和元素当作参数传给callback函数
callback.call(arr[i], i, arr[i]);
}
};
//调用
each([1, 2, 3], function(i, n) {
console.log([i, n]);
});
2.内部迭代器和外部迭代器
内部迭代器
上面的each函数属于内部迭代器,外界不需关心迭代器内部实现
然而,我们需要稍微改动each函数才能实现我们的需求
1 | var each = function(arr, callback) { |
1 | var isSameArray = function(arr1, arr2) { |
上面的isSameArray
函数实现的相当难看,能够实现功能得益于在js
中可以把函数当作参数传递的特性,但是在其他语言中未必可行
在没有闭包的语言中,内部迭代器本身的实现相当复杂,比如C语言实现内部迭代器就就需要用到函数指针,循环处理所需要的数据都要以参数的形式明确地从外面传递进去
外部迭代器
外部迭代器必须显式地请求迭代下一个元素。
外部迭代器增加了一些调用的复杂度,但相对也增强了迭代器的灵活性,我们可以手工控制迭代的过程或者顺序。
下面这个外部迭代器的实现来自《松本行弘的程序世界》第4 章,原例用Ruby
写成
1 | class ArrayIterator |
改成js
并且实现的上面的需求
ES5版本:
1 | var Iterator = function(arr) { |
ES6版本,其实这里Iterator最好用class实现
1 | const Iterator = arr => { |
浏览器上传组件(js实现)
重构某个项目中文件上传模块的代码时,发现了下面这段代码,它的目的是根据不同的浏览器获取相应的上传组件对象:
1 | var getUploadObj = function () { |
为了得到一个upload 对象,这个getUploadObj
函数里面充斥了try,catch以及if 条件分支。缺点是显而易见的:
- 第一是很难阅读
- 第二是严重违反开闭原则
后来我们还增加了一些另外的上传方式,比如HTML5
上传,此时唯一的办法是继续往getUploadObj
函数里增加条件分支。
现在来梳理一下问题,目前一共有3 种可能的上传方式,我们不知道目前正在使用的浏览器支持哪几种。于是从第一种方式开始,迭代所有方式进行尝试,直到找到了正确的方式为止。做法如下
- 把每种获取
upload 对象
的方法都封装在各自的函数里 使用一个迭代器,迭代获取这些
upload 对象
,直到获取到一个可用的为止:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var getActiveUploadObj = function () {
try {
return new ActiveXObject("TXFTNActiveX.FTNUpload"); // IE 上传控件
} catch (e) {
return false;
}
};
var getFlashUploadObj = function () {
if (supportFlash()) { // supportFlash 函数未提供
var str = '<object type="application/x-shockwave-flash"></object>';
return $(str).appendTo($('body'));
}
return false;
};
var getFormUpladObj = function () {
var str = '<input name="file" type="file" class="ui-file"/>'; // 表单上传
return $(str).appendTo($('body'));
};
在getActiveUploadObj
、getFlashUploadObj
、getFormUpladObj
这3 个函数中都有同一个约定:
如果该函数里面的upload 对象是可用的,则让函数返回该对象,反之返回false,提示迭代器继续往后面进行迭代。
所以我们的迭代器只需进行下面这几步工作
提供一个可以被迭代的方法,使得
getActiveUploadObj
,getFlashUploadObj
以及getFlashUploadObj
依照优先级被循环迭代如果正在被迭代的函数返回一个对象,表示找到了正确的upload 对象,反之如果该函数返回false,则让迭代器继续工作。
1
2
3
4
5
6
7
8
9
10
11
12var iteratorUploadObj = function () {
for (var i = 0, fn; fn = arguments[i++];) {
var uploadObj = fn();
if (uploadObj !== false) {
return uploadObj;
}
}
};
var uploadObj = iteratorUpload(getActiveUploadObj, getFlashUploadObj, getFormUpladObj);重构代码之后,获取不同上传对象的方法被隔离在各自的函数里互不干扰,try、catch 和if 分支不再纠缠在一起。
并且我们可以很方便地的维护和扩展代码。比如,后来我们又给上传项目增加了
Webkit
控件上传和HTML5
上传,我们要做的仅仅是下面一些工作。增加分别获取
Webkit
控件上传对象和HTML5
上传对象的函数:1
2
3
4
5
6var getWebkitUploadObj = function(){
// 具体代码略
};
var getHtml5UploadObj = function(){
// 具体代码略
};依照优先级把它们添加进迭代器:
1
2
3
4
5
6
var uploadObj = iteratorUploadObj( getActiveUploadObj,
getWebkitUploadObj,
getFlashUploadObj,
getHtml5UploadObj,
getFormUpladObj );
计数器(python实现)
1 | """http://ginstrom.com/scribbles/2007/10/08/design-patterns-python-style/""" |