本文也并非什麼了不起的技術創新,隻是分享一下我對.net子產品依賴關系及程式結構方面的一些看法。重點講述了子產品互相依賴的問題,以及如何用StructureMap來解決這個問題。
技術為解決問題而生。
上面這個命題并非本文重點,我将來有空再談這個。本文也并非什麼了不起的技術創新,隻是分享一下我對.net子產品依賴關系及程式結構方面的一些看法。先看一個最最簡單的hello world網站的子產品結構如何:

- 将Office的一些“公共功能”抽出成為新的子產品,供HR使用,且抽出的新子產品不依賴于HR
- 直接在HR中實作一部分Office的功能,使之不依賴Office
- 對各個業務邏輯子產品進行“接口抽象”,詳情後面會說
很明顯,方案2會導緻重複代碼,肯定不可取;而方案1也存在很大的問題,因為接下來老闆要求新增計算和檢視統計資料的Report子產品和處理公司審批流程的Flow子產品,它們與HR存在互相依賴的關系,這次看看你如何招架:
然後,再要加呢?“Oh my god!”你喊道:“快給我講方案3吧。”OK,現在我來擺出方案3,這不一定是最好的辦法,但卻是我所要使用的辦法:
圖看起來有些眼花缭亂,但再仔細一看便發覺其實隻是給原先的每個子產品增加了一個“接口層”而已,比如Office子產品,增加了IOffice,現在就變成Office是對IOffice的實作,而原先需要依賴于Office的子產品,現在轉去依賴IOffice,而“接口層”僅僅變成了一個描述,它隻包括資料及方法的聲明,不包括任何實作,是以它可以不依賴于任何其它子產品,它隻被别的子產品所依賴,包括實作它的子產品。相信到此大家也都了解了。那麼上圖那個虛框和“Config”子產品是什麼意思呢?
試着這樣想:假如現在Website要調用Office子產品,通過IOffice,但是實作IOffice的卻是Office子產品,那是不是得先執行個體化一個Office?誰來負責執行個體化?如果是Website負責這個執行個體化工作的話,那勢必Website會直接依賴與Office子產品,這樣就是走老路了,會出問題,而且Office本身又依賴于IHR、IPubMaterial和ICommon,那對應的HR、PubMaterial和Common又誰來執行個體化?看來如果照着老思路下去的話,還是會掉進“依賴關系地獄”中去的。不行,我們得借助一些工具,當然,重複造輪子也是不可取的,是以我們這次要使用一個叫StructureMap的工具。
使用過Microsoft Enterprise Library的朋友也許都不會對Unity陌生,這是一個IoC(Inversion of Control)工具,後來IoC又有了一個更貼切的名稱,叫DI(Dependency Injection),IoC和DI其實都是為了解決程式子產品之間的依賴關系而提出來的設計模式,它們本身并不是一種具體的技術,隻是一些設計套路,旨在給程式子產品松耦(Loose Coupling)。StructureMap也是一個和Unity類似的DI工具,個人感覺很不錯。
我目前從NuGet上擷取到的StructureMap的版本是2.6.4.1,需要說明的是StructureMap官方網站上提供的文檔有些落後,跟不上形勢了。所幸的是我們并不需要用它的所有功能,大多數軟體不都這樣麼?80%的功能是留給20%的人用的。
有了這個StructureMap,我們要使用ICommon,直接告訴StructureMap就是,具體用什麼去執行個體化,怎麼執行個體化,不用我們操心,StructureMap自動幫我們做,這就是解決的思路。現在剩下的問題就隻是告訴StrutureMap “ICommon”和“Common”的關系即可,可以直接用代碼來指明關系,也可以用配置檔案,配置檔案看起來更具“擴充性”,但這樣也就沒有了編譯期的出錯檢查,反正都是得寫,我這次就用代碼直接指明它們之間的對應關系吧。上圖中的Config,就是這麼一個用來指明接口與實作之間的對應關系的配置。(除了寫死和寫配置檔案之外,StructureMap還可以使用反射來自動建立接口和實作體之間的對應關系,具體可以參考它的文檔,這裡略過不表)
在開始一個真正的例子之前,我還是得說明一些東西,那就是前面所提到的“子產品”其實在不同的場合就有着不同的意義,這是一個抽象的概念,有時候是一個代碼檔案,有時候是一個類,有時候後是一個類庫,有時候是一個程式集,大家不要糾結于它究竟是什麼,而是要重點看看在自己的使用當中如何給它們松耦。
OK,都了解完之後,我們就開始一個小小的例子。簡單起見,我删掉了許多子產品,程式結構圖變成了:
IHR的内容有:
public class Employee
{
public string EmpNo { get; set; }
public string ChineseName { get; set; }
}
public class EmployeeDetail:Employee
{
public string Descriptions { get; set; }
public int TaskAmount { get; set; }
}
public interface IHr
{
IEnumerable<Employee> GetAllEmployees();
EmployeeDetail GetEmployeeByNo(string strEmpNo);
int GetEmployeeAmount();
void AddEmployee(Employee emp);
string GetEmpNameByNo(string strEmpNo);
}
IOffice的功能有:
public class Task
{
public int Id { get; set; }
public string EmpNo { get; set; }
public string Descriptions { get; set; }
}
public class TaskDetail : Task
{
public string EmpName { get; set; }
}
public interface IOffice
{
IEnumerable<Task> GetAllTasks();
TaskDetail GetTask(int id);
void AddTask(string strEmpNo, string strDesc);
void DeleteTask(int id);
int GetTaskAmountOfEmployee(string strEmpNo);
}
ICommon的功能有:
public enum LogType
{
Information,
Warning,
Error
}
public interface ICommon
{
void Log(LogType logType, string moduleName, string content, params object[] values);
}
很明顯,這些都是“接口”,不包含任何的實作,它們隻會被别的子產品依賴,而不會依賴别的子產品。
功能相信一看名字就知道怎麼回事,不需要太多解釋。值得說一下的是HR子產品的“GetEmployeeByNo”方法傳回的“EmployeeDetail”中包括了一個“TaskAmount”,也就是這個員工的“任務數”,這是Office子產品的功能,這意味着HR子產品需要調用Office子產品的方法;接着看Office子產品中的“GetTask”方法,會傳回一個“TaskDetail”,“TaskDetail”中含有“EmpName”,即員工姓名,這個需要從HR子產品中擷取。這是一個“互相依賴”!但這次我向你保證沒有問題,因為我們采用了新的設計模式工具——StructureMap,現在,我們來告訴StructureMap如何來幫助我們生成這些接口的執行個體,看代碼:
public static class ContainerBootstrapper
{
public static void BootstrapStructureMap()
{
ObjectFactory.Initialize(x =>
{
x.For<IHr>().Singleton().Use<HrManager>();
x.For<IOffice>().Singleton().Use<OfficeManager>();
x.For<ICommon>().Singleton().Use<CommonManager>().Ctor<string>("logPath").Is(AppDomain.CurrentDomain.BaseDirectory+"log");
});
}
}
一個靜态配置類,一個靜态方法,隻需要在程式入口函數處調用一下即可。上面的“Singleton”方法是告訴StructureMap我們的HR、Office和Common子產品都是“單執行個體”的,自始自終隻有一個執行個體,如果把“.Singleton()”去掉,那麼每次請求執行個體的時候都會建立一個新的執行個體出來,另外還有“HttpContextScoped”和“HybridHttpOrThreadLocalScoped”模式,前者表示每個HttpContext使用一個執行個體,這得在Web程式中才有效,否則按預設處理,而後者則會判斷目前程式類型,如果是Web程式,那麼和前者一樣,如果不是Web程式,那麼就每個線程使用一個執行個體, 究竟怎麼選擇,這個要看你自身的需要,在我的這個小小的demo中,Singleton即可。另外,對ICommon的實作的CommonManager的構造函數是帶參數的,需要指定,否則就會出現運作時錯誤,上面的代碼的意思是logPath這個參數是個string,把“AppDomain.CurrentDomain.BaseDirectory+"log"”作為它的值初始化。
接下來看看Main函數代碼片段,簡單起見,我寫了個控制台程式來“冒充”Website:
static void Main(string[] args)
{
//初始化StructureMap
ContainerBootstrapper.BootstrapStructureMap();
//擷取HR執行個體
IHr hr = ObjectFactory.GetInstance<IHr>();
//用之
//...
//擷取Office執行個體
IOffice office = ObjectFactory.GetInstance<IOffice>();
//用之
//...
}
非常好!這樣一來,子產品項目依賴的關系就解決了。大家還可以看看“log”目錄,你會發現一次運作中,盡管多次請求擷取HR和Office的執行個體(HR中會請求Office,而Office也會請求HR),但它們都隻會被初始化了一次,這是由于配置中指明了它們是Singleton。現在,我們來看看這個Solution的結構:
在這個Solution中,我是把IHr、IOffice和ICommon合并到了Interface這個Project中去,這樣的話項目引用會少一點,但如果你要把它們分開的話也是完全沒有問題的。
花了這麼大的力氣來處理子產品互相依賴的問題其實還有一個目的,那就是可以做單元測試,如果時間允許,我将會另寫一篇文章來講述如何測試。
At Last,當然少不了完整代碼(Visual Studio 2010):structuremap_demo.7z