利用反射取代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

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

1
2
3
4
5
public interface ISearch
{
List<SearchResult> Search(SearchRequest request);
}

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
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,這裡我想要改善,但暫時沒想到好的法子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
internal class SearchModuleAttribute : Attribute
{
internal Type SearchModuleType { get; }

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

Enum 這邊就可以掛上屬性

1
2
3
4
5
6
7
8
9
10
11
12
public enum SearchType
{
[SearchModule(typeof(SearchIdModule))]
Id,

[SearchModule(typeof(SearchNameModule))]
Name,

[SearchModule(typeof(SearchAgeModule))]
Age
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

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裡面也可以,就是看自己怎樣比較好理解,好維護,下面這個是一個範例,程式碼不算太難,感受一下就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/// <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);
}
}

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