序:忙碌多事的八月帶著些許的倦意早已步入尾聲,金秋九月承載著抗戰勝利70週年的喜慶撲面而來。沒來得及任何準備,似乎也不需要任何準備,因為生活不需要太多將來時。每天忙著上班、加班、白加班,忘了去憤,忘了去算計所謂的價值。天津爆炸事故時刻警示著我們生命的無常,逝者安息,活著的人生活還得繼續,珍惜生命,遠離傷害。武漢,這座炙熱的城市,雖值金秋,卻依然經受著“秋老虎”的烘烤,馬路上蒸騰的熱氣迎面襲來,全身毛孔張開,汗流不止,在這般高溫下,似乎汗水都要被榨乾,其實,被榨乾的何止是汗水!!!籲!籲!籲!說好的MEF呢?說好的面向介面程式設計呢?都快奔三張的人了,還學著小年輕玩無病呻吟,有點裝嫩的味道。沒辦法,思想脫韁了,有點野性難馴的意思了。好啦,不扯啦,進入今天的正題吧。
前面兩篇分別介紹了下MEF的簡單用法和MEF與倉儲模式的結合使用,這章來個終結吧。毛爺爺教導我們,做事要有始有終。本篇,博主打算通過分享一個面向介面程式設計的框架來說明使用MEF的靈活性。
1、面向介面程式設計:有一定程式設計經驗的博友應該都熟悉或者瞭解這種程式設計思想,層和層之間通過介面依賴,下層不是直接給上層提供服務,而是定義一組介面供上層呼叫。至於具體的業務實現,那是開發中需要做的事情,在專案架構階段,只需要定義好層與層之間的介面依賴,將框架搭起來,編譯可以直接通過。為什麼要有這麼一種設計?既然是架構設計,當然是為了提高架構的靈活性,降低層和層之間的依賴(耦合)。
2、博主本著“不講清楚誓不罷休”的原則,自己從零開始搭了一個簡單的框架Demo,當然,可能對於大牛們來說是沒太大價值的,但請不要笑話博主不斷探索的勇氣。先來看看框架大概的結構吧。
首先說明下各層次的意思:
一、ESTM.Client
ESTM.Client.Winform:Winform專案,使用者UI展現,這個沒什麼好說的。
ESTM.Client.IBLL:客戶端IBLL介面層,用於定義客戶端的業務介面,記住這裡僅僅是向UI層提供介面功能。
ESTM.Client.BLL:客戶端BLL實現層,用於客戶端IBLL介面層的實現,提供UI層真是業務邏輯。
二、ESTM.Common
ESTM.Common.Model:通用DTOModel層,注意,這裡不是EF的實體Model,而是另外定義的一個資料轉換的Model層。
三、ESTM.Service
ESTM.Service.WCF:WCF宿主專案,用於提供WCF的介面契約和實現。這裡用WCF的目的是為了隔離客戶端和服務端的程式碼。
ESTM.Service.IBLL:服務端IBLL介面層,用於定義WCF層的業務介面,和ESTM.Client.IBLL層的功能類似。
ESTM.Service.BLL:服務端BLL實現層,實現服務端IBLL介面層。
ESTM.Service.DAL:服務端DAL資料訪問層,裡面使用EF建立資料庫連線。
再來看看各層次之間的呼叫關係:
最後說說這樣設計的好處:
(1)整個框架採用面向介面程式設計模式,每個層次不是直接向其上層提供服務(即不是直接例項化在上層中),而是通過定義一組介面,僅向上層暴露其介面功能,上層對下層僅僅是介面依賴,而不依賴具體實現。如是說,客戶端IBLL介面層僅僅提供一套介面供UI層呼叫,對於UI層來說,它根本感覺不到客戶端BLL實現層的存在,極端點說,即使不寫BLL實現層,專案也可以編譯通過,因為介面的功能已經定義好了。至於具體的實現,那就是業務的問題了。當我們需要更改業務邏輯時,只需要更改BLL實現層的程式碼就好了,對於IBLL介面層和上層UI不用做任何的改變,更進一步說,甚至將客戶端BLL實現層全部重寫或者整個替換掉,IBLL和UI層都可以不做任何改變。這也正是面向介面程式設計最大的優勢。
(2)上張圖裡面也提到了DTOModel層,為什麼要有DTOModel這麼一個物件,而不是直接將EF的實體Model傳到前端來呢?個人覺得原因有兩點:一是上文提到的安全性問題,客戶端永遠只能操作DTOmodel,當客戶端提交資料到後臺來時,永遠都是先將DTOmodel轉換位EF的model,然後去運算元據庫,試想,如果UI表現層能直接操作EF的model,是否會造成運算元據庫的入口的不唯一的問題;二是,比如資料庫裡面有A和B兩張表,我們前端需要展示A表的A.1、A.2兩欄位,還需要展示B表的B.3、B.4欄位,當我們使用DTOmodel的時候,只需要構造好一個DTO_Model,裡面有4個欄位,前端可以直接拿來用就好了,如果不用DTO,要麼直接傳object,要麼將A、B兩張表的模型傳過來在前端構造,無論哪種方式應該都沒有使用DTO方便吧。
當然這些都是博主自己的理解,如果博友們覺得有問題可以指出~~
好了,說了這麼多框架,下面進入今天的正題。看看MEF是如何在專案中飛的吧~~先來看看各層的程式碼:
(1)ESTM.Service.DAL裡面通過EF建立資料庫的連線 :博主為了測試隨便拖了一張使用者表進來。
Base.cs裡面通過MEF匯入EF的上下文物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Base { [Import] public DbContext EntityFramework { set; get; } public Base() { //因為這裡有Import,所以需要裝配MEF regisgter().ComposeParts(this); } public CompositionContainer regisgter() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); return container; } } |
對應在Export在edmx檔案下面的MyModel.Context.cs裡面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
[Export(typeof(DbContext))] public partial class Entities : DbContext { public Entities() : base("name=Entities") { } protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); } public DbSet TB_USERS { get; set; } } |
(2)ESTM.Service.IBLL服務端IBLL介面層定義服務端介面:
1 2 3 4 5 6 |
public interface IServiceUser { List GetAllUser(); void AddUser(DTO_USERS oUser); } |
(3)ESTM.Service.BLL服務端BLL實現層定義介面實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
[Export("Users",typeof(IServiceUser))] public class ServiceUser : IServiceUser { //需要注意:1.新增服務引用在Client.Bll裡面,所以,WCF連線的配置要拷貝到Winform專案下面的App.Config裡面 //2.DAL裡面的連線字串也要拷貝到WCF裡面,原因同上 public List GetAllUser() { var lstRes = new List(); var oService = new DAL.ServiceUser(); var lstEFModel = oService.GetAllUsers(); //一般用AutoMapper將EF的Model轉換成DTO的Model.z這裡為了測試,我們暫且手動轉換。使用反射轉換 var lstEFModelProp = typeof(TB_USERS).GetProperties(); var lstDTOModelProp = typeof(DTO_USERS).GetProperties(); foreach (var oEFModel in lstEFModel) { var oResUser = new DTO_USERS(); foreach (var oProp in lstEFModelProp) { var oDTOMOdelProp = lstDTOModelProp.FirstOrDefault(x => x.Name == oProp.Name); if (oDTOMOdelProp == null) { continue; } oDTOMOdelProp.SetValue(oResUser, oProp.GetValue(oEFModel)); } lstRes.Add(oResUser); } return lstRes; } public void AddUser(DTO_USERS oUser) { } |
注意在BLL實現層裡面有EF的Model和DTOmodel之間的轉換,因為在DAL裡面取到的是EF的實體模型,而需要傳到前端的是DTOmodel的模型,專案中一般用AutoMapper等第三方工具轉換物件,我這裡為了簡單自己手動通過反射轉了下。
(4)ESTM.Service.WCF服務端WCF宿主層,定義WCF的介面契約。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
static void Main(string[] args) { var strUri = "http://127.0.0.1:1234/MyWCF.Server"; Uri httpAddress = new Uri(strUri); using (ServiceHost host = new ServiceHost(typeof(CSOAService)))//需要新增System.SystemModel這個dll。。。。CSOAService這個為實現ICSOAService的實現類,WCF真正的實現方法再這個類裡面 { ///////////////////////////////////////新增服務節點/////////////////////////////////////////////////// host.AddServiceEndpoint(typeof(ICSOAService), new WSHttpBinding(), httpAddress);//ICSOAService這個為向外暴露的介面 if (host.Description.Behaviors.Find<ServiceMetadataBehavior>() == null) { ServiceMetadataBehavior behavior = new ServiceMetadataBehavior(); behavior.HttpGetEnabled = true; behavior.HttpGetUrl = httpAddress; host.Description.Behaviors.Add(behavior); } host.Opened += delegate { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("MyWCF.Server服務已經啟動成功。" + strUri); }; host.Open(); while (true) { Console.ReadLine(); } } } |
1 2 3 4 5 6 |
[ServiceContract] public interface ICSOAService { [OperationContract] List<DTO_USERS> GetAllUsers(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class CSOAService:ICSOAService { [Import("Users")] public IServiceUser Service { set; get; } public CSOAService() { regisgterAll().ComposeParts(this); } public List<DTO_USERS> GetAllUsers() { return Service.GetAllUser(); } public CompositionContainer regisgterAll() { AggregateCatalog aggregateCatalog = new AggregateCatalog(); var thisAssembly = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory, "*.dll"); aggregateCatalog.Catalogs.Add(thisAssembly); var _container = new CompositionContainer(aggregateCatalog); return _container; } |
程式碼沒什麼複雜的邏輯,就是先註冊MEF例項化變數,然後取值。[Import(“Users”)]這裡有匯入,根據我們前兩篇的講解,那麼肯定是存在一個[Export(“Users”)]這樣的匯出,於是乎,我們可以根據IServiceUser 介面往下找,最後可以找到在ESTM.Service.BLL這個裡面有一個如下的匯出:
1 2 3 4 5 |
[Export("Users",typeof(IServiceUser))] public class ServiceUser : IServiceUser { //........ } |
(5)ESTM.Client.IBLL客戶端IBLL介面層
1 2 3 4 |
public interface IManagerUser { List<DTO_USERS> GetAllUser(); } |
(6)ESTM.Client.BLL客戶端BLL實現層
1 2 3 4 5 6 7 8 9 10 11 |
[Export("Users",typeof(IManagerUser))] public class ManagerUser : IManagerUser { public List<Common.Model.DTO_USERS> GetAllUser() { //WCF服務物件 var oWCFService = new ServiceReference_MyWCF.CSOAServiceClient(); return oWCFService.GetAllUsers().ToList(); } } |
在這個層裡面是通過WCF服務去呼叫資料的,所以需要新增WCF的服務引用。
(7)ESTM.Client.Winform客戶端UI層:定義一個DataGridView展示列表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public partial class Form1 : Form { [Import("Users")] public IManagerUser Manager { set; get; } public Form1() { InitializeComponent(); regisgterAll().ComposeParts(this); this.dataGridView1.DataSource = Manager.GetAllUser(); } public CompositionContainer regisgterAll() { AggregateCatalog aggregateCatalog = new AggregateCatalog(); var thisAssembly = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory, "*.dll"); aggregateCatalog.Catalogs.Add(thisAssembly); var _container = new CompositionContainer(aggregateCatalog); return _container; } } |
得到結果:
前面MEF的第一篇中已經說過使用MEF的優勢之一就是降低層與層之間的耦合,我們現在來結合框架說說它是如何作業的。首先我們來看看ESTM.Client.Winform這個專案的引用:
它是沒有新增ESTM.Client.BLL這一層的引用的,可是我們在Form1.cs裡面有如下程式碼:
程式執行起來,走完註冊MEF以後可以看到Manager的變數值就是ESTM.Client.BLL裡面的ManagerUser物件。這就是MEF的功勞,當呼叫regisgterAll()這個方法的時候,MEF會根據匯入匯出自動去尋找匹配,並且自動例項化。如果是沒有MEF,我們UI層就必須要新增ESTM.Client.BLL的引用了。當然有一點需要注意的地方,雖然UI層不用新增ESTM.Client.BLL的引用,但是由於在UI裡面使用了ManagerUser這個物件,所以UI層bin目錄下面必須要有ESTM.Client.BLL.dll這個檔案以及ESTM.Client.BLL專案所必須的dll,你可以手動拷貝這些dll到UI的bin目錄下面。甚至為了簡單,你也可以在UI層上面新增ESTM.Client.BLL這個的引用,但是博主覺得,這樣貌似違背了面向介面程式設計的原則,不爽,奈何沒想到更好的解決方案。
對於上面UI層必須要新增BLL實現層這一問題找到解決方案了,在此記錄下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public partial class Form1 : Form { [Import("Users")] public IManagerUser Manager { set; get; } public Form1() { InitializeComponent(); regisgterAll().ComposeParts(this); this.dataGridView1.DataSource = Manager.GetAllUser(); } public CompositionContainer regisgterAll() { AggregateCatalog aggregateCatalog = new AggregateCatalog(); var thisAssembly = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory, "*.dll"); aggregateCatalog.Catalogs.Add(thisAssembly); var _container = new CompositionContainer(aggregateCatalog); return _container; } } |
ESTM.Client.BLL專案右鍵→屬性
輸出路徑改成UI層的bin目錄下面即可。2015年9月16日加。
在搭建這個小框架過程中,博主遇到幾個問題在此和博友分享下:
1.新增服務引用在Client.Bll裡面,由於Client.BLL是一個內庫,最終它會生成一個dll,所以,WCF連線的配置要拷貝到Winform專案下面的App.Config裡面。
2.DAL裡面的連線字串也要拷貝到WCF的App.Config裡面,原因同上。
3.註冊MEF的方法
1 2 3 4 5 6 7 8 9 |
public CompositionContainer regisgterAll() { AggregateCatalog aggregateCatalog = new AggregateCatalog(); var thisAssembly = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory, "*.dll"); aggregateCatalog.Catalogs.Add(thisAssembly); var _container = new CompositionContainer(aggregateCatalog); return _container; } |
可以抽到一個公共的地方,不用每個地方都寫。注意由於MEF的匯入匯出涉及到多個內庫,所以這裡要遍歷bin目錄下面所有的dll去尋找匹配。
4.DAL層可以還做一下封裝,博主的專案是用的倉儲模式封裝EF,然後在Service.BLL裡面呼叫倉儲的服務去訪問資料庫。