迭代器模式

先放上gof中对于迭代器模式的介绍镇楼

  1. 意图
    提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
  2. 别名
    游标(Cursor)。
  3. 动机
    一个聚合对象, 如列表(list), 应该提供一种方法来让别人可以访问它的元素,而又不需暴露它的内部结构. 此外,针对不同的需要,可能要以不同的方式遍历这个列表。但是即使可以预见到所需的那些遍历操作,你可能也不希望列表的接口中充斥着各种不同遍历的操作。有时还可能需要在同一个表列上同时进行多个遍历。迭代器模式都可帮你解决所有这些问题。这一模式的关键思想是将对列表的访问和遍历从列表对象中分离出来并放入一个迭代器(iterator)对象中。迭代器类定义了一个访问该列表元素的接口。迭代器对象负责跟踪当前的元素; 即, 它知道哪些元素已经遍历过了。

类图如下

工作中遇到的问题

在日常工作中,我们组负责的系统会经常与外部系统进行大量数据交互,大量数据交互的载体是纯文本文件,我们需要解析文件每一行的数据,处理后入库,所以在我们系统中就有了如下的代码了。

        public void ParseFile(string filePath, Encoding fileEncoding)
{
FileStream fs = null;
try
{
fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
using (var sr = new StreamReader(fs, fileEncoding))
{
fs = null;
string line = null;
while ( (line = sr.ReadLine()) != null )
{
//解析改行数据
}
}
}
finally
{
if (fs != null)
{
fs.Close();
}
}
}

这样子的代码存在两个问题:1-无法进行单元测试 2-无法扩展。

来析一下问题的根源

实际上这两个问题的根源都是因为直接依赖了文件系统。在我们的业务处理逻辑中,我们实际关心的是内容,而不是内容从何而来。如果内容格式不发生更改,业务逻辑代码就应该保持不变。文件作为内容的载体,可能会变为socket或者nosql数据库,如果这种情况一旦发生,难道把业务代码copy一份出来,然后把从文件读取数据改为从socket或者nosql读取?在进行单元测试时,我希望可以提供一个字符串数组就能对我的业务逻辑进行测试,而不是要提供一个文件。那么好了,我们要做的事情是将具体的数据来源隐藏掉,给业务代码提供一组API,让业务代码使用这组API可以获取到它所关心的内容。换句话说,我要提供一种方法来让人访问数据载体的元素,但是我并不像把数据载体暴露出来,这个目的简直跟迭代器模式的动机一毛一样呀。

开始动手改造

在文件解析场景中,文件就是迭代器模式中提到的聚合对象,文件中的每一行就是聚合对象的内部元素。这样我们先定义出迭代器接口和具体的文件迭代器

 public interface IIterator
{
void First();
void Next();
bool IsDone();
string GetCurrentItem();
}
 class FileIterator : IIterator
{
private readonly StreamReader _reader = null;
private string _current = null;
public FileIterator(string filePath, Encoding encoding)
{
_reader = new StreamReader(new FileStream(filePath, FileMode.Open, FileAccess.Read), encoding);
} public void First()
{
Next();
} public void Next()
{
_current = _reader.ReadToEnd();
} public bool IsDone()
{
return _current == null;
} public string GetCurrentItem()
{
return _current;
}
}

而此时我们的业务代码变成了这样

 public void ParseFile(IIterator iterator)
{
for (iterator.First(); !iterator.IsDone(); iterator.Next())
{
var current = iterator.GetCurrentItem();
Console.WriteLine(current);
//对数据进行处理
}
}

通过迭代器模式,业务代码对数据载体一无所知,按照给定的一组API,获取想要的数据即可,当进行单元测试时,我们可以提供一个基于数组的迭代器,对业务代码进行UT

class ArrayIterator:IIterator
{
private int _currentIndex = -1;
private readonly string[] _array = null; public ArrayIterator(string[] array)
{
_array = array;
} public void First()
{
Next();
} public void Next()
{
_currentIndex++;
} public bool IsDone()
{
return _currentIndex >= _array.Length;
} public string GetCurrentItem()
{
return _array[_currentIndex];
}
}

问题并未完全解决

细心的读者已经发现了,在我上面实现的文件迭代器是存在问题的,因为我在构造函数里打开了文件流,但是并没有关闭它,所以按照C#里的标准做法,文件迭代器要实现 IDisposable接口,我们还要实现一个标准的Dispose模式,我们的文件迭代器就变成了这样。

 class FileIterator : IIterator,IDisposable
{
private StreamReader _reader = null;
private string _current = null;
private bool _disposed = false;
private FileStream _fileStream = null;
private readonly string _filePath = null;
private readonly Encoding _encoding = null;
public FileIterator(string filePath, Encoding encoding)
{
_filePath = filePath;
_encoding = encoding;
} public void First()
{
//原先在构造函数里实例化StreamReader不太合适,转移到First方法里
_fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
_reader = new StreamReader(_fileStream, _encoding);
_fileStream = null;
Next();
} public void Next()
{
_current = _reader.ReadToEnd();
} public bool IsDone()
{
return _current == null;
} public string GetCurrentItem()
{
return _current;
} public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
} protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
if (_reader != null)
{
_reader.Dispose();
}
if (_fileStream != null)
{
_fileStream.Dispose();
}
}
_disposed = true;
} ~FileIterator()
{
Dispose(false);
}
}

配合这次改造,业务代码也要做一些改变

 public void ParseFile(IIterator iterator)
{
try
{
for (iterator.First(); !iterator.IsDone(); iterator.Next())
{
var current = iterator.GetCurrentItem();
Console.WriteLine(current);
//对数据进行处理
}
}
finally
{
var disposable = iterator as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
}

使用迭代器模式,成功解耦了对文件系统的依赖,我们可以随心所欲地进行单元测试,数据载体的变动再也影响不到业务代码。

C#早就看穿了一切

上面的章节,我实现了经典gof迭代器模式,实际上,迭代器模式的应用是如此的普遍,以至于有些语言已经提供了内置支持,在C#中,与迭代器有关的有foreach关键字,IEnumerable,IEnumerable<T>,IEnumerator,IEnumerator<T>四个接口,看起来有四个接口,实际上是2个,只是因为在 C#2.0版本之前未提供泛型支持,在这里仅对两个泛型接口进行讨论。

在C#中,接口IEnumerator<T>就是迭代器,对应上面的Iterator,而IEnumerable<T>接口就是聚合对象,对应上面的Aggregate。在IEnumerable<T>中只定义了一个方法

public Interface IEnumerable<T>
{
IEnumerator<T> GetEnumerator();
}

而foreach关键字c#专门为了遍历迭代器才出现的,我面试别人的时候,特别喜欢问这样一个问题:“满足什么条件的类型实例才可以被foreach遍历?"看起来正确答案应该是实现了IEnumerable<T>接口的类型,实际上C#并不要求类型实现IEnumerable<T>接口,只要类型中定义了public IEnumerator<T> GetEnumerator()接口即可。

对于IEnumerator<T>接口,微软已经想到了迭代器中可能会用到非托管对象(实际上微软刚开始忽略了这个事情,所以最初的非泛型接口IEnumerator并没有继承IDisposable接口,直到2.0后才让泛型接口IEnumerator<T>继承了IDisposable),所以它的定义是这样子的。

public interface IEnumerator<out T> : IDisposable, IEnumerator
{
new T Current {get;}
} public interface IEnumerator
{
bool MoveNext(); Object Current {get;} void Reset();
}

在C#的IEnumerator<T>中,实际上将gof经典设计中的First(),IsDone()和Next()三个方法全都合并到了MoveNext()方法中,第一次迭代前现调用MoveNext(),并通过返回值判断迭代是否结束,还额外提供了一个Reset方法来重置迭代器。当我们使用foreach写出遍历一个对象的代码时,编译器会将我们的代码进行转换。比如我们现在要遍历一个32位整型List

List<int> list = new List<int> {0,1,2,3,4};
foreach (var item in list)
{
Console.WriteLine(item);
}

编译时编译器会将代码变成类似下面这样

List<int> list = new List<int> {0,1,2,3,4};
using (var enumerator = list.GetEnumerator())
{
while (enumerator.MoveNext())
{
Console.WriteLine(enumerator.Current);
}
}

继续改造我们的代码

既然C#中已经内置了迭代器接口,我们就没有必要定义自己的IIterator接口了,直接使用IEnumerable<T>和IEnumerator<T>接口即可。

 class FileEnumerable : IEnumerable<string>
{
private readonly string _filePath;
private readonly Encoding _fileEncoding;
public FileEnumerable(string filePath, Encoding fileEncoding)
{
_filePath = filePath;
_fileEncoding = fileEncoding;
}
public IEnumerator<string> GetEnumerator()
{
return new FileEnumerator(_filePath,_fileEncoding);
} IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

  

public class FileEnumerator : IEnumerator<string>
{
private string _current;
private FileStream _fileStream;
private StreamReader _reader;
private readonly string _filePath;
private readonly Encoding _fileEncoding;
private bool _disposed = false;
private bool _isFirstTime = true;
public FileEnumerator(string filePath, Encoding fileEncoding)
{
_filePath = filePath;
_fileEncoding = fileEncoding;
}
public string Current
{
get { return _current; }
} object IEnumerator.Current
{
get { return Current; }
} public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
} protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
if (_reader != null)
{
_reader.Dispose();
}
if (_fileStream != null)
{
_fileStream.Dispose();
}
}
_disposed = true;
} public bool MoveNext()
{
if (_isFirstTime)
{
_fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
_reader = new StreamReader(_fileStream, _fileEncoding);
_fileStream = null;
_isFirstTime = false;
}
return (_current = _reader.ReadLine()) != null;
} public void Reset()
{
throw new NotImplementedException();
} ~FileEnumerator()
{
Dispose(false);
}
}

  而此时我们的业务代码变成了这样子

