.NET框架設計(常被忽視的框架設計技巧)

王清培發表於2013-08-04

閱讀目錄:

  • 1.開篇介紹
  • 2.後設資料快取池模式(在執行時構造後設資料快取池)
    • 2.1.後設資料設計模式(抽象出對資料的描述資料)
    • 2.2.藉助Dynamic來改變IOC、AOP動態繫結的問題
    • 2.3.後設資料和模型繫結、後設資料應該隱藏在Model背後、後設資料與DSL的關係
  • 3.鏈式配置Dynamic模式(愛不釋手的思維習慣程式設計)
  • 4.委託工廠模式(要優於常見的 工廠,概念更加準確,減少汙染)
  • 5.規則外掛(視委託為特殊的規則物件原型)

1】開篇介紹

通過上一篇的“.NET框架設計—常被忽視的C#設計技巧”一文來看,對於框架設計的技巧還是有很多人比較有興趣的,那麼框架設計思想對於我們日常開發來說其實並不是很重要,但是對於我們理解框架背後的執行原理至關重要;當我們使用著LINQ靈活的語法的同時我們是否能理解它的背後執行原理、設計原理更深一點就是它的設計模式及複雜的物件模型;

從一開始學習.NET我就比較喜歡框架背後的設計模型,框架提供給我們的使用介面是及其簡單的,單純從使用上來看我們不會隨著對框架的使用時間而增加我們對框架內部設計的理解,反而會養成一樣拿來即用的習慣,我們只有去了解、深挖它的內部設計原理才是我們長久學習的目標;因為框架的內部設計模式是可以提煉出來並被總結的;

 

這篇文章總結了幾個我最近接觸的框架設計思想,可以稱他們為模式;由於時間關係,這裡只是介紹加一個簡單的介紹和示例讓我們能基本的瞭解它並且能在日後設計框架的時候想起來有這麼一個模式、設計方式可以借鑑;當然,這每一節都是一個很大主題,用的時候在去細心的分析學習吧;

2】後設資料快取池模式(在執行時構造後設資料快取池)

很多框架都有將特性放在屬性上面用來標識某種東西,但是這種方式使用不當的話會對效能造成影響;再從框架設計原則來講也是對DomainModel極大的汙染,從EntityFramework5.0之前的版本我們就能體會到了,它原本是將部分Attribute加在了Entity上的,但是這畢竟是業務程式碼的核心,原則上來說這不能有任何的汙染,要絕對的POJO;後來5.0之後就完全獨立了DomainModel.Entity,所有的管理都在執行時構造繫結關係,因為它有EDMX後設資料描述檔案;

那麼這些Attribute其實本質是.NET在執行時的一種後設資料,主要的目的是我們想在執行時將它讀取出來,用來對某些方面的判斷;那麼現在的問題是如果我們每次都去讀取這個Attribute是必須要走反射機制,當然你可以找一些框架來解決這個問題;(我們這裡討論的是你作為開發框架的設計者!)

反射影響效能這不用多講了,那麼常規的做法是會在第一次反射之後將這些物件快取起來,下次再用的時候直接在快取中讀取;這沒有問題,這是解決了反射的效能問題,那麼你的Attribute是否還要加在DomainModel中呢,如果加的話隨著程式碼量的增加,這些都會成為後面維護的成本開銷;那麼我們如何將乾淨的POJO物件提供給程式設計師用,但是在後臺我們也能對POJO進行強大的控制?這是否是一種設計問題?

2.1】後設資料設計模式(抽象出對資料的描述資料)

我一直比較關注物件與資料之間的關係,物件導向的這種縱橫向關係如何平滑的與E-R實體關係模型對接,這一直是複雜軟體開發的核心問題;這裡就用它來作為本章的示例的基本概要;

我們有一個基本的DomainModel聚合,如何在不影響本身簡潔性的情況下與E-R關係對接,比如我們在對聚合進行一個Add操作如何被對映成對資料庫的Insert操作;我們來看一下後設資料設計模式思想;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.DomainModel
 9 {
10     /// <summary>
11     /// Employee.<see cref="DomainModel.Employee"/>
12     /// </summary>
13     public class Employee
14     {
15         /// <summary>
16         /// Primary id.
17         /// </summary>
18         public string EId { get; set; }
19 
20         /// <summary>
21         /// Name.
22         /// </summary>
23         public string Name { get; set; }
24 
25         /// <summary>
26         /// Sex.<see cref="DomainModel.SexType"/>
27         /// </summary>
28         public SexType Sex { get; set; }
29 
30         /// <summary>
31         /// Address.
32         /// </summary>
33         public Address Address { get; set; }
34     }
35 }
View Code

