再次調整項目架構是因為和群友dezhou的一次聊天,我原來的想法是項目儘量做簡單點別搞太複雜了,僅使用了DbContext的註入,其他的也沒有寫介面耦合度很高。和dezhou聊過之後我仔細考慮了一下,還是解耦吧,本來按照軟體設計模式就應該是高內聚低耦合的,低耦合使項目的模塊獨立於其他模塊,增加了可維... ...
再次調整項目架構是因為和群友dezhou的一次聊天,我原來的想法是項目儘量做簡單點別搞太複雜了,僅使用了DbContext的註入,其他的也沒有寫介面耦合度很高。和dezhou聊過之後我仔細考慮了一下,還是解耦吧,本來按照軟體設計模式就應該是高內聚低耦合的,低耦合使項目的模塊獨立於其他模塊,增加了可維護性和移植性!
註:前面寫的博客詳細記錄沒項目操作的每一步,其實寫起博客來很費時間,而且整片博文里很多無用的信息。對MVC來說會添加控制器,添加視圖,添加類這些都最基本的要求了,並且前面博文里都寫了,後面也就不再詳細寫這些東西了,主要寫一些思路和關鍵代碼,具體內容以源代碼的形式放在博客後面提供下載。
一、預設項目結構
我們看一下,vs2015預設生成的項目結構。
項目中模型、數據訪問、業務邏輯和視圖相關的內容都在一個項目中,視圖、業務邏輯和顯示緊緊耦合,前期看著還沒什麼,到了內容多了項目變大以後,尤其是隔一段時間再更新項目,在看的話一片混亂,有時候一個小的改動造成整個項目導出報錯,頭痛之極。
二、三層架構
我們再看看三層架構:
- 用戶界面表示層(USL)
- 業務邏輯層(BLL)
- 數據訪問層(DAL)
三層架構主要是使項目結構更清楚,分工更明確,有利於後期的維護和升級。它未必會提升性能,因為當子程式模塊未執行結束時,主程式模塊只能處於等待狀態。這說明將應用程式劃分層次,會帶來其執行速度上的一些損失。但從團隊開發效率角和維護性上來說易於進行任務分配,可維護性高。
按照三層的思想,MVC中的控制器(C)和視圖(V)都是處理界面顯示相關的內容屬於用戶界面表示層(USL) ,模型(M)是控制器和視圖間交換的數據,所以MVC框架應該都屬於三層中的用戶界面表示層。
數據訪問層(DAL)和業務邏輯層(BLL) 、業務邏輯層和用戶界面表示層(USL) 也要交換數據,乾脆把模型(M)獨立出來,作為控制器和視圖,及三個層次之間交換的數據。
三、高耦合
我們看向Ninesky現在的項目結構,如下圖:
包含四個項目:
Ninesky.DataLibrary是數據訪問層,提供資料庫訪問的支持。
Ninesky.Base 是業務邏輯層,負責業務邏輯的處理。
Ninesky.Web 用戶界面表示層(USL),負責顯示頁面和顯示項目的邏輯處理。
Ninesky.Models 就是各層之間交換的數據實體。
從以上可以看到項目按照三層的思想進行了分層。PS:有群友問為什麼項目名稱叫DataLibrary、Base,不叫DAL,BLL?這可能是強迫症的原因,我反正看著DAL,BLL的項目名稱特別不舒服,改了個自己喜歡的名字,其實功能都一樣的。
再看一下項目的調用
看一下Ninesky.Base的CategoryService類。
代碼中位置1聲明瞭類CategoryRepository,這個類是 Ninesky.DataLibrary中的一個類。位置2將這個項目實例化了,在位置3處我們直接調用了這個類的Find方法。從上面可以看出CategoryService類是依賴CategoryRepository類的;Ninesky.Base項目是依賴於Ninesky.DataLibrary項目的。一個項目的類精確的調用了另一個項目類的方法那麼他們之間就是高耦合。發生高耦合就是軟體設計有問題,就要解耦,把依賴實現代碼轉換成依賴邏輯,這時候就要引入抽象層(通常是介面)。
四、依賴介面
我們添加一個dll項目Ninesky.InterfaceDataLibrary,給Ninesky.DataLibrary添加對Ninesky.InterfaceDataLibrary項目的引用。
在Ninesky.InterfaceDataLibrary項目添加InterfaceBaseRepository介面
1 using System; 2 using System.Collections.Generic; 3 using System.Linq.Expressions; 4 using System.Threading.Tasks; 5 6 namespace Ninesky.InterfaceDataLibrary 7 { 8 /// <summary> 9 /// 倉儲基類介面 10 /// </summary> 11 /// <typeparam name="T"></typeparam> 12 public interface InterfaceBaseRepository<T> where T : class 13 { 14 /// <summary> 15 /// 查詢[不含導航屬性] 16 /// </summary> 17 /// <param name="predicate">查詢表達式</param> 18 /// <returns>實體</returns> 19 T Find(Expression<Func<T, bool>> predicate); 20 } 21 }View Code修改BaseRepository代碼,讓BaseRepository繼承InterfaceBaseRepository
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Linq.Expressions; 5 using Microsoft.EntityFrameworkCore; 6 using Ninesky.InterfaceDataLibrary; 7 8 namespace Ninesky.DataLibrary 9 { 10 /// <summary> 11 /// 倉儲基類 12 /// </summary> 13 public class BaseRepository<T> :InterfaceBaseRepository<T> where T : class
14 { 15 protected DbContext _dbContext; 16 public BaseRepository(DbContext dbContext) 17 { 18 _dbContext = dbContext; 19 } 20 21 /// <summary> 22 /// 查詢[不含導航屬性] 23 /// </summary> 24 /// <param name="predicate">查詢表達式</param> 25 /// <returns>實體</returns> 26 public virtual T Find(Expression<Func<T, bool>> predicate) 27 { 28 return _dbContext.Set<T>().SingleOrDefault(predicate); 29 } 30 } 31 } 32View Code
在Ninesky.Base項目中引用Ninesky.InterfaceDataLibrary,我們在修改CategoryService代碼
1 public class CategoryService 2 { 3 private InterfaceBaseRepository<Category> _categoryRepository; 4 public CategoryService(DbContext dbContext) 5 { 6 _categoryRepository = new BaseRepository<Category>(dbContext); 7 } 8 9 /// <summary> 10 /// 查找 11 /// </summary> 12 /// <param name="Id">欄目Id</param> 13 /// <returns></returns> 14 public Category Find(int Id) 15 { 16 return _categoryRepository.Find(c => c.CategoryId == Id); 17 } 18 }View Code
在代碼開始處聲明瞭變數類型為InterfaceBaseRepository的變數,在構造函數中將InterfaceBaseRepository實例化為BaseRepository類型。
現在Ninesky.Base項目依然Ninesky.DataLibrary項目進行了依賴,並沒有進行解耦,如果要想解除多Ninesky.DataLibrary的依賴就要想辦法把介面的實例化轉移到項目之外去。
五、控制反轉
控制反轉就是把依賴的創建移到類的外部。那麼我們修改CategoryService類的構造函數。
構造函數傳遞了一個介面類型的參數,現在類中完全和Ninesky.DataLibrary沒有了關係,可以刪除對Ninesky.DataLibrary項目的引用了。
那現在又有了一個新的問題:控制反轉如何實現,怎麼進行介面的實例化?
常用的解決方法有服務定位器和依賴註入。
六、服務定位器
服務定位器就是在類中集中進行實例化。
單獨創建一個項目,添加對項目的引用,然後再工廠類中集中進行實例化。
1 public class Factory 2 { 3 public InterfaceBaseRepository<Category> GetBaseRepository() 4 { 5 return new BaseRepository<Category>(); 6 } 7 }View Code
服務定位器的好處是實現比較簡單,可以創建一個全局的服務定位器,缺點就是組件需求不透明。Ninesky採用另一種控制反轉的實現:依賴註入。
七、依賴註入。
以前.Net MVC中註入挺麻煩的,幸好.Net Core MVC中內建了依賴註入的支持。
修改CategoryController代碼,使用構造函數註入。這裡為了例子的簡單在控制器中直接使用數據存儲層的類進行註入,而沒有使用業務邏輯層的類。
控制器中採用構造函數註入,構造函數中傳遞CategoryService參數。
1 public class CategoryController : Controller 2 { 3 /// <summary> 4 /// 數據上下文 5 /// </summary> 6 private NineskyDbContext _dbContext; 7 8 /// <summary> 9 /// 欄目服務 10 /// </summary> 11 private CategoryService _categoryService; 12 13 public CategoryController(CategoryService categoryService) 14 { 15 _categoryService = categoryService; 16 } 17 18 /// <summary> 19 /// 查看欄目 20 /// </summary> 21 /// <param name="id">欄目Id</param> 22 /// <returns></returns> 23 [Route("/Category/{id:int}")] 24 public IActionResult Index(int id) 25 { 26 var category = _categoryService.Find(id); 27 if (category == null) return View("Error", new Models.Error { Title = "錯誤消息", Name="欄目不存在", Description="訪問ID為【"+id+"】的欄目時發生錯誤,該欄目不存在。" }); 28 switch (category.Type) 29 { 30 case CategoryType.General: 31 if (category.General == null) return View("Error",new Models.Error { Title="錯誤消息", Name="欄目數據不完整",Description="找不到欄目【"+category.Name+"】的詳細數據。" }); 32 return View(category.General.View, category); 33 case CategoryType.Page: 34 if (category.Page == null) return View("Error", new Models.Error { Title = "錯誤消息", Name = "欄目數據不完整", Description = "找不到欄目【" + category.Name + "】的詳細數據。" }); 35 return View(category.Page.View, category); 36 case CategoryType.Link: 37 if (category.Link == null) return View("Error", new Models.Error { Title = "錯誤消息", Name = "欄目數據不完整", Description = "找不到欄目【" + category.Name + "】的詳細數據。" }); 38 return Redirect(category.Link.Url); 39 default: 40 return View("Error", new Models.Error { Title = "錯誤消息", Name = "欄目數據錯誤", Description = "欄目【" + category.Name + "】的類型錯誤。" }); 41 42 } 43 } 44 }View Code
然後我們進入,Web的啟動類Startup進行註入。如下圖:
第一個紅框內是在《2.1、欄目的前臺顯示》中註入的上下文;
第二個紅框到第四個紅框內是今天添加的內容。
第三個紅框內註入InterfaceBaseRepository介面,使用BaseRepository進行實例化。
有第二個紅框的內容是因為BaseRepository實例化時有一個DbContext類型的參數。在註入的時候要求用到的參數必須要在前面註入,並且系統並不會自動吧NineskyDbContext轉換為DbContext。所以必須註入一個DbContext類型的參數。
第四個紅框是註入CategoryService。這裡CategoryService同樣可以使用介面,時間原因沒寫。
至此可以看到,CategoryService解除了對BaseRepository的依賴,在Ninesky.Base項目中沒有對Ninesky.DataLibrary進行任何的依賴,類的實例化是在Web項目中進行註入的,Web項目對Ninesky.DataLibrary進行了依賴。同樣的方法也可以實現Web項目對Ninesky.Base項目的解耦。
如果要完全解除Ninesky.Web項目對Ninesky.DataLibrary和Ninesky.Base項目的依賴,可以使用配置文件載入,這次先不寫了。
F5瀏覽器中查看一下,可以看到取出了數據,只是因為數據存儲層的代碼沒有包含導航屬性所以數據不完整。
八、其他
代碼托管地址:https://git.oschina.net/ninesky/Ninesky
文章發佈地址:http://www.ninesky.cn
代碼包下載:Ninesky2.3項目架構調整-控制反轉和依賴註入的使用.rar