public void ParseFile(IEnumerable<string> aggregate)
{
foreach (var item in aggregate)
{
Console.WriteLine(item);
// //对数据进行处理
}
}

  在进行单元测试时,我可以直接传递一个字符串数组进去了。

最终版本

看起来我们对于代码的重构已经完美了,但是实际上C#对于迭代器的内置支持要更彻底,在上面,我们必须要自己写一个实现了IEnumerator<T>接口的类型,这个工作虽然不难,但是还是有点繁琐的,C# 针对迭代器模式,提供了yield return和yield break来帮助我们更快更好的实现迭代器模式。下面是代码重构的最终版本,我们无需自己定义FileEnumerator类了

 class FileEnumerable : IEnumerable<string>
{
private readonly string _filePath;
private readonly Encoding _fileEncoding;
public FileEnumerable(string filePath, Encoding fileEncoding)
{
_filePath = filePath;
_fileEncoding = fileEncoding;
}
public IEnumerator<string> GetEnumerator()
{
FileStream fileStream = null;
try
{
fileStream = new FileStream(_filePath, FileMode.Open, FileAccess.Read);
using (var reader = new StreamReader(fileStream, _fileEncoding))
{
fileStream = null;
string line = null;
while ((line = reader.ReadLine()) != null)
{
yield return line;
}
yield break;
}
}
finally
{
if (fileStream != null)
{
fileStream.Dispose();
}
} } IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}

  这里编译器会根据我们的代码,结合yield return和yield break来帮助我们生存一个实现了IEnumerator<string>接口的类型出来。

关于Dispose模式,和yield return,yield break本篇不做过多展开,有兴趣的可以找下资料,msdn会告诉你

