日常開發很常碰到一個情況,就是需要依據傳入的參數,決定 new 不一樣的 instance 出來,所以在數量少的時候,我們可以透過if...else...
的方式直衝,再多一些些,可以用switch...case...
的方式來做,但如果這個分支已經有 5~7 個以上,再接著用上述的兩種做法就有點bad smell
的感覺了
這裡用反射+Attribute
的方式來解這個問題,其實並不算完美的作法,因為相對的best practice
可能每個人的觀念及想法都不同,我只是將我覺得好維護的方法紀錄一下,底下的 Code 都是先 Copy 過來後手打編輯,刪掉很多不能出現的東西,所以不要直接拿去用喔
情境
假設現在的情況是做一個搜尋引擎,前端傳來的部分包含了實際搜尋的 keyword,還有搜尋類型 type,前端透過下拉選單去變更這個搜尋類型,希望後端可以依據不同的搜尋類型,有不一樣的邏輯。
case 大概就是這樣,初版的 Code,在後端是直接用if...else...
去處理的 legacy code,我的目標是重構這一段程式碼,希望達到幾個目的
- 我希望各種邏輯可以各自獨立維護
- 我不喜歡程式複雜度太高
- 我希望以後如果下拉選單又多了一個新的類別,可以很容易添加新邏輯
反射與 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
|
public enum SolutionType { First,
Second,
Third,
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"); }
public static IHammingSolution GetInstance(SolutionType type) { Type tp = GetInstanceType(type); return (IHammingSolution) Activator.CreateInstance(tp); } }
|
我覺得實務上我會比較想要用第一種,因為在使用上畢竟比較直覺,但是在程式碼那邊,可能就要多一些理解;但如果對這個還不是很熟悉,那就還是用第二種會比較容易理解