1. 程式人生 > >類依賴項的不透明性和透明性

類依賴項的不透明性和透明性

在 TDD 的實踐中,總是要考慮類的依賴項的透明性(Transparent)和不透明性(Opaque),進而採用合理的方式提高程式碼的可測試性。

不透明依賴

我們先看段前置條件程式碼,以供後文使用。

 1   public interface IUserProvider
 2   {
 3     IList<User> GetUserCollection();
 4   }
 5 
 6   public class UserProvider : IUserProvider
 7   {
 8     public IList<User> GetUserCollection()
9 { 10 return new List<User>() 11 { 12 new User() 13 { 14 Name = "hello", 15 LastActivity = DateTime.Now.AddDays(-1), 16 }, 17 }; 18 } 19 } 20 21 public class User 22 { 23 public string Name { get; set
; } 24 public DateTime LastActivity { get; set; } 25 }

現在,我們需要一個負責管理 User 的類 UserManager,其實現了介面 IUserManager。

 1   public interface IUserManager
 2   {
 3     int NumberOfUsersActiveInLast10Days(string userName);
 4   }
 5 
 6   public class UserManager : IUserManager
 7   {
 8     public
int NumberOfUsersActiveInLast10Days(string userName) 9 { 10 IUserProvider userProvider = ServiceLocator.Current.GetInstance<IUserProvider>(); 11 IList<User> userCollection = userProvider.GetUserCollection(); 12 int result = 0; 13 foreach (User user in userCollection) 14 { 15 if (user.Name.StartsWith(userName) 16 && user.LastActivity > DateTime.Now.AddDays(-10)) 17 result++; 18 } 19 return result; 20 } 21 }

通過 UserManager 內定義的 函式 NumberOfUsersActiveInLast10Days 我們可以得到過去 10 天內活躍的使用者數量。

 1   class Program
 2   {
 3     static void Main(string[] args)
 4     {
 5       IUnityContainer container = new UnityContainer();
 6       ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container));
 7 
 8       container.RegisterType<IUserProvider, UserProvider>(new ContainerControlledLifetimeManager());
 9       container.RegisterType<IUserManager, UserManager>(new ContainerControlledLifetimeManager());
10 
11       UserManager userManager = new UserManager();
12       int activeUserCount = userManager.NumberOfUsersActiveInLast10Days("hello");
13       Console.WriteLine(activeUserCount);
14 
15       Console.ReadKey();
16     }
17   }

在函式 NumberOfUsersActiveInLast10Days 中,我們從 ServiceLocator 中獲取了一個 IUserProvider 的實現,然後通過其獲取所有 User。再根據給定條件過濾使用者,返回過去 10 天內的活躍使用者數量。

在 UserManager 的使用中,我們並不知道其依賴於 ServiceLocator 和 UserProvider 等類。

這種將 IoC 呼叫直接嵌入到程式碼實現中的隱式使用方式稱之為不透明依賴注入

測試不透明依賴

現在我們來為 NumberOfUsersActiveInLast10Days 編寫單元測試程式碼。

第一個用例為驗證在資料庫中不存在使用者名稱以給定字串開頭的使用者。

如果我不知道 NumberOfUsersActiveInLast10Days 的內部實現,採用黑盒測試的方式,我會寫出如下程式碼。

 1     [TestMethod]
 2     public void GetActiveUsers_TestCaseOfZeroUsers_WouldReturnEmptyCollection()
 3     {
 4       // arrange
 5       // no clear idea what to mock here
 6 
 7       // act
 8       var userManager = new UserManager();
 9       int numberOfUsers = userManager.NumberOfUsersActiveInLast10Days("x");
10 
11       // assert
12       Assert.IsTrue(numberOfUsers == 0);
13     }

則執行測試用例後,得到的結果是:

"未將物件引用設定到物件的例項。"

此時,我們知道了 NumberOfUsersActiveInLast10Days 函式還要依賴 ServiceLocator 和 UserProvider 類。

現在,我們來改進測試程式碼。

 1     [TestMethod]
 2     public void GetActiveUsers_TestCaseOfZeroUsers_WouldReturnEmptyCollection()
 3     {
 4       // arrange
 5       IUnityContainer container = new UnityContainer();
 6       ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container));
 7 
 8       IUserProvider userProvider = Substitute.For<IUserProvider>();
 9       userProvider.GetUserCollection().Returns<IList<User>>(new List<User>());
10       container.RegisterInstance<IUserProvider>(userProvider, new ContainerControlledLifetimeManager());
11 
12       // act
13       var userManager = new UserManager();
14       int numberOfUsers = userManager.NumberOfUsersActiveInLast10Days("x");
15 
16       // assert
17       Assert.IsTrue(numberOfUsers == 0);
18     }

則現在我們可以通過此測試了。

透明依賴

可以看到,在程式碼中使用不透明依賴將導致為程式碼編寫單元測試變得困難和不可預測。

現在我來將依賴項重構為透明依賴,通過建構函式將依賴注入。

 1   public class UserManager : IUserManager
 2   {
 3     private readonly IUserProvider _userProvider;
 4 
 5     public UserManager(IUserProvider userProvider)
 6     {
 7       _userProvider = userProvider;
 8     }
 9 
10     public int NumberOfUsersActiveInLast10Days(string userName)
11     {
12       IList<User> userCollection = _userProvider.GetUserCollection();
13       int result = 0;
14       foreach (User user in userCollection)
15       {
16         if (user.Name.StartsWith(userName)
17             && user.LastActivity > DateTime.Now.AddDays(-10))
18           result++;
19       }
20       return result;
21     }
22   }

程式碼的使用也需稍作修改。

1       UserManager userManager = new UserManager(container.Resolve<IUserProvider>());
2       int activeUserCount = userManager.NumberOfUsersActiveInLast10Days("hello");
3       Console.WriteLine(activeUserCount);

這種可以明確的通過建構函式顯式的注入的依賴項稱之為透明依賴

測試透明依賴

改進測試程式碼,直接去掉了對 ServiceLocator 的依賴。

 1     [TestMethod]
 2     public void GetActiveUsers_TestCaseOfZeroUsers_WouldReturnEmptyCollection()
 3     {
 4       // arrange
 5       IUserProvider userProvider = Substitute.For<IUserProvider>();
 6       userProvider.GetUserCollection().Returns<IList<User>>(new List<User>());
 7 
 8       // act
 9       var userManager = new UserManager(userProvider);
10       int numberOfUsers = userManager.NumberOfUsersActiveInLast10Days("x");
11 
12       // assert
13       Assert.IsTrue(numberOfUsers == 0);
14     }

這一次執行順利的通過。

結論

通過使用透明依賴方式,可以極大的簡化測試編寫過程,並且可以引導更簡潔的設計。同時,配合 IoC 容器的合理使用將極大的發揮依賴注入的能力。

參考資料