當閱讀了dynamic型別有關的C# 4.0白皮書時,我很自然的想到了TDD(Test Diven Development),TDD原本意圖讓設計師在撰寫真正程式碼前撰寫測試碼,這個立意很好,因為大多數的設計師總是在完成程式後再來考慮撰寫測試碼,結果是測試碼永遠跟不上真正的程式碼,被放棄的機率高的嚇人。
C# 4.0 New Feature : Dynamic Programming And TDD
文/黃忠成
當閱讀了dynamic型別有關的C# 4.0白皮書時,我很自然的想到了TDD(Test Diven Development),TDD原本意圖讓設計師在撰寫真正程式碼前撰寫測試碼,這個立意很好,因為大多數的設計師總是在完成程式後再來考慮撰寫測試碼,結果是測試碼永遠跟不上真正的程式碼,被放棄的機率高的嚇人。但TDD的執行流程中存在著許多困難點,例如如何在沒有真實程式碼的情況下撰寫測試碼?又如何在沒有資料庫的情況下,撰寫相關的資料庫測試程式碼?這使得我在講述TDD後學員們總是聽聽就算了,僅有少數會肯真正的遵循TDD模式,而這些少數,多半也是受命於上面的要求而行之。
在Visual Studio 2010中,dynamic戲劇化的解決了TDD的幾個問題,在此就讓我以一個TDD例子來演示此應用。
首先透過Visual Studio 2010來建立一個Class Library Project,修改Class1.cs的內容如下。
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ClassLibrary2 { public class Calculator { } } |
接著建立Test Project。
圖1
圖2
圖3
移除預設的CalculatorConstructorTest函式,加入以下函式:
[TestMethod()] public void TestCalcSum() { dynamic o = new ClassLibrary2.Calculator(); int result = o.Sum(15, 15); Assert.AreEqual(result, 30); } |
受dynamic之協助,這段程式碼是可以編譯的,但執行測試會是紅燈。
圖4
圖5
圖6
這很正常,TDD一開始一定是紅燈,接著我們要讓它變綠燈,先修改成下列這樣。
[TestMethod()] public void TestCalcSum() { ClassLibrary2.Calculator o = new ClassLibrary2.Calculator(); int result = o.Sum(15, 15); Assert.AreEqual(result, 30); } |
然後於o.Sum的Sum處按右鍵。
圖7
選擇Method Stub後按下,接著可於class1.cs中發現Sum函式的定義,於此添加真正的實作。
public int Sum(int val1, int val2) { return val1 + val2; } |
完成後編譯並重新測試,綠燈就會出現了。
圖8
這就是TDD於Visual Studio 2010的IDE及dynamic協助下的實踐,在DynamicObject的協助下,也可以輕易的做出TDD中Mock Object的寫法。
OK,就讓我們走更遠一些,以Northwind資料庫為例,在TDD的原則下,於撰寫資料庫相關測試碼時,最完美的時間點是在連資料庫都沒有的時候,dynamic可以幫到我們什麼忙呢?先修改Class1.cs如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Dynamic; namespace ClassLibrary2 { public class Calculator { public int Sum(int val1, int val2) { return val1 + val2; } } public class DynamicDataContext : DynamicObject { internal Dictionary<dynamic, dynamic> _bags = new Dictionary<dynamic, dynamic>(); public override bool TryGetMember(GetMemberBinder binder, out object result) { if (_bags.Keys.Contains(binder.Name)) result = _bags[binder.Name]; else { TableObject<dynamic> table = new TableObject<dynamic>(); table._context = this; _bags.Add(binder.Name, table); result = _bags[binder.Name]; } return true; } public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result = null; if (binder.Name.Equals("SubmitChanges")) return true; return false; } } public class PropertyCollectionObject : DynamicObject { private Dictionary<dynamic, dynamic> _bags = new Dictionary<dynamic, dynamic>(); public override bool TryGetMember(GetMemberBinder binder, out object result) { if (_bags.Keys.Contains(binder.Name)) result = _bags[binder.Name]; else result = null; return result != null; } public override bool TrySetMember(SetMemberBinder binder, object value) { if (_bags.Keys.Contains(binder.Name)) _bags[binder.Name] = value; else _bags.Add(binder.Name, value); return true; } } public class TableObject<T> : DynamicObject, IList<T> { internal DynamicDataContext _context = null; private List<T> items = new List<T>(); public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { result = null; if (binder.Name.Equals("InsertOnSubmit")) items.Add((T)args[0]); else if (binder.Name.Equals("DeleteOnSubmit")) items.Remove((T)args[0]); return true; } public int IndexOf(T item) { return items.IndexOf(item); } public void Insert(int index, T item) { items.Insert(index, item); } public void RemoveAt(int index) { items.RemoveAt(index); } public T this[int index] { get { return items[index]; } set { items[index] = value; } } public void Add(T item) { items.Add(item); } public void Clear() { items.Clear(); } public bool Contains(T item) { return items.Contains(item); } public void CopyTo(T[] array, int arrayIndex) { items.CopyTo(array, arrayIndex); } public int Count { get { return items.Count; } } public bool IsReadOnly { get { return false; } } public bool Remove(T item) { return items.Remove(item); } public IEnumerator<T> GetEnumerator() { return items.GetEnumerator(); } System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return items.GetEnumerator(); } } public class NorthwindDataContext : DynamicDataContext { } public class Customer : PropertyCollectionObject { } } |
這段程式碼不難,只是DynamicObject的應用罷了,有趣的是我做出了DynamicDataContext及TableObject,一個扮演著LINQ To SQL的DataContext角色,一個則扮演著LINQ To SQL中的Entity Class角色,只要有這兩個類別,你便可以變出任何一個DataContext及任何一個Entity Class,差別只是命名而已,接著讓我們來看看測試碼怎麼寫?
using System.Linq; ......... [TestMethod] public void TestCustomersAdd() { dynamic context = new NorthwindDataContext(); dynamic c = new Customer(); IEnumerable<dynamic> table = (IEnumerable<dynamic>)context.Customers; c.CustomerID = "A9010"; c.CompanyName = "GIS"; context.Customers.InsertOnSubmit(c); context.SubmitChanges(); dynamic result = (from s1 in table where s1.CustomerID == "A9010" select s1).First(); Assert.AreEqual(result.CustomerID, "A9010"); } |
猜猜這結果是什麼?綠燈!
圖9
接著讓我們玩真的,註解掉Class1中的NorthwindDataContext及Customers兩個類別,然後加入LINQ To SQL Classes,並把Northwind資料庫的Customers拖進去。
圖10
編譯後會得到一個錯誤訊息,提示我們沒有於Test Project中添加System.Data.Linq的Reference,加入後重新編譯即可,接著重新執行測試。
圖11
再看看資料庫。
圖12
事情還沒完哦,先手動把這筆資料刪掉,接著進行重構(refactoring)。
[TestMethod] public void TestCustomersAdd() { NorthwindDataContext context = new NorthwindDataContext(); Customer c = new Customer(); IEnumerable<Customer> table = (IEnumerable<Customer>)context.Customers; c.CustomerID = "A9010"; c.CompanyName = "GIS"; context.Customers.InsertOnSubmit(c); context.SubmitChanges(); dynamic result = (from s1 in table where s1.CustomerID == "A9010" select s1).First(); Assert.AreEqual(result.CustomerID, "A9010"); } |
歡迎進入TDD With Visual Studio 2010 And C#的世界。
dynamic!是惡魔還是天使?
我想我很難告訴讀者,dynamic的出現到底是好事還是壞事,每個語言做出變革時,總有著擁護者及反對者,的確!dynamic的不定型,會讓不了解dynamic用途的設計師寫出難以維護及除錯的程式碼,但也因為不定型,所以能簡化許多工作,熟優熟劣,端看程式設計師有多了解dynamic設計真正的用途,並謹慎的將其用在刀口上,而不是盲目的使用它。
最後就讓我下一個定義吧,這只是我初步的結論,遵循與否就看個人了:
一、將dynamic用在與COM、JavaScript、IronPython、IronRuby的互通及整合上。
二、將dynamic用在TDD的過程中,但實體碼成形後,立即進行refactoring,並移除dynamic。
三、將dynamic用在Framework的撰寫上,以增加Framework的延展性為設計目的。
除了以上三點外,我想我不會把dynamic用在其它的地方,尤其是一般的應用程式。