在應用開發中,通常都會涉及各種 POJO/POCO 實體類(DO, DTO, BO, VO)的編寫,這些看起來平常枯燥的任務底下很可能隱藏著諸多性能和功能缺陷,剛好這裡有個不用手寫代碼、易用還兼顧功能和高性能的方案,瞭解下... ...
前言
在應用開發中,通常都會涉及各種 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<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,從功能角度上看,目前的設計還是不錯的。但是,某些方法的設計有嚴重性能缺陷的,主要有以下幾點:
- 每次讀寫屬性都會解析 Lambda 表達式的操作會產生巨大的性能損耗;
- 採用字典來保存實體屬性值的設計機制,會導致值類型的屬性讀寫反覆被裝箱(Boxing)、拆箱(Unboxing);
- 字典的讀寫效率也遠低於直接操作成員欄位的語言原語方式。
綜上所述,雖然目前方案有性能缺陷,但應對一般場景其實是沒有問題的,而且功能和易用性方面都是很好的;但是,性能對於後臺程式猿而言猶如懸在頭頂的達摩克利斯之劍,這正是這個系列文章要最終解決的問題。在此之前,如果大家有關於這個問題的性能優化方案,歡迎關註我們的公眾號(Zongsoft)留言討論。
敬請期待更精彩的下篇,關註我們的公眾號可以第一時間看到哦!
提示:
本文可能會更新,請閱讀原文:https://zongsoft.github.io/blog/zh-cn/zongsoft/entity-dynamic-generation-1/,以避免因內容陳舊而導致的謬誤,同時亦有更好的閱讀體驗。