利用Unity對於緊密耦合情況進行鬆綁
前言
本篇針對架構分層後所產生的緊密耦合情況進行重構。
架構分層
- Presentation Layer (Web - MVC)
- Service Layer
- Data Access Layer (Entity Framework)
DbContext
public class PilotDbContext : DbContext
{
// Coustructors
public PilotDbContext()
: base(ConstantKey.DbName)
{
}
public PilotDbContext(string dbName)
: base(dbName)
{
}
// Properties
public DbSet<User> Users { get; set; }
}
Service Layer
public interface IUserService
{
// Methods
List<User> GetAll();
User Get(string userId);
}
// 線上使用: 資料來源為DB
public class UserService : IUserService
{
// Fields
protected PilotDbContext _db;
// Constructors
public UserService()
{
// 直接產生 PilotDbContext (依賴特定實作)
_db = new PilotDbContext(ConstantKey.DbName);
}
// Methods
public List<User> GetAll()
{
return _db.Users.ToList();
}
public User Get(string userId)
{
return _db.Users.FirstOrDefault(x => x.UserId == userId);
}
}
// 測試使用: 資料來源為記憶體
public class MockUserService : IUserService
{
// Methods
public List<User> GetAll()
{
return new List<Domain.Entities.User>()
{
new User(){UserId = "chris", Email="ooxx@gmail.com"}
};
}
public User Get(string userId)
{
throw new NotImplementedException();
}
}
Presentation Layer - Controller
public class UserController : Controller
{
private IUserService _userService;
public UserController()
{
// 直接產生 UserService (依賴特定實作)
this._userService = new UserService();
}
public ActionResult Index()
{
return View(_userService.GetAll());
}
}
分層後 緊密耦合關係依然存在!!
緊密耦合關係來自於程式直接依賴特定實作。舉例來說,在UserController的建構子中直接生成出特定實作IUerService介面的UserService物件,如此當我們需要進行單元測試時,就無法透過任何方式來改變符合測試需求之MockUserService實作來執行測試任務。因此以下將透過Unity來寬鬆此耦合情況。
使用Unity來寬鬆耦合情況
- 使用NuGet下載Unity Boostrapper for ASP.NET MVC
- 在App_Start中自動產生UnityConfig.cs及UnityMvcActivator.cs檔案
-
在UnityConfig.cs註冊待注入物件與介面之對應關係。此處筆者希望dbContext的生命週期依附於每個Http Request中,所以透過PerRequestLifetimeManager來對具象類別PilotDbContext進行設定;當Unity在解析PilotDbContext類別時, 會對當前的Http Request建立一新的PilotDbContext物件,而其生命週期會隨著Http Request結束而自動釋放。此外,由於PilotDbContext的建構子需傳入參數,所以須透過InjectionConstructor方式傳入。
public class UnityConfig { private static Lazy<IUnityContainer> container = new Lazy<IUnityContainer>(() => { var container = new UnityContainer(); RegisterTypes(container); return container; }); public static IUnityContainer GetConfiguredContainer() { return container.Value; } public static void RegisterTypes(IUnityContainer container) { // NOTE: To load from web.config uncomment the line below. Make sure to add a Microsoft.Practices.Unity.Configuration to the using statements. // container.LoadConfiguration(); // TODO: Register your types here // container.RegisterType<IProductRepository, ProductRepository>(); // 註冊: 要求IUserService時, 自動呼叫UserService建構函式來生成實體 container.RegisterType<IUserService, UserService>(); // 註冊: PilotDbContext生命週期會隨著Http Request結束而自動釋放 container.RegisterType<PilotDbContext>(new PerRequestLifetimeManager(), new InjectionConstructor(ConstantKey.DbName)); } }
使用多載建構子之注意事項
A. Unity預設會使用參數最多的那個建構函式生成物件
B. 參數個數最多的建構函式並不只一個,Unity拋出錯誤
C. 如果要指定預設的建構函式可加上[InjectionConstructor]標籤
D. 若具有2個建構函式套用[InjectionConstructor]標籤,Unity拋出錯誤 -
修改程式以建構子注入所需物件
Service Layer
public class UserService : IUserService { // Fields protected PilotDbContext _db; // Constructors public UserService(PilotDbContext db) { _db = db; } // Methods // ... }
Presentation Layer - Controller
public class UserController : Controller { private IUserService _userService; public UserController(IUserService userService) { this._userService = userService; } public ActionResult Index() { // DI UserService by contructor return View(_userService.GetAll()); // Resolve UserService manually //var localUserService = UnityConfig.GetConfiguredContainer().Resolve<IUserService>(); //return View(localUserService.GetAll()); } }
簡述透過Unity解析待注入物件過程
- Http Request 進入並指向 UserController的Index方法(Action)
- 生成UserController (Unity解析建構子參數IUserService產生UserService實體)
- 生成UserService (Unity解析建構子參數產生PilotDbContext實體)
- 生成PilotDbContext(確認是否已存在),並將其生命週期設定為單個Http Request內
- 依序遞迴回應上層以生成UserController執行任務
優點
- 透過Unity來控制DbContext的生命週期
- 透過注入來解耦介面與特定實體的關係 (不依賴特定實作)
-
專案開發先期可透過MockService以模擬資料進行開發 (切換自如)
public class UnityConfig { //... public static void RegisterTypes(IUnityContainer container) { // Using mock service to simulate testing data container.RegisterType<IUserService, MockUserService>(); //container.RegisterType<IUserService, UserService>(); // ... } }
參考資訊
希望此篇文章可以幫助到需要的人
若內容有誤或有其他建議請不吝留言給筆者喔 !