[C#.NET] 如何定義多欄位的常數

  • 2514
  • 0
  • C#
  • 2019-09-06

當資料庫只存放 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

Image result for microsoft+mvp+logo