[Refactoring] 利用Unity對於緊密耦合情況進行鬆綁

利用Unity對於緊密耦合情況進行鬆綁

前言

本篇針對架構分層後所產生的緊密耦合情況進行重構。

架構分層

  1. Presentation Layer (Web - MVC)
  2. Service Layer
  3. 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來寬鬆耦合情況

  1. 使用NuGet下載Unity Boostrapper for ASP.NET MVC
  2. 在App_Start中自動產生UnityConfig.cs及UnityMvcActivator.cs檔案
  3. 在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拋出錯誤

  4. 修改程式以建構子注入所需物件

    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解析待注入物件過程

  1. Http Request 進入並指向 UserController的Index方法(Action)
  2. 生成UserController (Unity解析建構子參數IUserService產生UserService實體)
  3. 生成UserService (Unity解析建構子參數產生PilotDbContext實體)
  4. 生成PilotDbContext(確認是否已存在),並將其生命週期設定為單個Http Request內
  5. 依序遞迴回應上層以生成UserController執行任務

優點

  1. 透過Unity來控制DbContext的生命週期
  2. 透過注入來解耦介面與特定實體的關係 (不依賴特定實作)
  3. 專案開發先期可透過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>();
            // ...
        }
    }
    

參考資訊

http://huan-lin.blogspot.com/2014/04/my-di-book.html


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !