當資料庫只存放 Key,UI 需要呈現"說明"讓用戶可以閱讀,常見的做法有:DB 存放說明欄位、應用程式定義說明欄位。
這個 Key 是給應用程式判斷邏輯用是常數,我選擇放在應用程式,若 DB 也要閱讀定義,就從應用程式寫到 DB;反之,你也可以統一在 DB 定義,透過 T4 產生 cs,讓應用程式使用。
不管你選哪種方式,統一一種就好。
假如在資料庫存放著 IsTransform、 Status 欄位的狀態,如下圖
當應用程式需要判斷 IsTransform 的狀態時,你可能會這樣寫
if (orders[0].IsTransform == "Y") { //Do Thing }
if (orders[0].Status == "99") { //Do Thing }
orders[0] 是來自資料庫的查詢結果,假如 IsTransform 為 "Y"、Status 為 "99",進入邏輯判斷;閱讀程式碼時看到 "99" 還要想一下這是甚麼意思,有更好的做法嗎?
比較好的做法是用一個變數把狀態描述清楚,消除魔術數字、字串,換個寫法程式碼的可讀性變高了
string Approve = "99"; if (orders[0].Status == Approve) { //Do Thing }
當定義越來越多的時候,你可以用列舉
這個給 Status 欄位用,資料庫存放的是列舉的 Value,"99"、"10"
public enum EnumApprove { /// <summary> /// 已核准 /// </summary> Approve = 99, /// <summary> /// 開立 /// </summary> Open = 10 }
假如資料庫放 "99",轉一下就能拿到列舉然後進入邏輯判斷
Enum.TryParse("99", out EnumApprove value); switch (value) { case EnumApprove.Approve: break; case EnumApprove.Open: break; default: throw new ArgumentOutOfRangeException(); }
這給 IsTransform 欄位用,資料庫存放的是列舉的項目,Y、N
public enum EnumTransfer { /// <summary> /// 已轉換 /// </summary> Y = 1, /// <summary> /// 未轉換 /// </summary> N = 0. }
不論是用列舉成員還是值,都可以幫你轉好
東西不用嗎?
或者,也可以把他們集中放在某一個類別、結構
public class TransferStatus { public static string Transform { get; set; } = "Y"; public static string NoTransform { get; set; } = "N"; }
public class ApproveStatus { public static string Approve { get; set; } = "99"; public static string Open { get; set; } = "10"; }
問題來了,當一個狀態不夠用需要增加時怎麼辦?資料庫的 Key 怎麼對應到擴充的狀態
若你是使用列舉的話,可以使用 Attribute 替列舉增加欄位
public class DefineAttribute : Attribute { public string Code { get; set; } public string Description { get; set; } public string Name { get; set; } public int Value { get; set; } public DefineAttribute(string code, string description) { this.Code = code; this.Description = description; } }
public enum EnumApprove { /// <summary> /// 已核准 /// </summary> [Define("99", "已核准")] Approve = 99, /// <summary> /// 開立 /// </summary> [Define("10", "已開立")] Open = 10 }
為了可以透過 Key/Value 去找到正確的擴充屬性,我把列舉項目轉成 Dictionary
private static readonly ConcurrentDictionary<Type, Dictionary<string, DefineAttribute>> _nameCaches = new ConcurrentDictionary<Type, Dictionary<string, DefineAttribute>>(); public static Dictionary<string, DefineAttribute> GetLookupByName(Type type) { Dictionary<string, DefineAttribute> results = null; if (_nameCaches.ContainsKey(type)) { results = _nameCaches[type]; } else { results = new Dictionary<string, DefineAttribute>(); foreach (var item in Enum.GetValues(type)) { var result = (DefineAttribute) type.GetMember(item.ToString())[0] .GetCustomAttributes(typeof(DefineAttribute), false)[0]; result.Name = item.ToString(); result.Value = Convert.ToInt32(item); results.Add(result.Name, result); } _nameCaches.TryAdd(type, results); } return results; }
這是透過 Value 取擴充屬性
private static readonly ConcurrentDictionary<Type, Dictionary<string, DefineAttribute>> _valueCaches = new ConcurrentDictionary<Type, Dictionary<string, DefineAttribute>>(); public static Dictionary<string, DefineAttribute> GetLookupByValue(Type type) { Dictionary<string, DefineAttribute> results = null; if (_valueCaches.ContainsKey(type)) { results = _valueCaches[type]; } else { results = new Dictionary<string, DefineAttribute>(); foreach (var item in Enum.GetValues(type)) { var result = (DefineAttribute) type.GetMember(item.ToString())[0] .GetCustomAttributes(typeof(DefineAttribute), false)[0]; result.Name = item.ToString(); result.Value = Convert.ToInt32(item); results.Add(result.Value.ToString(), result); } _valueCaches.TryAdd(type, results); } return results; }
使用時就帶入列舉成員的名字查詢
[TestMethod] public void GetLookupByName_EnumApprove_Open() { var description = DefineManager.GetLookupByName<EnumApprove>()["Open"].Description; Assert.AreEqual("已開立", description); }
另外,再增加一個擴充方法
public static DefineAttribute GetDefineByName(this Enum source) { return GetLookupByName(source.GetType())[source.ToString()]; }
使用起來更方便
[TestMethod] public void GetEnumLookup_Extension_Test() { var description = EnumTransfer.N.GetDefine().Description; Assert.AreEqual("未轉換", description); }
資料最好統一是要放列舉的項目或值,不然還要判斷現在是項目還是值
列舉的方法看完了,接下來看用類別的用法,很簡單把原本的 string 換成 Status
public class Status { public string Code { get; set; } public string Description { get; set; } }
public class ApproveStatus { public static Status Approve { get; set; } = new Status {Code = "99", Description = "已核准"}; public static Status Open { get; set; } = new Status {Code = "10", Description = "開立"}; }
public class TransferStatus { public static Status Transform { get; set; } = new Status {Code = "Y", Description = "已轉換"}; public static Status NoTransform { get; set; } = new Status {Code = "N", Description = "未轉換"}; }
為了可以透過 Key 去找到正確的擴充屬性,我把靜態屬性轉成 Dictionary
public static class DefineManager { private static readonly ConcurrentDictionary<Type, Dictionary<string, Status>> _caches = new ConcurrentDictionary<Type, Dictionary<string, Status>>(); public static Dictionary<string, Status> GetLookup<T>() { Dictionary<string, Status> result = null; var type = typeof(T); if (_caches.ContainsKey(type)) { result = _caches[type]; } else { result = new Dictionary<string, Status>(); var propertyInfos = type.GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public); foreach (var property in propertyInfos) { var value = property.GetValue(null, null) as Status; result.Add(value.Code, value); } _caches.TryAdd(type, result); } return result; } }
調用端調用
[TestMethod] public void GetLookup_By_ApproveStatus_Approve() { var description = DefineManager.GetLookup<ApproveStatus>()["99"].Description; Assert.AreEqual("已核准", description); }
[TestMethod] public void GetLookup_By_TransferStatus_Y() { var description = DefineManager.GetLookup<TransferStatus>()["Y"].Description; Assert.AreEqual("已轉換", description); }
該選哪一種方式來進行常數的定義??
實作下來的感覺會發現用 class 定義多欄位的方式會比較簡單,列舉在使用上要注意一下,比如說要統一用列舉的項目或是值
專案位置
https://github.com/yaochangyu/sample.dotblog/tree/master/Lab.NoMagicNumeric
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET