我最近在造一個比 Excel 差得多的表格控件,其中一個需求是屬性的繼承。大家都知道,表格里面有單元格,單元格里面允許放文本,文本可以放多段文本。本文的主角就是文本段落的樣式屬性,包括文本字體字號顏色等等屬性。文本段落的屬性,如果沒有特別設置,將使用單元格里面的文本樣式屬性。而如果單元格里面,沒有特別指定此單元格使用特殊的文本樣式,將會繼承使用當前所在的行的文本樣式。如果當前行沒有特殊指定文本樣式屬性,那么將會使用文檔的默認樣式。文檔默認樣式將會根據是否有特殊指定而采用主題樣式
如此復雜的層層繼承邏輯,如果每個屬性都需要自己一層層去尋找,那代碼量將會特別多。維護起來就想吃桌子
為了保住桌子,咱來寫一個支持層層繼承屬性的對象。如在當前層找不到某個屬性,將會往上一層自動去找,一層層找。如果都找不到,將返回默認值
以下是這個類的定義代碼
public class FlattenObject
{
/// <summary>
/// 創建帶繼承的對象
/// </summary>
/// <param name="reserved"></param>
public FlattenObject(FlattenObject? reserved = null)
{
Reserved = reserved;
}
private FlattenObject? Reserved { get; }
private Dictionary<string, object> ValueDictionary { get; } = new Dictionary<string, object>();
/// <summary>
/// 設置屬性值
/// </summary>
/// <param name="value"></param>
/// <param name="propertyName"></param>
protected void SetValue(object value, [CallerMemberName] string propertyName = null!)
{
ValueDictionary[propertyName] = value;
}
/// <summary>
/// 獲取屬性值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="propertyName"></param>
/// <returns></returns>
protected T? GetValue<T>([CallerMemberName] string propertyName = null!)
=> GetValue<T>(default!, propertyName);
/// <summary>
/// 獲取屬性值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="defaultValue"></param>
/// <param name="propertyName"></param>
/// <returns></returns>
protected T GetValue<T>(T defaultValue, [CallerMemberName] string propertyName = null!)
{
if (ValueDictionary.TryGetValue(propertyName, out var value))
{
return (T) value;
}
else
{
if (Reserved is not null)
{
return Reserved.GetValue<T>(defaultValue, propertyName);
}
else
{
return defaultValue;
}
}
}
}
通過 Reserved 屬性表示的是當前層的上一層的對象,用於給當前層進行繼承。因為每一層都包含了上一層的對象,因此從最下層就可以一層層自動找到屬性的值
繼承當前類型,即可寫出下面代碼
class FooFlattenObject : FlattenObject
{
public FooFlattenObject(FlattenObject reserved = null) : base(reserved)
{
}
public string FontName
{
set => SetValue(value);
get => GetValue<string>();
}
public int Count
{
set => SetValue(value);
get => GetValue<int>();
}
}
如上面代碼,在各個屬性的 set 和 get 都換成調用方法,而不需要定義字段
下面來嘗試寫單元測試
"給定可繼承的對象,可以從繼承的對象拿到屬性值".Test(() =>
{
var reserved = new FooFlattenObject()
{
FontName = "微軟雅黑"
};
var fakeFlattenObject = new FooFlattenObject(reserved);
Assert.AreEqual("微軟雅黑", fakeFlattenObject.FontName);
fakeFlattenObject.Count = 2;
Assert.AreEqual(2, fakeFlattenObject.Count);
Assert.AreEqual(0, reserved.Count);
});
可以看到在 reserved 對象里面設置了 FontName 的值,可以被 fakeFlattenObject 繼承拿到,同時自動讀取的代碼對於上層業務來說幾乎沒有
對 fakeFlattenObject 進行設置 Count 的值,不會影響到 reserved 對象
通過此方法可以讓存在層層繼承邏輯的代碼不需要大量重復。除了在表格上使用,也可以用在如解析 PPT 的形狀內文本,如 PPT 的圖片裁剪等需要繼承屬性的邏輯上
上面的代碼也存在不足,那就是對於結構體不友好,如 bool 或 int 等類型,都需要轉換為 object 存放
