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的, 但是想到Readed和Star状态可以同时存在所以, 还是用两个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操作的扩展也变得极为容易.其实在Specification和ItemActor中都体现出了Open-Close原则.即对修改封闭(不是通过在原有的类中添加新的方法来实现新的功能), 对扩展开放.(通过继承实现新的功能).
现在我们看看我们的需求: 无非就是实现对随笔集合中满足一定条件的随笔做相应的操作.
条件(Specification), 操作(ItemActor) 这两个变量完全在我们的控制之中,随它们怎么变我们都能轻松应付.
上面这张UML图显示出了本系统的类结构. 从中我们看出以后需要改变的只有第三层,上面两层基本不会再发生变化. 系统的可维护性自然体现出来.