享受代码,享受人生

SOA is an integration solution. SOA is message oriented first.
The Key character of SOA is loosely coupled. SOA is enriched
by creating composite apps.
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

测试驱动开发 --- Rss Reader Item Marker

Posted on 2005-03-29 19:08  idior  阅读(2657)  评论(8编辑  收藏  举报

1.         Refactory away External Loops        尽量将集合遍历的操作放在集合内

2.      Reduce Private Method                    尽量减少私有方法

3.     Open-Close Principle                        封闭开放原则

4.      Specification Pattern                        如何从一组对象中选出满足特定条件的对象

本文将围绕以上主题就最近很流行的RSS阅读器的一个功能(Mark)展开.

 

先介绍一下需求:

阅读博客的时候需要查看一篇篇的随笔,自然我们想让已经查看过的随笔和没有查看过的随笔区别开来.于是, 我们需要通过UI,利用黑体字将没有查看的随笔显著的标识.不过万一你一不小心点了一篇随笔,但是并没有查看它,我们当然希望可以将它设回未读的状态, 功能更强一点我们希望可以将所有已读的随笔都标回未读的状态.

为了使我们的阅读器显的更酷, 我们还提供了类似于Gmail那样将我们的随笔标为Star的状态,以显示该随笔的重要性. 你可以一个个的标记Star, 如果有一个方法可以让所有满足特定条件的随笔都标为Star, 当然更受欢迎些.

 

让我们来总结一下:

1.          提供一个集合, 可以往集合中加入随笔. 该功能主要用于对后面功能进行测试.

2.          每篇随笔有一个已读,未读和Star三种状态. 可以改变.

3.          将对所有的已读随笔标为未读.

4.          将符合某些特定条件的随笔标为Star.

 

 

下面开始我们的测试驱动开发:

首先实现随笔集合的添加功能.

     5     [TestFixture]

    6     public class ItemCollectionTest

    7     {

    8         ItemCollection items = new ItemCollection();

    9 

   10         [SetUp]

   11         public void AddItem()

   12         {

   13 

   14             items.Add(new Item("oo"));

   15             Assert.AreEqual(1, items.Count);

   16 

   17             items.Add(new Item("orm"));

   18             Assert.AreEqual(2, items.Count);

   19 

   20             items.Add(new Item("aop"));

   21 

   22             items.Add(new Item("soa"));

23                     }

24        }

 

然后当然要实现Get的功能.

   26      [Test]

   27         public void GetItem()

   28         {

   29             Item item1=items.Get(0);

   30             Assert.AreEqual("oo",item1.Title);

   31         }

第一个功能的测试代码就这么简单.目前我们只需要这两个功能 很快地,我们让测试通过.

    9    public class ItemCollection

   10     {

   11         public IList Items

   12         {

   13             get

   14             {

   15                 return items;

   16             }

   17         }

   18 

   19         private ArrayList items=new ArrayList();

   20 

   21         public void Add(Item item)

   22         {

   23             items.Add(item);

   24            

   25         }

   26 

   27         public Item Get(int id)

   28         {

   29             return items[id] as Item;

   30         }

   31 

   32         private int count;

   33         public int Count

   34         {

   35             get

   36             {

   37                 return items.Count;

   38             }

   39         }

   40     }

Ok,Pass. (这里不对集合操作做详细讨论.)

让我们考虑第二个功能, 有关状态的转换. 这里本来是考虑用enum, 但是想到ReadedStar状态可以同时存在所以, 还是用两个bool量表示比较简单, 管它呢, 满足目前的需要就可以了, 如果以后需要修改, 重构一下就是了.测试代码如下:

    6     [TestFixture]

    7     public class ItemTest

    8     {

    9         [Test]

   10         public void ItemStatus()

   11         {

   12             Item item = new Item();

   13             Assert.AreEqual(false, item.ReadedFlag);

   14             item.MarkReaded();

   15             Assert.AreEqual(true, item.ReadedFlag);

   16             item.MarkUnReaded();

   17             Assert.AreEqual(false, item.ReadedFlag);

   18         }

   19     }

很快让它通过.

    8     public class Item

    9     {

   10         private bool readedFlag=false;

   11 

   12         private string title;

   13 

   14         public Item()

   15         {

   16 

   17         }

   18 

   19         public Item(string s)

   20         {

   21             title=s;

   22         }

   23 

   24         public bool ReadedFlag

   25         {

   26             get

   27             {

   28                 return readedFlag;

   29             }

   30         }

   31 

   32         public void MarkReaded()

   33         {

   34             readedFlag=true;

   35         }

   36 

   37         public void MarkUnReaded()

   38         {

   39             readedFlag=false;

   40         }

   41 

   42         public string Title

   43         {

   44             get

   45             {

   46                 return title;

   47             }

   48         }

   49     }

 

 

第三个功能稍微复杂些.有关标记(Mark)的操作自然想到建立一个专门管理标记的类.于是测试代码如下:

    6     [TestFixture]

    7     public class MarkManagerTest

    8     {

    9         ItemCollection items=new ItemCollection();

   10 

   11         [SetUp]

   12         public void Init()

   13         {

   14             items.Add(new Item("oo"));

   15             items.Add(new Item("orm"));

   16             items.Add(new Item("aop"));

   17             items.Add(new Item("soa"));

   18             Assert.AreEqual(4, items.Count);

   19         }

   20 

   21         [Test]

   22         public void MarkAllReadedToUnReaded()

   23         {

   24             items.Get(0).MarkReaded();

   25             Assert.AreEqual(true,items.Get(0).ReadedFlag);

   26 

   27             items.Get(2).MarkReaded();

   28             Assert.AreEqual(true,items.Get(2).ReadedFlag);

   29 

   30             MarkManager markManager=new MarkManager(items);

   31             markManager.MakeReadedToUnReaded();

   32 

   33             Assert.AreEqual(false,items.Get(0).ReadedFlag);

   34             Assert.AreEqual(false,items.Get(2).ReadedFlag);

   35 

   36         }

   37     }

如果实现将所有的已读随笔变成未读状态, 一个最简单的方法就是遍历所有的随笔,让它们的状态都改成未读. 实现如下:

    8     public class MarkManager

    9     {

   10         ItemCollection items=new ItemCollection();

   11 

   12         public MarkManager(ItemCollection items)

   13         {

   14             this.items=items;

   15         }

   16 

   17         public void MakeReadedToUnReaded()

   18         {

   19             foreach(Item item in items.Items)

   20             {

   21                 item.MarkUnReaded();

   22             }

   23         }

   24     }

主要过程就是Line 19 -22.

 

测试通过了,不过Bad Smell已经出现. 将集合暴露在外,首先破坏了类的封装性.其次会产生很多的重复代码. 比如在这个例子中, 如果使用目前的方法, 在以后会不断出现有关集合遍历的代码片段. 为了消除重复, 我们需要对已有的代码进行重构.

在测试驱动开发中主要有三个步骤:

1.          编写测试代码

2.          编写代码使测试通过

3.          针对代码中出现的Bad Smell重构

其中前两个步骤, 大家都知道,但是第三个步骤却往往被人忽视.需要提起注意的是:测试驱动开发如果少了三者中的任意一个就不再完整.

 

让我们看看怎样实现Refactory away External Loops, 为了避免不断的出现重复的遍历集合的代码,自然就想到把遍历集合的任务放在集合中,让它自己完成.有关遍历时的操作可以通过一个Delegate来指派. 这样就可以只用一处代码就实现有关遍历集合的所有操作.

 

看看我们的重构过程.

 

首先把遍历集合的操作转移到集合类中.然后声明一个Delegate,并在遍历集合的时候使用它.

    9    public class ItemCollection

   10     {

   11         public delegate void IteratorAction(Item item);

   12 

   13         public void Iterate(IteratorAction action)

   14         {

   15             foreach (Item item in items)

   16                 action(item);

   17      }

 

然后在我们的MarkManager中为Delegate加上我们的方法.在这里就是将Item的状态设为未读.如下所示:

    8    public class MarkManager

    9     {

   10         public void MakeReadedToUnReaded()

   11         {

   12             items.Iterate(new ItemCollection.IteratorAction(MarkItemUnReaded));

   13         }

   14 

   15         private void MarkItemUnReaded(Item item)

   16         {

   17             item.MarkUnReaded();

   18         }

然后运行测试.Great! 测试通过.并且以后再也不会到处散布着遍历集合的代码了.

现在最明显的Bad Smell 已经消除. 让我们继续下一个测试案例吧.

 

将符合某些特定条件的随笔标为Star. 看上去比较困难,先把条件去掉实现将所有随笔标为Star.编写测试代码:

   38      [Test]

   39         public void MarkAllToStar()

   40         {

   41             foreach (Item item in items.Items)

   42                 Assert.AreEqual(false,item.StarFlag);

   43 

   44             MarkManager markManager=new MarkManager(items);

   45             markManager.MarkAllStar();

   46 

   47             foreach (Item item in items.Items)

   48                 Assert.AreEqual(true,item.StarFlag);

   49         }

似乎这里出现了遍历集合,不过别忘了这是测试代码,实际代码中并不会出现,如下所示:

    8     public class MarkManager

    9     {

   10         public void MarkAllStar()

   11         {

   12             items.Iterate(new ItemCollection.IteratorAction(MarkItemStar));

   13         }

   14 

   15         private void MarkItemStar(Item item)

   16         {

   17             item.MarkStar();

   18         }

由于前面的重构,使得这个功能添加的非常简单.

下面我们来看看如何对满足特定条件的随笔标记Star.首先可以确定这个条件是和Item自身的属性相关的, 比如Title, Date之类的,而不会是别的什么条件.然后对于各种各样的条件我们要想办法去描述它们,于是乎Specification类呼之而出.开始编写测试代码:

 

   59      [Test]

   60         public void MarkSatifyedToStar()

   61         {

   62             foreach (Item item in items.Items)

   63                 Assert.AreEqual(false,item.StarFlag);

   64 

   65             items.Get(0).MarkReaded();

   66             items.Get(2).MarkReaded();

   67 

   68             MarkManager markManager=new MarkManager(items);

   69             Specification specification=new UnReadedSpecification();

   70             markManager.MarkStar(specification);

   71 

   72             Assert.AreEqual(false,items.Get(0).StarFlag);

   73             Assert.AreEqual(true,items.Get(1).StarFlag);

   74             Assert.AreEqual(false,items.Get(2).StarFlag);

   75             Assert.AreEqual(true,items.Get(3).StarFlag);

   76 

   77         }

 

Specification中主要应用了Template-Method模式.

    6     public class Specification

    7     {

    8         public virtual bool IsSatisfy(Item item)

    9         {

   10             return true;

   11         }

   12     }

 

    8     public class UnReadedSpecification:Specification

    9     {

   10         public override bool IsSatisfy(Item item)

   11         {

   12             return item.ReadedFlag==false;

   13         } 

   14     }

对于未来多种多样的条件,我们只要让它继承Specification,并覆写IsSatisy方法就可以了.

然后创建MarkManager中的MarkStar方法.

    8     public class MarkManager

    9     {

   10         private void MarkItemStar(Item item)

   11         {

   12             item.MarkStar();

   13         }

   14         public void MarkStar(Specification specification)

   15         {

   16             items.Iterate(new ItemCollection.IteratorAction(MarkItemStar),specification);

   17         }

,这里我们修改了Iterate函数,以致前面的代码都无法通过了.好在Specification默认的IsSatisfy返回true. 所以在前面(MarkAllStar等等)加上一个new Specification() 参数就可以通过测试了.

    9     public class ItemCollection

   10     {

   11         public delegate void IteratorAction(Item item);

   12 

   13         public void Iterate(IteratorAction action,Specification spec)

   14         {

   15             foreach (Item item in items)

   16             {

   17                 if (spec.IsSatisfy(item))

   18                     action(item);

   19             }

   20         }

现在测试通过了, 又该开始重构了. Bad Smell在哪? 针对三种需求,现在各有三个方法. 如果以后我们又有新的需求难道不断得为MarkManager添加方法? 这显然不利变化和维护.既然我们已经定义了Action, 为何不把它作为一个参数?这样我们只要一个方法不就可以实现各种各样的操作.

MarkManager中添加一个新的方法Mark(Specification s, IteratorAction action)

然后将原来的方法用它进行替代, 再把原来的private方法从MarkManager中抽出来,放入测试代码中.新的测试代码如下:

    6 [TestFixture]

    7     public class MarkManagerTest

    8     {

    9         ItemCollection items=new ItemCollection();

   10 

   11         [SetUp]

   12         public void Init()

   13         {

   14             items.Add(new Item("oo"));

   15             items.Add(new Item("orm"));

   16             items.Add(new Item("aop"));

   17             items.Add(new Item("soa"));

   18         }

   19         [TearDown]

   20         public void Cler()

   21         {

   22             items.Clear();

   23         }

   24 

   25         private void MarkItemStar(Item item)

   26         {

   27             item.MarkStar();

   28         }

   29 

   30         private void MarkItemUnReaded(Item item)

   31         {

   32             item.MarkUnReaded();

   33         }

   34 

   35         [Test]

   36         public void MarkAllReadedToUnReaded()

   37         {

   38             Assert.AreEqual(4, items.Count);

   39 

   40             items.Get(0).MarkReaded();

   41             Assert.AreEqual(true,items.Get(0).ReadedFlag);

   42 

   43             items.Get(2).MarkReaded();

   44             Assert.AreEqual(true,items.Get(2).ReadedFlag);

   45 

   46             MarkManager markManager=new MarkManager(items);

   47             markManager.Mark(new Specification(), new ItemCollection.IteratorAction(MarkItemUnReaded));

   48 

   49             Assert.AreEqual(false,items.Get(0).ReadedFlag);

   50             Assert.AreEqual(false,items.Get(2).ReadedFlag);

   51         }

   52 

   53         [Test]

   54         public void MarkAllToStar()

   55         {

   56             Assert.AreEqual(4, items.Count);

   57 

   58             foreach (Item item in items.Items)

   59                 Assert.AreEqual(false,item.StarFlag);

   60 

   61             MarkManager markManager=new MarkManager(items);

   62             markManager.Mark(new Specification(), new ItemCollection.IteratorAction(MarkItemStar));

   63 

   64             foreach (Item item in items.Items)

   65                 Assert.AreEqual(true,item.StarFlag);

   66         }

   67 

   68         [Test]

   69         public void MarkSatifyedToStar()

   70         {

   71             foreach (Item item in items.Items)

   72                 Assert.AreEqual(false,item.StarFlag);

   73 

   74             items.Get(0).MarkReaded();

   75             items.Get(2).MarkReaded();

   76 

   77             MarkManager markManager=new MarkManager(items);

   78             Specification spec=new UnReadedSpecification();

   79             markManager.Mark(spec, new ItemCollection.IteratorAction(MarkItemStar));

   80 

   81 

   82             Assert.AreEqual(false,items.Get(0).StarFlag);

   83             Assert.AreEqual(true,items.Get(1).StarFlag);

   84             Assert.AreEqual(false,items.Get(2).StarFlag);

   85             Assert.AreEqual(true,items.Get(3).StarFlag);

   86         }

87         }

88          

相应的MarkManager的重构:

    8     public class MarkManager

    9     {

   10         ItemCollection items=new ItemCollection();

   11 

   12         public MarkManager(ItemCollection items)

   13         {

   14             this.items=items;

   15         }

   16 

   17         public void Mark(Specification spec, ItemCollection.IteratorAction action)

   18         {

   19             items.Iterate(spec,action);

   20         }

   21     }

这样MarkManager就变得非常简单并且可以应对未来多变的Mark策略. 不过对于测试代码中存在的Private方法, 对我来说也是一种Bad Smell, 我们应该尽量减少Private 方法,尤其是那种比较大的Private方法. 虽然在这里Private很简单, 但是它如同前面的各种Mark方法一样.如果我们要Mark成感叹号呢?我们又要修改我们的Client(这里是Test),不断添加新的方法. 如何消除Private方法? 很简单,引入新的对象,Private方法中的操作交给新的对象,变成Public方法.

    8     public abstract class ItemActor

    9     {

   10         private ItemCollection.IteratorAction action;

   11 

   12         public ItemCollection.IteratorAction Action

   13         {

   14             get

   15             {

   16                 return action;

   17             }

   18         }

   19 

   20         public ItemActor()

   21         {

   22             action=new ItemCollection.IteratorAction(ItemAction);

   23         }

   24 

   25         protected abstract void ItemAction(Item item);

   26 

   27     }

不同的Item操作只要实现不同的ItemAction方法就可以了. 看看MarkStar的实现

    8     public class ItemMarkStarActor:ItemActor

    9     {

   10         protected override void ItemAction(Item item)

   11         {

   12             item.MarkStar();

   13         }

   14     }

其他的操作也是同样的道理.

最后看看我们最终的测试代码.

    1 using System;

    2 using NUnit.Framework;

    3 

    4 namespace CollectionDP

    5 {

    6     [TestFixture]

    7     public class MarkManagerTest

    8     {

    9         ItemCollection items=new ItemCollection();

   10 

   11         [SetUp]

   12         public void Init()

   13         {

   14             items.Add(new Item("oo"));

   15             items.Add(new Item("orm"));

   16             items.Add(new Item("aop"));

   17             items.Add(new Item("soa"));

   18         }

   19         [TearDown]

   20         public void Cler()

   21         {

   22             items.Clear();

   23         }

   24 

   25         private void MarkItemStar(Item item)

   26         {

   27             item.MarkStar();

   28         }

   29 

   30         private void MarkItemUnReaded(Item item)

   31         {

   32             item.MarkUnReaded();

   33         }

   34 

   35         [Test]

   36         public void MarkAllReadedToUnReaded()

   37         {

   38             Assert.AreEqual(4, items.Count);

   39 

   40             items.Get(0).MarkReaded();

   41             Assert.AreEqual(true,items.Get(0).ReadedFlag);

   42 

   43             items.Get(2).MarkReaded();

   44             Assert.AreEqual(true,items.Get(2).ReadedFlag);

   45 

   46             MarkManager markManager=new MarkManager(items);

   47             markManager.Mark(new Specification(), new ItemMarkUnReadedActor().Action);

   48 

   49             Assert.AreEqual(false,items.Get(0).ReadedFlag);

   50             Assert.AreEqual(false,items.Get(2).ReadedFlag);

   51         }

   52 

   53         [Test]

   54         public void MarkAllToStar()

   55         {

   56             Assert.AreEqual(4, items.Count);

   57 

   58             foreach (Item item in items.Items)

   59                 Assert.AreEqual(false,item.StarFlag);

   60 

   61             MarkManager markManager=new MarkManager(items);

   62             markManager.Mark(new Specification(), new ItemMarkStarActor().Action);

   63 

   64             foreach (Item item in items.Items)

   65                 Assert.AreEqual(true,item.StarFlag);

   66         }

   67 

   68         [Test]

   69         public void MarkSatifyedToStar()

   70         {

   71             foreach (Item item in items.Items)

   72                 Assert.AreEqual(false,item.StarFlag);

   73 

   74             items.Get(0).MarkReaded();

   75             items.Get(2).MarkReaded();

   76 

   77             MarkManager markManager=new MarkManager(items);

   78             Specification spec=new UnReadedSpecification();

   79             markManager.Mark(spec,  new ItemMarkStarActor().Action);

   80 

   81 

   82             Assert.AreEqual(false,items.Get(0).StarFlag);

   83             Assert.AreEqual(true,items.Get(1).StarFlag);

   84             Assert.AreEqual(false,items.Get(2).StarFlag);

   85             Assert.AreEqual(true,items.Get(3).StarFlag);

   86         }

   87     }

   88 }

 

Private 方法消失了, Item操作的扩展也变得极为容易.其实在SpecificationItemActor中都体现出了Open-Close原则.即对修改封闭(不是通过在原有的类中添加新的方法来实现新的功能), 对扩展开放.(通过继承实现新的功能).

现在我们看看我们的需求: 无非就是实现对随笔集合中满足一定条件的随笔做相应的操作.

条件(Specification), 操作(ItemActor) 这两个变量完全在我们的控制之中,随它们怎么变我们都能轻松应付.

 

上面这张UML图显示出了本系统的类结构. 从中我们看出以后需要改变的只有第三层,上面两层基本不会再发生变化. 系统的可维护性自然体现出来.