摘要:前言在應用開發中,通常都會涉及各種實體類的編寫,有時這些實體類還需要實現接口以支持屬性變更通知,一般我們都會手寫這些代碼或者通過工具根據數據庫表定義抑或別的什么模板映射文件之類的來生成它們。
前言
在應用開發中,通常都會涉及各種 POJO/POCO 實體類(DO, DTO, BO, VO)的編寫,有時這些實體類還需要實現 INotifyPropertyChanged 接口以支持屬性變更通知,一般我們都會手寫這些代碼或者通過工具根據數據庫表定義抑或別的什么模板、映射文件之類的來生成它們。
但是,在業務實現中往往伴隨著諸如“如何簡單且高效的獲取某個實體實例有哪些屬性發生過變更?”、“變更后的值是什么?”這樣的問題,而大致的解決方法有:
由實體容器來跟蹤實例的屬性變更;
改造實體類(譬如繼承特定實體基類,在基類中實現這些基礎構造)。
方法(1)需要配合一整套架構設計來提供支撐,也不是專為解決上述實體類的問題而設,并且實現和使用也都不夠簡單高效,故此略過不表。接下來我將通過幾篇文章來詳細闡述這些問題的來由以及解決方案,并給出完整的代碼實現以及性能比對測試。
關于源碼下面將要介紹的所有代碼均位于我們的開源系列項目(地址:https://github.com/Zongsoft),項目主要采用 LGPL 2.1授權協議,歡迎大家參與并使用(請遵照授權協議)。而本文相關的源碼位于其中 Zongsoft.CoreLibrary 項目的 feature-data 分支(https://github.com/Zongsoft/Zongsoft.CoreLibrary/tree/feature-data)及其中的 /samples/Zongsoft.Samples.Entities 范例項目,由于目前我正在忙著造 Zongsoft.Data 數據引擎這個輪子,不排除后面介紹到的代碼會有一些調整,待該項目完成后這些代碼亦會合并到 master 分支中,敬請留意。
基礎版本萬里長城也是從第一塊磚頭開始磊起來的,就讓我們來搬第一塊磚吧:
public class User { private uint _userId; private string _name; // 傳統寫法 public uint UserId { get { return _userId; } set { _userId = value; } } // C# 7.0 語法 public string Name { get => _name; set => _name = value; } // 懶漢寫法:僅限不需要操作成員字段的場景 public string Namespace { get; set; } }
以上代碼特地用了三種編碼方式,它們被C#編譯器生成的IL沒有模式上的不同,故而性能沒有任何區別,大家根據自己的口味采用某種即可,因為我們的源碼由于歷史原因可能會有一些混寫,在此一并做個展示而已。
由于業務需要,我們希望實體類能支持屬性變更通知,即讓它支持 INotifyPropertyChanged 接口,這么簡單的需求當然不在話下:
public class User : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private uint _userId; private string _name; public uint UserId { get => _userId; set { if(_userId == value) return; _userId = value; this.OnPropertyChanged("UserId"); // 傳統寫法 } } public string Name { get => _name; set { if(_name == value) return; _name = value; this.OnPropertyChanged(nameof(Name)); // nameof 為 C# 7.0 新增操作符 } } protected virtual void OnPropertyChanged(string propertyName) { // 注意 ?. 為 C# 7.0 新增操作符 this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
一切看起來是那么完美,但是,當我們寫了幾個這樣的實體類,尤其是有些實體類的屬性還不少時,體驗就有點糟糕了。自然我們會想到寫個實體基類來實現屬性變更通知的基礎構造,當然,在某些特定場景也可以通過工具來生成類似上面這樣的C#實體類文件,但工具生成的方式有一定局限性并且不易維護(譬如需要在生成的代碼基礎上進行特定改造),在此不再贅述。
實體基類在進行基礎類庫或API設計的時候,我有個建議:__從應用場景開始__。具體的作法是,先嘗試編寫使用這些API的應用代碼,待各種應用場景的使用代碼基本都完成后,API接口也就自然而然的確定了。譬如,在我們這個需求中我希望這么去使用實體基類:
public class User : ModelBase { private uint _userId; private string _name; public uint UserId { get => _userId; set => this.SetPropertyValue(nameof(UserId), ref _userId, value); } public string Name { get => _name; set => this.SetPropertyValue(nameof(Name), ref _name, value); } }
有了這樣的實體基類后,增強了功能后代碼依然如第一塊磚的“基礎版本”一樣簡潔,真是高興啊!但這就夠了么,能不能把具體實體類里面的成員字段也省了,交給基類來處理呢?嗯,有點意思,試著寫下應用場景代碼:
public class User : ModelBase { public uint UserId { get => (uint)this.GetPropertyValue(nameof(UserId)); set => this.SetPropertyValue(nameof(UserId), value); } }
看起來棒極了,代碼變得更簡潔了,真是天才啊!淡定,喪心病狂的 C# 設計者似乎看到了這種普遍的需求,于是在 C# 5 中增加了 System.Runtime.CompilerServices.CallerMemberNameAttribute 自定義標記,C# 編譯器將自動把調用者名字生成出來傳遞給加注了該標記的參數,因此這樣的代碼還可以繼續簡化:
public class User : ModelBase { public uint UserId { get => (uint)this.GetPropertyValue(); set => this.SetPropertyValue(value); } }
但是,屬性的 getter 里面的那個類型強制轉換,怎么看都像是一朵“烏云”啊,能不能把它也去掉呢?嗯,利用C#的泛型類型推斷可以完美解決它,繼續強勢進化:
public class User : ModelBase { public uint UserId { get => this.GetPropertyValue(() => this.UserId); set => this.SetPropertyValue(() => this.UserId, value); } }
哇喔,有點小崇拜自己了,這代碼漂亮的一批!至此,實體基類的API接口基本確定,已經迫不及待想要去實現它了。
提示:由于采用 CallerMemberNameAttribute 自定義標記的參數會導致 C# 編譯器要求該參數必需有默認值,因此有些 SetPropertyValue(...) 方法重載版本中 propertyName 參數需要位于參數集的最后,為了與上面的范例代碼對應就省略了這些參數的標記,并保持與原有范例相同的簽名設計。
using System; using System.Linq.Expressions; public class ModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected object GetPropertyValue([CallerMemberName]string propertyName = null); protected T GetPropertyValue(Expression > property); protected void SetPropertyValue (string propertyName, ref T field, T value); protected void SetPropertyValue (string propertyName, T value); protected void SetPropertyValue (Expression > property, T value); }
實體基類的實現主要思路就是采用字典來記錄各屬性的變更值,有了這個基礎,要繼續增加諸如“獲取哪些屬性發生過變更”之類的需求自然就很容易了:
public class ModelBase : INotifyPropertyChanged { // other members public bool HasChanges(params string[] propertyNames); public IDictionaryGetChangedPropertys(); }
具體的代碼就不在這里貼出了,有興趣的可以參考:https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/master/src/Common/ModelBase.cs,從功能角度上看,目前的設計還是不錯的。但是,某些方法的設計有嚴重性能缺陷的,主要有以下幾點:
每次讀寫屬性都會解析Lambda 表達式的操作會產生巨大的性能損耗;
采用字典來保存實體屬性值的設計機制,會導致值類型的屬性讀寫反復被裝箱(Boxing)、拆箱(Unboxing);
字典的讀寫效率也遠低于直接操作成員字段的語言原語方式。
綜上所述,雖然目前方案有性能缺陷,但應對一般場景其實是沒有問題的,而且功能和易用性方面都是很好的;但是,性能對于后臺程序猿而言猶如懸在頭頂的 達摩克利斯之劍,這正是這個系列文章要最終解決的問題。在此之前,如果大家有關于這個問題的性能優化方案,歡迎關注我們的公眾號(Zongsoft)留言討論。
敬請期待更精彩的下篇,關注我們的公眾號可以第一時間看到哦!
本文可能會更新,請閱讀原文: https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-1,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。
本作品采用?知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議?進行許可。歡迎轉載、使用、重新發布,但必須保留本文的署名 鐘峰(包含鏈接:http://zongsoft.github.io),不得用于商業目的,基于本文修改后的作品務必以相同的許可發布。如有任何疑問或授權方面的協商,請致信給我 (zongsoft@qq.com)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/76656.html
摘要:源碼位于項目的命名空間中表示數據實體的接口。獲取實體中發生過變更的屬性集。新辦法解決辦法其實很簡單,正是本文的標題動態生成,徹底解放實現者并確保實現的正確性。業務方不再定義具體的實體類,而是定義實體接口即可,實體類將由實體生成器來動態生成。 前言 由于采用字典的方式來保存屬性變更值的底層設計思想,導致了性能問題,雖然.NET的字典實現已經很高效了,但相對于直接讀寫字段的方式而言依然有巨...
摘要:與靜態代理對比,動態代理是在動態生成代理類,由代理類完成對具體方法的封裝,實現的功能。本文將分析中兩種動態代理的實現方式,和,比較它們的異同。那如何動態編譯呢你可以使用,這是一個封裝了的庫,幫助你方便地實現動態編譯源代碼。 發現Java面試很喜歡問Spring AOP怎么實現的之類的問題,所以寫一篇文章來整理一下。關于AOP和代理模式的概念這里并不做贅述,而是直奔主題,即AOP的實現方...
閱讀 3225·2021-11-24 09:39
閱讀 3158·2021-10-21 09:38
閱讀 2396·2019-08-29 15:28
閱讀 3737·2019-08-26 12:23
閱讀 2615·2019-08-26 12:19
閱讀 1358·2019-08-23 12:44
閱讀 2125·2019-08-23 12:02
閱讀 993·2019-08-22 17:05