這裡有一個以Employee實體為聚合根的聚合,裡面包含一些基本的屬性,特別需要強調的是Sex屬性和Address,這兩個屬性分別是Complex型別的屬性;
Complex型別的屬性是符合物件導向的需要的,但是在關係型資料庫中是很難實現的,這裡就需要我們用後設資料將它描述出來並能在一些行為上進行控制;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.DomainModel
 9 {
10     /// <summary>
11     /// Address .
12     /// </summary>
13     public struct Address
14     {
15         /// <summary>
16         /// Address name.
17         /// </summary>
18         public string AddressName { get; set; }
19     }
20 }
View Code

這是Address型別的定義;

 1 namespace ConsoleApplication1.DomainModel
 2 {
 3     /// <summary>
 4     /// Sex type.
 5     /// </summary>
 6     public enum SexType
 7     {
 8         Male,
 9         Female
10     }
11 }
View Code

這是SexType型別的定義;都比較簡單;  

只有這樣我們才能對DomainModel進行大面積的複雜設計,如果我們不能將資料物件化我們無法使用設計模式,也就談不上擴充套件性;

圖1:

這是我們的物件模型,那麼我們如何將它與資料庫相關的資訊提取出來形成獨立的後設資料資訊,對後設資料的抽取需要動、靜結合才行;

什麼動、靜結合,我們是否都會碰見過這樣的問題,很多時候我們的程式碼在編譯時是確定的,但是有部分的程式碼需要在執行時動態的構造,甚至有些時候程式碼需要根據當前的IDE來生成才行,但是最終在使用的時候這些在不同階段生成的程式碼都需要結合起來變成一個完整的後設資料物件;

框架在很多時候需要跟IDE結合才能使使用變的順手,比如我們在開發自己的ORM框架如果不能直接嵌入到VisualStudio中的話,用起來會很不爽;當我們用自己的外掛去連線資料庫並且生成程式碼的時候,有部分的後設資料模型已經在程式碼中實現,但是有部分需要我們動態的去設定才行;

我們來看一下關於後設資料的基礎程式碼;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ORM.Meta
 9 {
10     using System;
11     using System.Collections.Generic;
12     using System.Linq;
13     using System.Text;
14     using System.Threading.Tasks;
15 
16     /// <summary>
17     /// Data source context.
18     /// </summary>
19     public abstract class DataBaseContext : List<MetaTable>, IDisposable
20     {
21         /// <summary>
22         /// Data base name.
23         /// </summary>
24         protected string DataBaseName { get; set; }
25 
26         /// <summary>
27         /// Connection string.
28         /// </summary>
29         protected string ConnectionString { get; set; }
30 
31         /// <summary>
32         /// Provider child class add table.
33         /// </summary>
34         /// <param name="table"></param>
35         protected virtual void AddTable(MetaTable table)
36         {
37             this.Add(table);
38         }
39 
40         /// <summary>
41         /// Init context.
42         /// </summary>
43         protected virtual void InitContext() { }
44         public void Dispose() { }
45     }
46 }
View Code

這表示資料來源上下文,屬於執行時後設資料的基礎設施;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ORM.Meta
 9 {
10     using System.Collections.Generic;
11     using System.Linq;
12 
13     /// <summary>
14     /// Database Table meta.
15     /// </summary>
16     public class MetaTable : List<MetaColumn>
17     {
18         /// <summary>
19         /// Table name.
20         /// </summary>
21         public string Name { get; set; }
22 
23         /// <summary>
24         /// Entity name.
25         /// </summary>
26         public string EntityName { get; set; }
27 
28         /// <summary>
29         /// Get column by column name.
30         /// </summary>
31         /// <param name="name">Column name.</param>
32         /// <returns><see cref="ORM.MetaColumn"/></returns>
33         public MetaColumn GetColumnByName(string name)
34         {
35             var column = from item in this.ToList() where item.CoumnName == name select item;
36             return column.FirstOrDefault();
37         }
38     }
39 }
View Code

簡單的表示一個Table,裡面包含一系列的Columns;要記住在設計後設資料基礎程式碼的時候將介面留出來,方便在IDE中植入初始化後設資料程式碼;

圖2:

到目前為止我們都是在為後設資料做基礎工作,我們看一下有系統生成的宣告的後設資料程式碼;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.Repository
 9 {
10     using System;
11     using System.Collections.Generic;
12     using System.Linq;
13     using System.Text;
14     using System.Threading.Tasks;
15 
16     /// <summary>
17     /// IDE Builder.
18     /// </summary>
19     public class DesignBuilder_DataBaseContext : ORM.Meta.DataBaseContext
20     {
21         //this begin IDE builder.
22         protected override void InitContext()
23         {
24             ORM.Meta.MetaTable metaTable = new ORM.Meta.MetaTable() { Name = "TB_Employee", EntityName = "Employee" };
25             metaTable.Add(new ORM.Meta.MetaColumn()
26             {
27                 CoumnName = "EId",
28                 DataType = ORM.Meta.DataType.NVarchar
29             });
30             metaTable.Add(new ORM.Meta.MetaColumn()
31             {
32                 CoumnName = "Name",
33                 DataType = ORM.Meta.DataType.NVarchar
34             });
35             metaTable.Add(new ORM.Meta.MetaColumn()
36             {
37                 CoumnName = "Sex",
38                 DataType = ORM.Meta.DataType.Int
39             });
40             metaTable.Add(new ORM.Meta.MetaColumn()
41             {
42                 CoumnName = "Address",
43                 DataType = ORM.Meta.DataType.NVarchar
44             });
45             this.AddTable(metaTable);
46         }
47         //end
48     }
49 }
View Code

我假設這是我們框架在IDE中生成的部分後設資料程式碼,當然你可以用任何方式來存放這些後設資料,但是最後還是要去物件化;

圖3:

這個目錄你可以直接隱藏,在後臺屬於你的框架需要的一部分,沒有必要讓它汙染專案結構,當然放出來也有理由;如果想讓你的LINQ或者表示式能直接穿過你的後設資料上下文你需要直接擴充套件;

1 static void Main(string[] args)
2         {
3             using (Repository.DesignBuilder_DataBaseContext context = new Repository.DesignBuilder_DataBaseContext())
4             {
5                 var employee = from emp in context.Employee where emp.EId == "Wqp123" select emp;
6             }
7         }
View Code

 

這裡所有的程式碼看上去很簡單,沒有多高深的技術,這也不是本篇文章的目的,任何程式碼都需要設計的驅動才能產生價值,我們構建的基礎程式碼都是後設資料驅動;當你在執行時把這些後設資料放入Cache,既不需要加Attribute也不需要反射反而活動了更大程度上的控制,但是要想構建一個能用的後設資料結構需要結合具體的需求才行;

2.2】藉助Dynamic來改變IOC、AOP動態繫結的問題

要想在執行時完全動態的繫結在編譯時定義的物件行為是需要強大的IOC框架支撐的,這樣的框架我們是做不來的或者需要很多精力,得不償失;對於後設資料設計需要將AOP通過IOC的方式注入,在使用的時候需要改變一下思路,AOP的所有的切面在編譯時無法確定,後期通過IOC的方式將所有的行為注入;這裡我們需要使用動態型別特性;

使用Dynamic之後我們很多以往不能解決問題都可以解決,更向超程式設計跨進了一步;對於IOC、AOP的使用也將變的很簡單,也有可能顛覆以往IOC、AOP的使用方式;而且動態程式設計將在很大程度上越過設計模式了,也就是設計模式的使用方式在動態程式設計中將不復存在了;

 1 using (Repository.DesignBuilder_DataBaseContext context = new Repository.DesignBuilder_DataBaseContext())
 2             {
 3                 var employees = from emp in context.Employee where emp.EId == "Wqp123" select emp;
 4 
 5                 Employee employee = new Employee() { EId = "Wqp123" };
 6 
 7                 var entityOpeartion = DynamicBehavior.EntityDymanicBehavior.GetEntityBehavior<Employee>(employee);
 8 
 9                 entityOpeartion.Add();
10             }
View Code

 

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.DynamicBehavior
 9 {
10     using System;
11     using System.Dynamic;
12 
13     public class EntityDymanicBehavior
14     {
15         public static dynamic GetEntityBehavior<TEntity>(TEntity entity)
16         {
17             //auto mark entity behavior
18             dynamic dy = new ExpandoObject();
19             //load meta data mark dynamic behavior
20             dy.Entity = entity;
21             dy.Add = new Action(() =>
22             {
23                 Console.WriteLine("Action Add " + entity.GetType());
24             });
25             return dy;
26         }
27     }
28 }
View Code

圖4:

畫紅線的部分是可以抽取來放入擴充套件方法Add中的,在構造的內部是完全可以進入到後設資料快取池中拿到這些資料然後直接動態生成擴充套件方法背後的真實方法;

2.3】後設資料和模型繫結、後設資料應該隱藏在Model背後、後設資料與DSL的關係

後設資料的繫結應該在執行時動態去完成,這點在以往我們需要大費力氣,通過CodeDom、Emit才能完成,但是現在可以通過Dynamic、DLR來完成;思維需要轉變一下,動態程式設計我們以往用的最多的地方在JS上,現在可以在C#中使用,當然你也可以使用專門的動態語言來寫更強大的後設資料框架,IronRuby、IronPython都是很不錯的,簡單的瞭解過Ruby的後設資料程式設計,很強大,如果我們.NET程式設計師眼饞就用Iron…系列;

在開發複雜的動態行為時儘量使用後設資料設計思想,不要把資料和表示資料的資料揉在一起,要把他們分開,在執行時Dynamic繫結;後設資料應該在Model的背後應該在DomainModel的背後;

後設資料和DSL有著天然的淵源,如果我們能把所有的語句元件化就可以將其封入.NET元件中,在IDE中進行所見即所得的DSL設計,然後生成可以直接執行的Dynamic程式碼,這可能也是超程式設計的思想之一吧;

圖5:

這可能是未來10年要改變的程式設計路線吧,我只是猜測;最後軟體將進一步被自定義;

3】鏈式配置Dynamic模式(愛不釋手的思維習慣程式設計)

再一次提起鏈式程式設計是覺得它的靈活性無話可說,語言特性本身用在哪裡完全需求驅動;把鏈式用來做配置相關的工作非常的合適;我們上面做了後設資料配置相關的工作,這裡我們試著用鏈式的方法來改善它;

Dynamic型別本身的所有行為屬性都是可以動態構建的,那麼我們把它放入鏈式的方法中去,根據不同的引數來實現動態的新增行為;

擴充套件Dynamic型別需要使用ExpandoObject開始;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.DynamicBehavior
 9 {
10     using System;
11     using System.Dynamic;
12 
13     public static class EntityDynamicBehaviorExtent
14     {
15         /// <summary>
16         /// Add dynamic method.
17         /// </summary>
18         /// <param name="entity"></param>
19         /// <returns></returns>
20         public static ExpandoObject AddExten(this ExpandoObject entity)
21         {
22             dynamic dy = entity as dynamic;
23             dy.Add = new Func<ExpandoObject>(() => { Console.WriteLine("add " + entity); return entity; });
24             return entity;
25         }
26         /// <summary>
27         /// where  dynamic method. 
28         /// </summary>
29         /// <typeparam name="T"></typeparam>
30         /// <param name="entity"></param>
31         /// <param name="where"></param>
32         /// <returns></returns>
33         public static ExpandoObject WhereExten<T>(this ExpandoObject entity, Func<T, bool> where)
34         {
35             dynamic dy = entity as dynamic;
36             dy.Where = where;
37             return entity;
38         }
39     }
40 }
View Code

擴充套件方法需要擴充套件 ExpandoObject物件,DLR在執行時使用的是ExpandoObject物件例項,所以我們不能夠直接擴充套件Dynamic關鍵字;

1 Employee employee1 = new Employee() { EId = "Wqp123" };
2                 var dynamicEntity = DynamicBehavior.EntityDymanicBehavior.GetEntityBehavior<Employee>(employee1);
3                 (dynamicEntity as System.Dynamic.ExpandoObject).AddExten().WhereExten<Employee>(emp =>
4                 {
5                     Console.WriteLine("Where Method.");
6                     return emp.EId == "Wqp123";
7                 });
8                 dynamicEntity.Add().Where(employee1);
View Code

 圖6:

紅線部分必須要轉換才能順利新增行為;

4】委託工廠模式(要優於常見的 工廠,概念更加準確,減少汙染)

對於工廠模式我們都會熟悉的一塌糊塗,各種各樣的工廠模式我們見的多了,但是這種型別的工廠使用方式你還真的沒見過;其實這種委託是想部分的邏輯交給外部來處理;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.DomainModel
 9 {
10     /// <summary>
11     /// Address factory.
12     /// </summary>
13     /// <returns></returns>
14     public delegate Address Factory();
15 
16     /// <summary>
17     /// Employee.<see cref="DomainModel.Employee"/>
18     /// </summary>
19     public class Employee
20     {
21         public Employee() { }
22         /// <summary>
23         /// Mark employee instance.
24         /// </summary>
25         /// <param name="eID"></param>
26         /// <param name="name"></param>
27         /// <param name="sex"></param>
28         /// <param name="addressFactory">address factory.</param>
29         public Employee(string eID, string name, SexType sex, Factory addressFactory)
30         {
31             this.EId = eID;
32             this.Name = name;
33             this.Sex = sex;
34             this.Address = addressFactory();
35         }
36         /// <summary>
37         /// Primary id.
38         /// </summary>
39         public string EId { get; set; }
40 
41         /// <summary>
42         /// Name.
43         /// </summary>
44         public string Name { get; set; }
45 
46         /// <summary>
47         /// Sex.<see cref="DomainModel.SexType"/>
48         /// </summary>
49         public SexType Sex { get; set; }
50 
51         /// <summary>
52         /// Address.
53         /// </summary>
54         public Address Address { get; set; }
55     }
56 }
View Code

我們定義了一個用來建立Employee.Address物件的Factory,然後通過建構函式傳入;

1 Employee employee2 = new Employee("Wqp123", "Wqp", SexType.Male, new Factory(() =>
2                 {
3                     return new Address() { AddressName = "Shanghai" };
4                 }));
View Code

這裡純粹為了演示方便,這種功能是不應該在DommianModel中使用的,都是在一些框架、工具中用來做靈活介面用的;

5】規則外掛(視委託為特殊的規則物件原型)

規則外掛其實跟上面的委託工廠有點像,但是絕對不一樣的設計思想;如何將規則外掛出去,放入Cache中讓執行時可以配置這個規則引數;委託是規則的天然宿主,我們只要將委託序列化進Cache就可以對它進行引數的配置;

 1 /*==============================================================================
 2  * Author:深度訓練
 3  * Create time: 2013-08-04
 4  * Blog Address:http://www.cnblogs.com/wangiqngpei557/
 5  * Author Description:特定領域軟體工程實踐;
 6  *==============================================================================*/
 7 
 8 namespace ConsoleApplication1.DomainModel.Specification
 9 {
10     using System;
11     using System.Linq.Expressions;
12 
13     /// <summary>
14     /// Employee add specification.
15     /// </summary>
16     [Serializable]
17     public class EmployeeSpecificationAdd : System.Runtime.Serialization.IDeserializationCallback
18     {
19         /// <summary>
20         /// specification.
21         /// </summary>
22         [NonSerialized]
23         private Func<Employee, bool> _specification;
24         /// <summary>
25         /// Gets specification.
26         /// </summary>
27         public Func<Employee, bool> Specificaion { get { return _specification; } }
28 
29         /// <summary>
30         /// employee.
31         /// </summary>
32         private Employee Employee { get; set; }
33 
34         /// <summary>
35         /// Mark employee specificatoin.
36         /// </summary>
37         /// <param name="employee"></param>
38         public EmployeeSpecificationAdd(Employee employee)
39         {
40             this.Employee = employee;

41             InitSpecification();
42         }
43         /// <summary>
44         /// Is Check.
45         /// </summary>
46         /// <returns></returns>
47         public bool IsCheck()
48         {
49             return _specification(Employee);
50         }
51 
52         public void OnDeserialization(object sender)
53         {
54             InitSpecification();
55         }
56         private void InitSpecification()
57         {
58             this._specification = (emp) =>
59             {
60                 return !string.IsNullOrWhiteSpace(emp.EId) && !string.IsNullOrWhiteSpace(emp.Name);
61             };
62         }
63     }
64 }
View Code

圖7:

注意這裡的反序列化介面實現,因為Lambda無法進行序列化,也沒有必要進行序列化;

 1 EmployeeSpecificationAdd specification = new EmployeeSpecificationAdd(employee2);
 2 
 3                 Stream stream = File.Open("specification.xml", FileMode.Create);
 4                 BinaryFormatter formattter = new BinaryFormatter();
 5                 formattter.Serialize(stream, specification);
 6 
 7                 stream.Seek(0, SeekOrigin.Begin);
 8                 specification = formattter.Deserialize(stream) as EmployeeSpecificationAdd;
 9 
10                 stream.Close();
11                 stream.Dispose();
12                 if (specification.IsCheck())
13                 {
14                     Console.WriteLine("Ok...");
15                 }
View Code

既然能將規則序列化了,就可以把它放在任何可以使用的地方了,配置化已經沒有問題了;

示例Demo地址:http://files.cnblogs.com/wangiqngpei557/ConsoleApplication2.zip

 

相關文章