迭代器模式的一种应用场景以及C#对于迭代器的内置支持的更多相关文章

  1. 设计模式(8) - 迭代器模式(iterator)- 实现ArrayList和linkedList的迭代器

    上周六就開始写这篇博客,之后一直耽误了.到前天才開始写.今天醒的早,就把这部分整理一下. 本文内容參考易学设计模式和马士兵的迭代器模式的视频. 了解迭代器模式一个作用就是让你在使用 迭代器遍历集合类的 ...

  2. Spring MVC内置支持的4种内容协商方式【享学Spring MVC】

    每篇一句 十个光头九个富,最后一个会砍树 前言 不知你在使用Spring Boot时是否对这样一个现象"诧异"过:同一个接口(同一个URL)在接口报错情况下,若你用rest访问,它 ...

  3. nginx应用场景,特性,目录结构,常用模块,内置变量,URL和URI,http状态码,配置文件详解

    1.nginx介绍 1丶俄罗斯人开发的,开源www服务软件 2丶软件一共780K 3丶nginx本身是一款静态(html,js,css,jpg等)www软件 4丶静态小文件高并发,同时占用的资源很少, ...

  4. 23种设计模式之迭代器模式(Iterator)

    迭代器模式是一种对象的行为型模式,提供了一种方法来访问聚合对象,而不用暴露这个对象的内部表示.迭代器模式支持以不同的方式遍历一个聚合对象,复杂的聚合可用多种方法来进行遍历:允许在同一个聚合上可以有多个 ...

  5. php 23种设计模式 - 迭代器模式

    迭代器模式 迭代器模式 (Iterator),又叫做游标(Cursor)模式.提供一种方法访问一个容器(Container)对象中各个元素,而又不需暴露该对象的内部细节. 当你需要访问一个聚合对象,而 ...

  6. Java源代码-迭代器模式

    Java无疑是最成功的项目之一了,而在其中学习设计模式和架构设计,无疑是最好不过了. 概念: 提供一种方法访问容器中的各个元素,而又不暴露该对象的内部细节. 使用场景: 和容器经常在一起,我们定义了一 ...

  7. [Head First设计模式]生活中学设计模式——迭代器模式

    系列文章 [Head First设计模式]山西面馆中的设计模式——装饰者模式 [Head First设计模式]山西面馆中的设计模式——观察者模式 [Head First设计模式]山西面馆中的设计模式— ...

  8. JAVA 设计模式 迭代器模式

    用途 迭代器模式 (Iterator) 提供一种方法顺序访问一个聚合对象中各个元素,而又不暴露该对象的内部表示. 迭代器模式是一种行为型模式. 结构

  9. C#设计模式(16)——迭代器模式(Iterator Pattern)

    一.引言 在上篇博文中分享了我对命令模式的理解,命令模式主要是把行为进行抽象成命令,使得请求者的行为和接受者的行为形成低耦合.在一章中,将介绍一下迭代器模式.下面废话不多说了,直接进入本博文的主题. ...

随机推荐

  1. 发现IE6的一个BUG,添加受信任站点后,页面无法跳转

    最近客户爆了一个问题,说是最近使用我们的系统,一登录浏览器就直接关闭了.   经排查,属于IE6设置受信任站点的问题,受信任站点设置了通配符,如 http://192.168.1.* 这样的格式,而我 ...

  2. 十五天精通WCF——第十天 学会用SvcConfigEditor来简化配置

    我们在玩wcf项目的时候,都是自己手工编写system.serviceModel下面的配置,虽然在webconfig中做wcf的服务配置的时候,vs提供大多 数的代码提示,但对于不太熟悉服务配置的小鸟 ...

  3. VMWare克隆之后设置eth0

    [root@hadoop001 ~]# cd /etc/udev/rules.d/ [root@hadoop001 rules.d]# vim 70-persistent-net.rules 将eth ...

  4. SHA-1 加密算法破解现已只需要 10 天

    转自:http://www.linuxeden.com/html/news/20151009/163173.html SHA-1是如今很常见的一种加密哈希算法,HTTPS传输和软件签名认证都很喜欢它, ...

  5. 论Top与ROW_NUMBER读取第一页的效率问题

    10.29 前一段时间研究关于分页的问题,由于数据库属于百万级的,考虑了关于优化方面的问题.其中一个考虑是:第一页展现的频率肯定是最高的,所以我想第一页就使用Top N来读取. 这个想法本身是没有错, ...

  6. C#设计模式(21)——责任链模式

    一.引言 在现实生活中,有很多请求并不是一个人说了就算的,例如面试时的工资,低于1万的薪水可能技术经理就可以决定了,但是1万~1万5的薪水可能技术经理就没这个权利批准,可能就需要请求技术总监的批准,所 ...

  7. Android 横竖屏切换小结

    (自己体会:每次横竖屏自动切时都会run Activity的onCreate,即相当后重新进入Activity初始化一样:) 转自:http://www.cnblogs.com/franksunny/ ...

  8. 怎么找到占用usb的模块,linux下Jlink连接失败

    问题是这样产生的,我在linux下安装jlink,启动JLinkExe执行,总是提示不能通过usb连接: SEGGER J-Link Commander V5.10q (Compiled Mar :: ...

  9. 如何在报表权限中使用session

    1. 问题描述 权限中使用session,一般是用来存放用户名和密码,下面以报表开发工具FineReport为例,分两种情况介绍用户名和密码的保存: 2. 同一应用下session 由于session ...

  10. 使用Openswan接入Windows Azure Site to Site VPN

    Winodows Azure的Site to Site VPN支持主流的防火墙和路由器等接入设备.具体型号和系列请参考下表: VENDOR DEVICE FAMILY MINIMUM OS VERSI ...