《OEA – 實體擴充屬性系統 – 設計方案說明書》

來源:互聯網
上載者:User

    這篇設計文檔是 12 月份寫來參加公司的研發峰會的,自己倒是信心滿滿,不過最後還是沒有入圍。現在想想也沒啥大用,所以貼出來,期待與園友交流。

    文檔有點長,沒全部貼在部落格中,有興趣的可以下載附件中的 PDF。

 附件:《實體擴充屬性系統-系統設計說明書.pdf》

================= 分隔線 ======================

 

 

目錄

前言... 4

1 背景與需求... 5

1.1 產品 721 客戶化開發的需要... 5

1.2 實體動態列... 6

1.3 分離唯讀/視圖屬性... 6

1.4 提升架構效能... 6

1.5 支援 WPF 綁定... 6

1.6 其它需求... 7

2 分析... 8

2.1 主要功能需求... 8

2.2 非功能需求分析... 8

2.3 約束... 9

2.4 風險... 9

3 設計方案... 10

3.1 一些決策... 10

3.2 風險點驗證... 10

3.2.1 支援 WPF 綁定... 10

3.2.2 效能關鍵點... 12

3.3 方案描述... 12

3.3.1 結構說明... 13

3.3.2 相關UML圖... 14

3.3.3 如何支撐需求... 18

3.4 重點實現細節... 18

4 設計驗證... 24

4.1 功能需求驗證... 24

4.2 WPF綁定驗證... 24

4.3 效能驗證... 25

5 使用手冊... 25

5.1 使用情境介紹(單元測試)... 25

5.1.1 屬性預設值... 26

5.1.2 強制替換屬性值... 27

5.1.3 屬性值設定時的取消與強制替換... 27

5.1.4 引用實體屬性的設定取消... 28

5.1.5 屬性變更事件... 29

5.1.6 產品721擴充屬性... 30

5.1.7 所有擴充屬性的介面產生... 31

5.1.8 唯讀屬性的使用方法... 31

5.1.9 唯讀擴充屬性的使用方法... 32

5.1.10 運行時動態屬性(動態列)... 33

5.1.11 序列化... 34

5.1.12 WPF綁定驗證... 35

5.1.13 擴充屬性ORM驗證... 35

5.1.14 實體狀態的相關屬性... 35

5.2 代碼產生 – CodeSnippets. 36

5.3 其它問題... 37

5.3.1 擴充屬性的CLR屬性編寫注意點... 37

5.3.2 何時使用屬性擴充,何時使用繼承擴充?... 38

前言

在產品線開發中,支援產品的客戶化在產品規模化開發中是非常重要的一部分。而客戶化中的非常重要的一部分則是屬性值的客戶化,包括屬性值的添加、刪除、修改及屬性對應的介面的客戶化。由於產品對屬性值的擴充方案一直是使用類繼承的方案來完成,導致了產品出現了許多的問題:

l 其中最重要一個問題是有時候無法給一個客戶兩個可選的的功能包,而為瞭解決這個問題,開發人員又不得不做大量的代碼移植,把可選包的代碼都移植到主幹版本中,導致了臨時代碼過多,維護成本太大。

l 另外,我們的產品基於實體開發,為實現動態列的需求繞了許多路,最終決定使用資料表的模式來編寫,同樣造成大量重複代碼,開發人員開發效率低下。

基於曆史遺留的這些問題,我們設計了全新的屬性系統。本系統設計完成之後,解決了許多曆史遺留問題,也帶來了許多意想不到的價值。例如:

l 支援簡單地完成客戶化開發中屬性的擴充。

l 支援更簡單地實現領域實體的動態屬性(介面中的動態列,原來要100行代碼,現在只要20行。)

l 視圖屬性分離(更好的可維護性)

l 屬性效能提升(效能提升)

l 減少了序列化資料(傳輸效率提升,效能提升)

l 統一的屬性介面(平台可提供更加強大的功能支援)

本次設計是在曆史代碼上進行重構,但是本質上是設計一個完全獨立功能的子系統。本文從需求、分析、方案、實現、驗證等角度說明了整個設計是如何完成的。並在最後,給出了系統的使用手冊以協助開發人員日常應用。

備忘:

本文檔中,為了方便起見,將會把“實體擴充屬性系統”簡稱為 EMPS。(Entity Managed Property System,意為實體Managed 屬性系統)

另外,文中說到的版本號碼:曆史的OEA版本是2.5,升級到EMPS之後,OEA版本是2.6。

1 背景與需求

本節主要說明整個系統設計之初,設計的背景及最終整理出來的需求列表。這些需求是前期不斷收集、累積的結果。接下來,將會詳細說明一些主要的需求:

1.1 產品 721 客戶化開發的需要

部門的幾個產品都是基於 OEA 平台開發的。OEA 平台主要解決產品開發模式下客戶化開發、以及在產品開發過程中如何提高開發效率兩大問題。

(關於產品開發中的721概念及OEA中的客戶化設計,參見:《基於OEA架構的客戶化設計(三) “外掛程式式”DLL》。關於 OEA 的瞭解,參見:《OEA 架構示範 - 快過原型的產品開發》。)

客戶化開發中,主要解決的問題是如何在客戶化版本中對主幹版本中的產品進行擴充。各種擴充一般都依託於底層的中繼資料,這些中繼資料描述整個系統。當我們對中繼資料進行修改時,整個應用程式也就發生了相應的變化。這些產品的擴充可以簡單分為:模組層級別的擴充、實體層級的擴充、屬性層級的擴充。模組的擴充在此不進行討論。

先說屬性擴充:我們一般會對產品中定義好的類的屬性進行以下擴充:添加一個屬性、刪除一個屬性、修改一個屬性。(所以,擴充並不只是意味著添加。)添加屬性意味著我們需要為已經定義完成的類添加一個額外的屬性,這個屬性可以映射到資料庫,可以在產品介面中顯示,行為和直接定義的屬性是一致的。刪除屬性則意味著,資料庫中不再有對應的欄位,介面不再顯示。修改屬性一般只會修改屬性的各種中繼資料,例如,修改它映射資料庫的欄位中繼資料,修改它在介面中顯示的列的中繼資料等;這些修改其實已經在中繼資料的設計方案中解決,相關內容可以查看:《基於OEA架構的客戶化設計(一) 總體設計》、《基於OEA架構的客戶化設計(二) 中繼資料設計》以及《基於OEA架構的客戶化設計(三) “外掛程式式”DLL》。

實體的擴充一般可以通過繼承的方法實現,當繼承出新的子類後,在中繼資料中用它將原來的父類進行覆蓋即可。有些時候,我們還會為某個類擴充一些彙總父子關係,例如:我們可以為某一個建設項目擴充出其相關的合約列表,這樣,原來只顯示項目的介面中,就能緊接著顯示每一個項目相應的合約列表。而這種彙總父子關係的擴充,雖然是實體層級的添加,但是實質上是對實體添加新的一對多關聯性。也就是說,這種實體的擴充,可以轉換為屬性擴充,即在原有實體的基礎上擴充一個一對多關聯性的屬性。

基於以上分析,我們知道,一個可擴充的屬性系統,幾乎是客戶化軟體產品運行時的最基礎設施。

在 2.6 版本之前的 OEA,屬性擴充主要使用繼承的方式來實現。簡單地說,就是繼承需要擴充的實體,添加新的屬性,然後使用這個實體替換掉原來的類。該方案主要是為了實現屬性的添加,但是屬性的刪除以及修改都是通過修改屬性的元描述來實現的。這樣的方式導致了許多問題:屬性的刪除只是刪除了介面,而資料庫、運行時實體也都還存在該屬性;屬性的修改不能修改屬性中的行為代碼;重點說下屬性的添加造成的缺點:

經常需要對某個類擴充一兩個屬性,而現在只能繼承出子類,同時把父類隱藏起來,或者直接覆蓋父類,用進來比較複雜; 同時,類型變多,開發人員的學習成本,維護成本都隨之變大。

更重要的是,.NET 中 CLR 單繼承體系的限制,使得通過繼承無法實現這樣的擴充: 兩個獨立的擴充包“2”以可選的形式對主包“7”進行擴充,也就是說,產品 721 客戶化開發中,兩個“2”的擴充包是兩個單獨的程式集,但是單繼承的限制,我們不能同時使用它們。對於這種情況,我們目前的處理方式是把兩個“2”的包都放到了主包中,而使用中繼資料的方式對不需要的功能來進行隱藏,這種實現方式是臨時的、錯誤的。

1.2 實體動態列

