在應用開發中,一般都會涉及各類 POJO/POCO 實體類(DO, DTO, BO, VO)的編寫,有時這些實體類還須要實現 INotifyPropertyChanged
接口以支持屬性變動通知,通常咱們都會手寫這些代碼或者經過工具根據數據庫表定義抑或別的什麼模板、映射文件之類的來生成它們。html
可是,在業務實現中每每伴隨着諸如「如何簡單且高效的獲取某個實體實例有哪些屬性發生過變動?」、「變動後的值是什麼?」這樣的問題,而大體的解決方法有:git
方法(1)須要配合一整套架構設計來提供支撐,也不是專爲解決上述實體類的問題而設,而且實現和使用也都不夠簡單高效,故此略過不表。接下來我將經過幾篇文章來詳細闡述這些問題的來由以及解決方案,並給出完整的代碼實現以及性能比對測試。github
下面將要介紹的全部代碼均位於咱們的開源系列項目(地址: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<T>(Expression<Func<T>> property); protected void SetPropertyValue<T>(string propertyName, ref T field, T value); protected void SetPropertyValue<T>(string propertyName, T value); protected void SetPropertyValue<T>(Expression<Func<T>> property, T value); }
實體基類的實現主要思路就是採用字典來記錄各屬性的變動值,有了這個基礎,要繼續增長諸如「獲取哪些屬性發生過變動」之類的需求天然就很容易了:
public class ModelBase : INotifyPropertyChanged { // other members public bool HasChanges(params string[] propertyNames); public IDictionary<string, object> GetChangedPropertys(); }
具體的代碼就不在這裏貼出了,有興趣的能夠參考:https://github.com/Zongsoft/Zongsoft.CoreLibrary/blob/master/src/Common/ModelBase.cs,從功能角度上看,目前的設計仍是不錯的。可是,某些方法的設計有嚴重性能缺陷的,主要有如下幾點:
綜上所述,雖然目前方案有性能缺陷,但應對通常場景實際上是沒有問題的,並且功能和易用性方面都是很好的;可是,性能對於後臺程序猿而言猶如懸在頭頂的 達摩克利斯之劍,這正是這個系列文章要最終解決的問題。在此以前,若是你們有關於這個問題的性能優化方案,歡迎關注咱們的公衆號(Zongsoft)留言討論。
敬請期待更精彩的下篇,關注咱們的公衆號能夠第一時間看到哦!
本文可能會更新,請閱讀原文: https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-1,以免因內容陳舊而致使的謬誤,同時亦有更好的閱讀體驗。
本做品採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議<span data-type="color" style="color:rgb(79, 79, 79)"> </span>進行許可。歡迎轉載、使用、從新發布,但必須保留本文的署名 鍾峯(包含連接:http://zongsoft.github.io),不得用於商業目的,基於本文修改後的做品務必以相同的許可發佈。若有任何疑問或受權方面的協商,請致信給我 (zongsoft@qq.com)。