C# 4.0 New Feature : Dynamic Programming And TDD

當閱讀了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.SumSum處按右鍵。
圖7
選擇Method Stub後按下,接著可於class1.cs中發現Sum函式的定義,於此添加真正的實作。

 

public int Sum(int val1, int val2)
{
    return val1 + val2;
}
完成後編譯並重新測試,綠燈就會出現了。
圖8
這就是TDDVisual Studio 2010IDEdynamic協助下的實踐,在DynamicObject的協助下,也可以輕易的做出TDDMock 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的應用罷了,有趣的是我做出了DynamicDataContextTableObject,一個扮演著LINQ To SQLDataContext角色,一個則扮演著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中的NorthwindDataContextCustomers兩個類別,然後加入LINQ To SQL Classes,並把Northwind資料庫的Customers拖進去。
10
編譯後會得到一個錯誤訊息,提示我們沒有於Test Project中添加System.Data.LinqReference,加入後重新編譯即可,接著重新執行測試。
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用在與COMJavaScriptIronPythonIronRuby的互通及整合上。
二、將dynamic用在TDD的過程中,但實體碼成形後,立即進行refactoring,並移除dynamic
三、將dynamic用在Framework的撰寫上,以增加Framework的延展性為設計目的。
 
除了以上三點外,我想我不會把dynamic用在其它的地方,尤其是一般的應用程式。