軟體開發中常常遇到動態列的需求:表格中的資料的列是根據資料本身自動產生的,這對於基於領域實體類型、基於非動態類型的技術架構來開發的系統來說,要實現動態列基本上不可能。所以往往應用程式會另闢捷徑,使用 DataTable 來重新組裝資料後再顯示。這導致兩種模式同時存在於一個系統中,同樣的代碼會重複出現,增加維護成本。介面的代碼不一致,也加大了介面自動產生的困難。

如果有了擴充屬性,我們則可以在任意實體上擴充各種新的屬性,介面也就相應地成了“動態”列。

1.3 分離唯讀/視圖屬性

實體設計中常常會添加一些唯讀屬性,它的值是使用實體當前的值經過計算後得出。在 OEA 中,實體被設計為分布式對象(簡單地說,就是用戶端和服務端重用一套實體代碼。可以參見CSLA架構設計書籍《Expert C# 2008 Business Objects》。),這些分布式對象被直接綁定到介面上。為了介面顯示的需要,常常會為它們添加許多隻讀的視圖屬性,這樣就導致了視圖屬性過多,混雜在領域實體的代碼中,汙染了代碼,加大維護難度。

如果有了擴充屬性,我們則可以把這個唯讀屬性都放到一個單獨的類中去為這個實體做擴充,這樣,就可以得到更簡潔、結構更清晰的代碼。

1.4 提升架構效能

對於架構開發來說,常常需要在架構中對實體的屬性做統一的處理,來嚮應用層提供強大的功能支援。如果使用一般的實體設計,那麼屬性值的擷取、設定都不可避免地要使用到反射。而大量的屬性值操作將會意味著較差的效能。如果有了Managed 屬性,則在架構層面能夠使用和應用一致的屬性 API 來操作屬性,不再使用反射,速度可以有不少提升。

1.5 支援 WPF 綁定

一般情況下,我們使用 WPF 綁定時,都是直接綁定到 CLR Managed 屬性上。但是,如果使用擴充屬性的話,並不是所有屬性都會有一個 CLR 屬性封裝器。所以,這些擴充屬性必須支援 WPF 綁定也是我們的需求之一。

1.6 其它需求

l 支援屬性反擴充

在產品 721 開發中,常常在 “1” 的客戶化版本中需要刪除 “2”版本中為“7”擴充的屬性,這時,需要支援屬性的反擴充(或叫反註冊)。

l 擷取屬性值來源

由於目前 OEA 架構中的實體是分布式對象,我們常常需要在實體屬性改變時分辨屬性值的來源:是資料庫,還是UI介面,還是來自程式中的其它代碼。

l 定製序列化的資料

實體屬性被架構管理後,可以很輕易地實現各種資料格式的序列化。

l 需要支援屬性值的驗證、強制、更改通知等事件通知。

l 中繼資料重載

屬性的一切行為都將以回調的形式存放在中繼資料中。而中繼資料是可以被重載的。這樣,子類就才重寫這些行為。同時,我們就可以在進行產品客戶化的時候,為屬性重新定製這些行為。

最後,可以看一下在《實體擴充屬性方案分析腦圖》腦圖文檔中整理出來的需求概況圖,這些需求都是曆史版本中所不能支援的:

圖1. 實體擴充屬性需求列表

2 分析

由於前面已經把需求整理得比較明朗了。那麼這裡,我們首先要分析出主要需求、約束及相關的風險等。(關於架構設計的整個過程,可以參考這篇文章:《架構模組設計經驗總結》。)

2.1 主要功能需求

其實在圖一中已經把需求按照優先順序別進行了劃分,後面的整個設計將會圍繞這些需求進行。其中,最主要的功能性需求是以下三個。而設計目標則是至少實現以下三個需求,其它需求則按優先順序儘可能實現。

l 721客戶化開發中的屬性擴充

l 屬性託管(受架構管理)
意思是需要為上層架構提供統一維護屬性值的功能。

l 動態列

2.2 非功能需求分析

l 運行時效能

實體屬性可以說是實體設計中最重要的部分。而它的效能好壞則關係到系統中每一個實體的每一個屬性,這些屬性都直接關係到應用的效能。簡單地說,如果屬性系統慢,上層應用的效能必然會慢。換句話說,屬性系統的代碼開發是對效能十分敏感的,在核心代碼上需要十分謹慎。

2.5 版本的OEA架構使用的屬性主要還是 .NET 中的原生 CLR屬性系統 + CSLA 開源架構中的屬性系統。主要是為了支援屬性的統一管理。而本次設計,可以對系統帶來許多的新功能和支援,加之原有系統的屬性效能並沒有構成應用程式層開發的效能問題,所以,一定的效能消耗是可以接受的。

對這項的要求是:

使用同樣的代碼,和曆史屬性系統進行屬性測試對比,耗時不能超過原有的120%。

比較簡單,也比較嚴格。一旦不滿足此項,整個設計不可以被使用。

l 獨立性

雖然實體擴充屬性系統是作為 OEA 架構的一個重要組成部分,但是Managed 屬性、擴充屬性的需求在開發過程中常常會碰到。所以我們需要把實體擴充屬性系統設計為一個獨立的 DLL,這樣,它就可以在非 OEA平台的環境中使用。

l 可擴充性

EMPS的可擴充性並不是指該系統帶來的屬性的可擴充性(這其實是EMPS的功能需求),而是指屬性系統本身需要進行一些擴充。

當前,OEA架構中以產品中繼資料為整個架構的基礎設施。也就是說,OEA 架構中有管理應用中所有中繼資料的功能。而由圖1中的需求列表可以看到,EMPS也需要中繼資料的支援,例如屬性的預設值。但是,獨立性中已經要求EMPS被設計為一個完全獨立的模組,也就是說EMPS完全不依賴 OEA。那麼,這些屬性的中繼資料如何支援使用 OEA 來進行儲存呢?這,同樣是EMPS 設計過程中需要特殊考慮的一個擴充點。

l 易用性

此項為架構設計必須考慮的一個非功能需求。

2.3 約束

l ORM功能的修改

原來的OEA的ORM中支援使用OEAORM及EntityFramework4.1(CodeFirst)兩種模式,但是這兩種ORM當前無疑都只支援對CLR屬性的映射。而擴充屬性是沒有CLR屬性包裝器的,但是這些擴充屬性同樣需要映射資料庫。

也就是說:如果EMPS開發完成,要映射新的擴充屬性,必須要修改當前OEAORM模組。同時,無法再支援EntityFramework4.1了(EFCodeFirst基於CLR屬性來進行映射)。

l 原有屬性功能的相容

2.5 版本的OEA使用的屬性主要還是 .NET 中的原生 CLR屬性系統 + CSLA 開源架構中的屬性系統。這些屬性中已經寫了非常多的代碼。屬性的 Get 擷取器及 Set 設定器中的代碼,可謂五花八門。這些都必須在新的屬性系統中被完全相容,否則,必須導致業務功能出現問題。

l 大量曆史代碼的修改

由於本次設計本質上是一次在曆史版本上的重構,而產品開發截止到目前,已經產生了幾萬行的曆史代碼,其中的實體屬性也是幾千個。重構如此底層的設計,在盡量保證應用程式層 API 不變的前提下,也必然會造成較多的修改,同時,很可能會引起比較多的BUG。這是一個必須考慮的約束條件。

2.4 風險

l 屬性效能

由非功能需求的描述中知道,效能是至關重要的。關係到整個設計是否可用。但是,最終開發出來的模組效能,在設計時很難測量的。對於這個風險的規避使用以下方案:分析曆史屬性系統的關鍵效能影響點,在設計稿完成後,理論上檢查這些關鍵點是否能在新設計出來的屬性系統下運行良好。

l 支援WPF綁定

這是一個技術難關。

當前我們只是使用了 WPF 中直接綁定CLR屬性的方案。如何能讓我們在客戶化版本的程式集中擴充的擴充屬性也支援WPF綁定,成為了一個技術上的難題。

對這點的規避很簡單,在整個設計開始之前,先分析WPF綁定中的內部機制,解決這個問題後,才能開始其它的設計。

3 設計方案3.1 一些決策

由於本系統的設計比較複雜。所以先對相容性約束做了一個決策:

在設計過程中盡量考慮功能上與原屬性系統保持相容,介面上保持一致。但是當無法相容或者無法保持一致的介面時,可以不相容。但是這些不相容的設計點,都需要記錄下來,當設計完成後,逐個修改。如果改動較大,則使用組內的重構工具完成。

3.2 風險點驗證3.2.1 支援 WPF 綁定

經過查閱MSDN及搜尋出的網路資源,發現WPF中的綁定機制支援綁定DataTable資料表類型,而表中的欄位則是動態,根據結果資料的變化而變化。所以只要搞清楚DataTable是如何被WPF綁定支援的,那麼EMPS也可以使用同樣的機制進行綁定。

以下是WPF中DataTable的綁定機制分析:

圖2. WPF中DataTable支援綁定的核心類型分析

圖3. WPF中為DataTable產生視圖模型的流程圖

重點在於DataTable 實現 IListSource介面,並構造動態視圖動態類型 DataRowView並使其實現ICustomTypeDescriptor。(詳細過程參見這篇文章:《OEA 擴充屬性系統 - 任意適配 WPF Binding 的設計分析》,以及本系列中的文檔:《任意適配 WPF Binding 的設計分析》。)

搞清楚了整個設計及建立流程,那麼其實在設計EMPS時,支援這個機制就可以了。

3.2.2 效能關鍵點

需要分析,曆史架構中的屬性系統(CSLAManaged 屬性系統)在做到Managed 屬性的同時,是如何保證效能的呢?

其實,它其中屬性的核心重點在於使用強型別的FieldData<T>來儲存每一個屬性,並使用定長的屬性值的數組來存放:

private IFieldData[] _fieldData;

這樣的好處在於強型別保證了沒有裝箱拆箱操作,同時定長的數組支援了以O(1)的複雜度來尋找指定屬性:

但是,這樣搜尋屬性的前提是屬性值數組定長,而一個實體類型到底有多少個屬性,是在編譯期已經完全確定下來的。換句話說,在這個數組初始化時必須知道固定的屬性個數,這違背了屬性可擴充的需求,這也是為什麼使用這個屬性系統很難做到擴充的原因。

當然,在對其進行較大改動的前提下,也不是不可能。但是考慮到CSLA是個開源架構,其滿足需求與我們的需求有較大的區別,代碼比較臃腫,也無法實現我們所需要的一些功能,對它做大型的改動不如重新做一個完全符合需求的Managed 屬性架構。

經過之前的分析,可以想到,要得到較高效能的Managed 屬性系統,最好也是使用“強型別儲存屬性值”加“定長數組”的方案。但是如何支援屬性的擴充呢?“劃分屬性定義期”是個較好的解決方案。之後的主體設計中會對這個方案進行詳細的描述。

3.3 方案描述

整個設計中,借鑒了CSLAManaged 屬性以及WPF相依性屬性的設計,然後再構建出我們自己的屬性系統:

3.3.1 結構說明

圖4. EMPS結構說明

腦圖比較簡單,其中的具體內容可以參考腦圖《擴充屬性方案》。這裡只做簡要說明:

l 靜態結構

總體上,靜態結構比較簡單,主要分為兩個層次。底層是抽象的屬性中繼資料提供子系統,而另一層則是依賴於前者而構建的EMPS核心:運行時擴充屬性子系統。

提取抽象的屬性中繼資料提供系統是為了使中繼資料的儲存、提供都抽象化,後面可以和 OEA 中的中繼資料存放區模組進行適配。

而核心的EMPS則實現了整個的Managed 屬性。後面將會對其以類圖的形式重點說明。

l 動態結構

在這裡比較特殊地提出了屬性生命週期的概念。屬性的生命週期規定了屬性被定義(或者被反註冊)的時期(可能叫定義期會比較正確。),這裡主要有編譯期、啟動期、運行期。

l 編譯期

此階段中定義的屬性主要包括使用代碼編寫的一般屬性、擴充屬性。當然,也包括“2”和“1”的擴充包中編寫的一些對“7”的包中實體類進行擴充的擴充屬性。

定義屬性時,一同指定它對應的中繼資料。

l 啟動期

此階段主要以客戶化定義的方式來對編譯期屬性及其相應的中繼資料進行修改。

l 運行期

該階段主要用於附加運行時動態屬性。

這些動態屬性一般只用於顯示,它們會影響介面的產生。屬性的擴充和刪除,要在產生控制項之前就能確定,否則,介面沒有對應的列。

由於影響介面產生,所以需要為其指定OEA架構中對應的介面中繼資料。如果不指定,則使用預設中繼資料。不過這些中繼資料的設計會在OEA架構中完成,與EMPS的設計無關。

在這個階段中擴充的附加屬性,不會與服務端程式有任何關係。也就是說,不需要為這些擴充屬性定義 ORM 等服務端中繼資料。當然了,這些屬性的資料也不需要序列化後在網路上進行傳輸。

劃分出這幾個周期的主要原因:使得可以判斷出某個實體的編譯期、啟動期屬性列表長度。這是因為,編譯期和啟動期已經定義、修改或者客戶化的屬性,當程式進入運行時後是不會再發生改變的。而這些屬性佔據了應用開發的95%以上。所以我們只要知道了編譯期啟動期屬性的長度,也就意味著可以使用O(1)檢索的數組來存放,而不是更慢的List/HashTable,保證了這些屬性的效能。而對於運行時屬性來說,雖然它的長度不能固定,根據業務情境而變化,但是使用方式較少,可以不考慮效能。

3.3.2 相關UML圖

完整的UML圖,參見:《實體擴充屬性UML設計圖》。下面將挑選重點進行說明。

圖5.擴充屬性核心類結構概要設計圖

這張是實現擴充屬性的核心類結構概要設計圖,其中主要包含 ManagedPropertyObjectManagedPropertyManagedPropertyFieldManagedPropertyMeta等。由於是概要設計圖,其中的方法、屬性等相對實現完成後的系統來比,肯定不完整,但是它的作用主要是說明整個設計的核心思想。其中:

ManagedProperty 表示Managed 屬性,每定義一個Managed 屬性,系統都會產生一個此類型的對象用於標記。擷取、設定屬性的值時,都需要提供此標記來進行檢索。

ManagedPropertyMeta 表示Managed 屬性中繼資料,其中提供了許多資訊,例如:預設值、是否唯讀、屬性變更邏輯回調等;這些中繼資料對屬性值的擷取、設定的邏輯都有著比較大的影響。

ManagedPropertyField 表示某個對象中某個Managed 屬性對應的值。其實這個類後期在實現時會被定義為泛型類,這樣,值的儲存就不是object而是強型別的,不需要裝箱拆箱操作。

ManagedPropertyObject 表示擁有Managed 屬性的對象基類(實體),其中定義了根據ManagedProperty來擷取、設定值的介面,這使得該對象能夠象一般對象一樣擷取、儲存各種值。同時,它也提供了統一處理所有Managed 屬性值的介面,此類統一處理的介面在應用開發時很少用到,主要給上層的架構使用。上層架構可以應用這些介面完成以下的架構任務:統一的對象值拷貝、統一的序列化、檢索特定類型的值等,這樣的值的擷取、設定速度,遠比反射要快。

圖6. 擴充屬性倉儲概要設計圖

這張圖說明整個系統中的Managed 屬性都是被系統中的單例對象 ManagedPropertyRepository 給管理起來的,為了給上層提供更方便的查詢功能,也方便儲存,它使用 TypeIndicators 類來儲存某個實體類型的屬性列表。TypeIndicators這個類也負責為上層提供查詢:某一個類型已經定義好的屬性列表、某一類型及其所有父類定義的所有屬性的聯合屬性列表。同時,這個類中的屬性都會產生在類型中的屬性的索引,這樣,在擷取屬性值時就可以使用這個索引在屬性值數組中進行屬性值的尋找。

圖7.擴充屬性中繼資料概要設計圖

前面提到過,為了保證 EMPS的獨立性,我們需要把Managed 屬性中繼資料的資料擷取方案抽象化,這裡的 IPropertyMetaProvider 就是抽象的中繼資料提供器介面。而上層的OEA架構則會實現自己的提供器 OEAPropertyMetaProvider,並通過自己的中繼資料模組中的資訊(例中的OEAPropertyMeta)來為Managed 屬性提供中繼資料。

圖8. 擴充屬性實體實現WPF綁定相關概要設計圖

這張圖看上去會比較眼熟?沒錯,它和圖2中的WPF支援DataTable綁定的類圖比較相似。主要也是讓 EntityList 實現 IListSource介面,並添加 EntityView 類實現 ICustomTypeDescriptor 介面,這樣,就可以實現動態屬性的WPF綁定了。

3.3.3 如何支撐需求

主要的設計方案及類圖看完之後,我們需要考慮,整個方案是否能支撐起前面說到的所有需求。是對整個需求的可支撐性進行分析。相關內容可以參見:《實體擴充屬性方案分析腦圖》,在此不再贅述。

圖9. 設計方案對需求的支援度分析。

3.4 重點實現細節

在對需求支援分析之後,再經過召開的設計評審會議,發現這樣的方案可以實現基本全部需求。這樣,就進入實現環節了。實現環節就關注更多了細節性設計。本文檔將會挑選一部分重點進行說明。

首先,先來看看最終完成的代碼中,最核心部分的代碼結構圖:

圖10. 核心代碼結構圖

整個結構的實現與設計相差無幾。接下來,說明一些相對重要的代碼:

l 先是ManagedPropertyObject中的屬性值擷取、設定相關代碼:

前面的設計方案中提到,這個類主要作為所有實體類的基類,提供值的擷取、設定等。而這個類其實是把屬性值的管理都放到了內部的一個類ManagedPropertyObjectFieldsManager 中:

並把相關的操作都代理到這個類上:

ManagedPropertyObjectFieldsManager則實現了這些邏輯的核心代碼。其中,它的私人欄位定義如下:

可以看到,編譯期、啟動期屬性值與運行期屬性值被分開存放。前者使用數組,建構函式直接初始化,而後者則在需要時才會被序列化。還注意到,它繼承自CustomSerializationObject,使得整個屬性值列表是可以被自訂序列化的。下面,是泛型的屬性值擷取與設定邏輯:

internal TPropertyType GetProperty<TPropertyType>(ManagedProperty<TPropertyType> property)

{

var useDefault = true;

    TPropertyType result = default(TPropertyType);

if (property.IsReadOnly)

    {

        result = (property as ManagedProperty<TPropertyType>).ProvideReadOnlyValue(this._owner);

        useDefault = false;

    }

else

    {

if (property.LifeCycle == ManagedPropertyLifeCycle.CompileOrSetup)

        {

var field = this._compiledFields[property.TypeCompiledIndex] as ManagedPropertyField<TPropertyType>;

if (field != null)

            {

                result = field.Value;

                useDefault = false;

            }

        }

else

        {

if (this._runtimeFields != null)

            {

IManagedPropertyField f;

if (this._runtimeFields.TryGetValue(property, out f))

                {

var field = f as ManagedPropertyField<TPropertyType>;

                    result = field.Value;

                    useDefault = false;

                }

            }

        }

}

var meta = property.GetMeta(this);

if (useDefault) result = meta.DefaultValue;

result = meta.CoerceGetValue(this._owner, result);

return result;

}

internal void SetProperty<TPropertyType>(ManagedProperty<TPropertyType> property, TPropertyType value, ManagedPropertyChangedSource source)

{

    ForceNotReadOnly(property);

var meta = property.GetMeta(this);

bool cancel = meta.RaisePropertyChanging(this._owner, ref value, source);

if (cancel) return;

var hasOldValue = false;

    TPropertyType oldValue = default(TPropertyType);

ManagedPropertyField<TPropertyType> field = null;

//這個 if 塊中的代碼:尋找或建立對應 property 的 field,同時記錄可能存在的曆史值。

if (property.LifeCycle == ManagedPropertyLifeCycle.CompileOrSetup)

    {

        field = this._compiledFields[property.TypeCompiledIndex] as ManagedPropertyField<TPropertyType>;

if (field == null)

        {

//不管是不是預設值,都進行儲存。

//不需要檢測預設值更加快速,但是浪費了一些小的空間。

//預設值的檢測,在 GetNonDefaultPropertyValues 方法中進行實現。

            field = property.CreateField();

this._compiledFields[property.TypeCompiledIndex] = field;

        }

else

        {

            oldValue = field.Value;

            hasOldValue = true;

        }

    }

else

    {

if (this._runtimeFields == null)

        {

this._runtimeFields = new Dictionary<IManagedProperty, IManagedPropertyField>();

        }

else

        {

IManagedPropertyField f;

if (this._runtimeFields.TryGetValue(property, out f))

            {

                field = f as ManagedPropertyField<TPropertyType>;

                oldValue = field.Value;

                hasOldValue = true;

            }

        }

if (field == null)

        {

            field = property.CreateField();

this._runtimeFields.Add(property, field);

        }

    }

    field.Value = value;

if (!hasOldValue) { oldValue = meta.DefaultValue; }

if (!object.Equals(oldValue, value))

    {

//發生 Meta 中的事件

var args = meta.RaisePropertyChanged(

this._owner, oldValue, value, source

            );

//發生事件

this._owner.RaisePropertyChanged(args);

    }

}

可以看到,編譯期屬性主要通過一維數組進行存放,數組中每一個元素都是強型別的泛型對象 ManagedPropertyField<TPropertyType>。

另外,要注意的是,該類提供了同樣的非泛型介面:

非泛型方法主要是為上次架構提供,其中主要考慮裝箱拆箱操作的效能消耗。(關於介面加泛型類的底層架構設計方案,參見:《重構實踐:體驗interface的威力(一)》、《重構實踐:體驗interface的威力(二)》。)

GetProperty、SetProperty 方法是對效能最敏感的兩個方法,其實現必須特別小心,其內部調用的每一個方法,如 ManagedProperty.GetMeta(ManagedPropertyObject owner)、ManagedPropertyMetadata.RaisePropertyChanged等,也都必須要做特別的最佳化,需要考慮到裝箱拆箱、屬性檢索、不構造多餘對象等。具體代碼設計參考實際工程中的代碼,在此不做過多描述。

l 註冊屬性(及反註冊屬性)的方法位於 ManagedPropertyRepository類中。

其主要的職責是構造ManagedProperty對象,並計算它的一些重要屬性,例如:用於屬性快速Hash的GlobalIndex、用於編譯期屬性檢索值時使用的數組下標TypeIndex等。

4 設計驗證4.1 功能需求驗證

已經為EMPS添加了豐富的單元測試,該項驗證的內容被整合到第5項中,參見:《5.使用手冊》。

4.2 WPF綁定驗證

驗證這個比較簡單,只要基於它的應用程式運行起來之後,介面上的值都能正常擷取、設定即可。

不過,我們還是為它加了相應的單元測試,這個在後面會有描述。

4.3 效能驗證

之前說到如果EMPS相比原來的屬性系統,如果耗時超過120%,則該系統不可用。按照之前關鍵效能點的設計,應該是可以達到,但是,還是需要做出相應的驗證工作及最終的資料。

具體內容可以參見:《實體擴充屬性系統效能測試報告》。這裡說明一下最終結論:

效能結論:

新的 OEA Managed 屬性系統,在帶來眾多新功能的同時,不但沒有降低原有效能,反而因為最佳化掉無用的代碼,使得速度提升。故可以放心使用該屬性系統。

5 使用手冊5.1 使用情境介紹(單元測試)

由於已經為EMPS添加了比較豐富的單元測試,所以本使用手冊將主要以介紹單元測試的形式,覆蓋所有可能的使用情境,並介紹每一個情境其對應的使用方法。

單元測試所使用的實體類包含中的這些類:

右圖是所涉及到的所有單元測試。

“……………………內容較多,省略,有興趣的可以看附件中的文檔………………”

5.2 代碼產生 – CodeSnippets

在《OEA 架構示範 - 快過原型的產品開發》中可以看到,OEA架構的快速開發能力中,編譯期的代碼產生技術是一個重要部分。我們同樣為EMPS的80%使用情境都編寫了CodeSnippets(關於CodeSnippets的使用方法,參見:《善用VS中的Code Snippet來提高開發效率》):

匯入VS後,只要輸入OEAP……,VS就支援這些程式碼片段的產生,如:

5.3 其它問題5.3.1 擴充屬性的CLR屬性編寫注意點

使用EMPS定義的屬性,如果不是擴充屬性,都會定義一個對應的CLR屬性包裝器,如:

注意,CLR屬性內,不能添加任何代碼,所有需要對Code屬性的Get、Set的定製代碼,都需要以回調的形式編寫在EMPS中,如:

原因是介面架構、ORM架構、WPF綁定等架構內容都不會調用CLR屬性,而是直接調用GetProperty、SetProperty方法,而CLR中的代碼只是為了方便類庫的使用。

5.3.2 何時使用屬性擴充,何時使用繼承擴充?

EMPS雖然可以直接對某個實體類型進行屬性的擴充,但是我們依然老的方案,即使用CLR類繼承機制擴充舊的實體。那麼,我們需要特別注意兩種方案的區別:

1. 屬性擴充是直接對指定的領域實體進行擴充,一旦擴充,該領域實體類在整個應用程式中的屬性都被擴充。

2. 而繼承擴充則需要用於不同的領域實體中。

簡單地說,當你想在應用程式中擴充出一個新的領域實體類或者做一個全新的介面時,則使用繼承擴充。而當在做客戶化時,希望對現有的領域實體類進行完全擴充時,則應該使用EMPS來進行屬性擴充。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.