利用反射取代if判斷


日常開發很常碰到一個情況,就是需要依據傳入的參數,決定 new 不一樣的 instance 出來,所以在數量少的時候,我們可以透過if...else...的方式直衝,再多一些些,可以用switch...case...的方式來做,但如果這個分支已經有 5~7 個以上,再接著用上述的兩種做法就有點bad smell的感覺了

這裡用反射+Attribute的方式來解這個問題,其實並不算完美的作法,因為相對的best practice可能每個人的觀念及想法都不同,我只是將我覺得好維護的方法紀錄一下,底下的 Code 都是先 Copy 過來後手打編輯,刪掉很多不能出現的東西,所以不要直接拿去用喔

情境

假設現在的情況是做一個搜尋引擎,前端傳來的部分包含了實際搜尋的 keyword,還有搜尋類型 type,前端透過下拉選單去變更這個搜尋類型,希望後端可以依據不同的搜尋類型,有不一樣的邏輯。 case 大概就是這樣,初版的 Code,在後端是直接用if...else...去處理的 legacy code,我的目標是重構這一段程式碼,希望達到幾個目的

  1. 我希望各種邏輯可以各自獨立維護
  2. 我不喜歡程式複雜度太高
  3. 我希望以後如果下拉選單又多了一個新的類別,可以很容易添加新邏輯

反射與 Attribute

在這之前,當然要先將各種搜尋類型的邏輯,拆分到各自的類別,並且給它們一個共同的抽象介面,後續的操作就都是針對介面來設計

public interface ISearch
{
    List<SearchResult> Search(SearchRequest request);
}

這裡的 Search 就是我們要暴露出去的搜尋 Method,裡面的邏輯直接先取得 Instance 後,再透過約定好的介面 Search 方法來搜尋資料

public List<SearchResult> Search(SearchRequest request, SearchType type)
{
    ISearch instance = SearchFactory.GetInstance(type);
    return instance.Search(request).ToList();
}

GetInstance 原本是一個依據傳入的列舉透過 switch case 的方式取得 Instance,就像這樣

public static IOrderQASearchModule GetInstance(SearchType type)
{
    switch (type)
    {
        case SearchType.Id:
            return new SearchIdModule();
        case SearchType.Name:
            return new SearchNameModule();
        case SearchType.Age:
            return new SearchAgeModule();
    }
}

實際上搜尋的類別長這樣,這邊要稍微說的是,SearchRequest 是所有 Search 方法的傳入參數,實際上要使用,會再 new 自己的 DTO,這裡我想要改善,但暫時沒想到好的法子

public class SearchIdModule : ISearch
{
    private ISearchDAO _searchDAO_;
    protected ISearchDAO SearchDAO
    {
        get => this._searchDAO_ ?? (this._searchDAO_ = Factory.GetSearchDAO());
        set => this._searchDAO_ = value;
    }

    public List<SearchResult> Search(SearchRequest request)
    {
        return SearchDAO.SearchById(new SearchByIdRequest()
        {
            Page = request.Page,
            Limit = request.Limit,
            Id = request.Id,
        }).ToList();
    }
}

資料到了 DAO 之後,接著就是 adapter 去呼叫資料庫預儲程序,每一種 type 都有自己的 sp,這裡不是今天的重點,略過不提

我希望列舉可以直接與instance類別關聯起來,這樣就不需要switch case,而反射可以給予類別產生實體,兩個兜起來就是我要的

所以先弄一個 Attribute

internal class SearchModuleAttribute : Attribute
{
    internal Type SearchModuleType { get; }

    public SearchModuleAttribute(Type searchModuleType)
    {
        SearchModuleType = searchModuleType;
    }
}

Enum 這邊就可以掛上屬性

public enum SearchType
{
    [SearchModule(typeof(SearchIdModule))]
    Id,

    [SearchModule(typeof(SearchNameModule))]
    Name,

    [SearchModule(typeof(SearchAgeModule))]
    Age
}

最後改寫原先的 GetInstance 方法,從 Enum 取得對應的 Attriubte,接著拿到我們設定好的 Type,然後用Activator.CreateInstance(type)去產生 Instance


public static IOrderQASearchModule GetInstance(SearchType searchType)
{
    SearchModuleAttribute valueattribute = GetSearchModuleAttribute(searchType);
    Type type = valueattribute.SearchModuleType;

    IOrderQASearchModule searchModule = (IOrderQASearchModule) Activator.CreateInstance(type);
    return searchModule;
}

internal static SearchModuleAttribute GetSearchModuleAttribute(SearchType searchType)
{
    FieldInfo data = typeof(SearchType).GetField(searchType.ToString());
    Attribute attribute = Attribute.GetCustomAttribute(data, typeof(SearchModuleAttribute));
    SearchModuleAttribute valueattribute = (SearchModuleAttribute) attribute;
    return valueattribute;
}

使用 Dictionary

上一個方法是把類別的關係放在 Enum 上面,實際上動作是有比較繁瑣一點點,新手一點的可能會比較喜歡這個方法,也就是把這個關係,放在我們自己建立的Dictionary裡面

要把這個關係自己獨立一個類別,叫做Resource也可以,或者是要直接放在Factory裡面也可以,就是看自己怎樣比較好理解,好維護,下面這個是一個範例,程式碼不算太難,感受一下就行了

/// <summary>
/// 解決方案列舉
/// </summary>
public enum SolutionType
{
    /// <summary>
    /// 第一種解決方案
    /// </summary>
    First,

    /// <summary>
    /// 第二種解決方案
    /// </summary>
    Second,

    /// <summary>
    /// 第三種解決方案
    /// </summary>
    Third,

    /// <summary>
    /// 未實作的解決方案
    /// </summary>
    NotExist
}

public static class SolutionFactory
    {
        private static Dictionary<SolutionType, Type> _resources;

        private static Dictionary<SolutionType, Type> Resources
        {
            get => _resources ?? GetResources();
            set => _resources = value;
        }

        private static Dictionary<SolutionType, Type> GetResources()
        {
            return new Dictionary<SolutionType, Type>
            {
                [SolutionType.First] = typeof(SolutionOne),
                [SolutionType.Second] = typeof(SolutionTwo),
                [SolutionType.Third] = typeof(SolutionThree)
            };
        }

        private static Type GetInstanceType(SolutionType type)
        {
            if (Resources.ContainsKey(type)) return Resources[type];
            throw new ArgumentException("No Solution");
        }

        /// <summary>
        /// 取得解決方案實體
        /// </summary>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        public static IHammingSolution GetInstance(SolutionType type)
        {
            Type tp = GetInstanceType(type);
            return (IHammingSolution) Activator.CreateInstance(tp);
        }
    }

我覺得實務上我會比較想要用第一種,因為在使用上畢竟比較直覺,但是在程式碼那邊,可能就要多一些理解;但如果對這個還不是很熟悉,那就還是用第二種會比較容易理解