0%

【重構系列】(一) 多型

以下實做的SourceCode已經放到GitHub :
https://github.com/toyo0103/Demo_EditTemplate_1

最近在幫專案成員導入開發分層以及物件導向的觀念,也想把這Demo的過程記錄下來。畢竟想法可能隨著專案的經歷而不斷的在修正,也可藉此不斷回頭檢視自己的基本功是否足夠

一、首先來看一下專案分層的關聯圖

[![](https://2.bp.blogspot.com/-2uYHHE-iq7c/V7JxJbl6gCI/AAAAAAAAH3w/I-gYOBfJq8U_fLoVSQTuySsCtNMSb9J6ACLcB/s1600/1.png)](https://2.bp.blogspot.com/-2uYHHE-iq7c/V7JxJbl6gCI/AAAAAAAAH3w/I-gYOBfJq8U_fLoVSQTuySsCtNMSb9J6ACLcB/s1600/1.png)

Application : 應用層,參考了邏輯層

Service :  邏輯層,參考了資料存取層

Repository : 資料存取層

Lib : 共用層 ,被Application、Service、Repository參考

二、目前各層尚未重構的程式

Repository層

[![](https://2.bp.blogspot.com/-tg5bIS8XXLo/V7Kv7donQqI/AAAAAAAAH4A/LMM962SvdT8znywny9YxnzQbnxKS0H-oQCLcB/s1600/1.png)](https://2.bp.blogspot.com/-tg5bIS8XXLo/V7Kv7donQqI/AAAAAAAAH4A/LMM962SvdT8znywny9YxnzQbnxKS0H-oQCLcB/s1600/1.png)

模擬A廠商提供的POI(地圖座標點)資料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/// <summary>
/// POI A供應商
/// </summary>
public class ASupplierRepository
{
public List<POI> Get()
{
Random rnd = new Random();
//隨機建立十筆Supplier為A的資料回傳
var fixture = new Fixture();
var Result = fixture.Build<POI>()
.With(x => x.Name, string.Concat("捷運", rnd.Next(1, 100)))
.With(x => x.Supplier, SupplierEnum.A)
.CreateMany().ToList();
return Result;
}
}


****Service層

[![](https://4.bp.blogspot.com/-Bcv7-vwubuU/V7KxJwqZf9I/AAAAAAAAH4I/9vgY1F6fzbI28ZdK-OCZZmF9bzaM9_FJQCLcB/s1600/1.png)](https://4.bp.blogspot.com/-Bcv7-vwubuU/V7KxJwqZf9I/AAAAAAAAH4I/9vgY1F6fzbI28ZdK-OCZZmF9bzaM9_FJQCLcB/s1600/1.png)
** **** **傳入頻道代碼,依據頻道底下的類別,去各供應商取得POI資料,目前模擬頻道資料如下
[![](https://4.bp.blogspot.com/-e-CVa7nJHGo/V7KxhdAVBQI/AAAAAAAAH4M/4xbGZY9o5JE9Dg6fnyvtWQOmm0wMKcDXgCLcB/s320/1.png)](https://4.bp.blogspot.com/-e-CVa7nJHGo/V7KxhdAVBQI/AAAAAAAAH4M/4xbGZY9o5JE9Dg6fnyvtWQOmm0wMKcDXgCLcB/s1600/1.png)

頻道1底下有一個交通的類別,該類別的POI資料來源為A廠商提供
頻道2底下有一個交通的類別,該類別的POI資料來源為A廠商B廠商提供的總和

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
64
public class POIService
{
public List<POI> Get(string channelID)
{
var Result = new List<POI>();
var channel = GetChannel(channelID);
//逐一搜尋頻道底下的各類別
foreach (var category in channel.Categorys)
{
//依據該類別所指定的供應商向Repository要資料
category.Supplier.ForEach(x =>
{
switch (x)
{
case SupplierEnum.A:
var ASupplier = new ASupplierRepository();
var APOIs = ASupplier.Get();
Result.AddRange(APOIs);
break;
case SupplierEnum.B:
break;
}
});
}
//並接總合的結果回傳
return Result;
}
/// <summary>
/// 模擬頻道類別對應表
/// </summary>
/// <param name="channelID">The channel identifier.</param>
/// <returns>Channel.</returns>
Channel GetChannel(string channelID)
{
if (channelID == "1")
{
return new Channel
{
ID = "1",
Categorys = new List<Channel.Category>
{
new Channel.Category
{
Name = "交通",
Supplier =new List<SupplierEnum> { SupplierEnum.A }
}
}
};
}
return new Channel
{
ID = channelID,
Categorys = new List<Channel.Category>
{
new Channel.Category
{
Name = "交通",
Supplier = new List<SupplierEnum> { SupplierEnum.A , SupplierEnum.B }
}
}
};
}
}

Application層

將傳入的ID透過Service去取得POI資料,並解用Json回傳 ** **
```csharp public ActionResult Index(string id) { var Service = new POIService(); var POIs = Service.Get(id); return Json(POIs,JsonRequestBehavior.AllowGet); }
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


目前執行結果
<div class="separator" style="clear: both; text-align: center;">[![](https://3.bp.blogspot.com/-COJi7ExMLgY/V7KzgT1tBuI/AAAAAAAAH4c/59FwAwpv1U4UniOKgZUJGJMy-jM4AxskgCLcB/s320/1.png)](https://3.bp.blogspot.com/-COJi7ExMLgY/V7KzgT1tBuI/AAAAAAAAH4c/59FwAwpv1U4UniOKgZUJGJMy-jM4AxskgCLcB/s1600/1.png)</div>

### <span style="font-size: x-large;">三、加入B廠商Repository</span>
<div><span style="font-size: x-large;"></span>

### <span style="font-size: x-large;"> **<span style="color: #444444; font-size: large;"><u>Repository層加入BSupplierRepositoy</u></span>** </span>
<span style="font-size: x-large;"> </span></div><div class="separator" style="clear: both; text-align: center;">[![](https://1.bp.blogspot.com/-qhpGz7u6oVg/V7K0fW7yuaI/AAAAAAAAH4k/nGTc-zqfiE4MS4uEtO_gG_mdHZbJkkNsACLcB/s1600/1.png)](https://1.bp.blogspot.com/-qhpGz7u6oVg/V7K0fW7yuaI/AAAAAAAAH4k/nGTc-zqfiE4MS4uEtO_gG_mdHZbJkkNsACLcB/s1600/1.png)</div><div>**<span style="color: #990000;">
</span>** **<span style="color: #990000;">
</span>**</div>```csharp
/// <summary>
/// POI B供應商
/// </summary>
public class BSupplierRepository
{
public List<POI> Get()
{
Random rnd = new Random();
//隨機建立十筆Supplier為B的資料回傳
var fixture = new Fixture();
var Result = fixture.Build<POI>()
.With(x => x.Name, string.Concat("捷運", rnd.Next(1, 100)))
.With(x => x.Supplier, SupplierEnum.B)
.CreateMany().ToList();
return Result;
}
}

Service層

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//逐一搜尋頻道底下的各類別
foreach (var category in channel.Categorys)
{
//依據該類別所指定的供應商向Repository要資料
category.Supplier.ForEach(x =>
{
switch (x)
{
case SupplierEnum.A:
var ASupplier = new ASupplierRepository();
var APOIs = ASupplier.Get();
Result.AddRange(APOIs);
break;
case SupplierEnum.B: //補上B廠商
var BSupplier = new BSupplierRepository();
var BPOIs = BSupplier.Get();
Result.AddRange(BPOIs);
break;
}
});
}

執行結果

[![](https://3.bp.blogspot.com/-oLBfowU5dPE/V7K3te4Ug8I/AAAAAAAAH4w/HWxKp39vbfwkUb4zpQkvoB_umOlCnBk2gCLcB/s1600/1.png)](https://3.bp.blogspot.com/-oLBfowU5dPE/V7K3te4Ug8I/AAAAAAAAH4w/HWxKp39vbfwkUb4zpQkvoB_umOlCnBk2gCLcB/s1600/1.png)
四、重構開始 首先需要思考一個問題,雖然這樣已經可以達成需求,但很明顯的是如果今天提供POI的廠商一直增加的話,我們要一直替**Repository層**新增廠商之外,**Service層**也會一直在Switch那邊做修改。 
[![](https://3.bp.blogspot.com/-At8pfSKZU1A/V7O464uATbI/AAAAAAAAH6Y/_ZteovVvDKgiWIP4C7VOW0d_ID0T3Yn0gCLcB/s640/1.png)](https://3.bp.blogspot.com/-At8pfSKZU1A/V7O464uATbI/AAAAAAAAH6Y/_ZteovVvDKgiWIP4C7VOW0d_ID0T3Yn0gCLcB/s1600/1.png)
[![](https://4.bp.blogspot.com/-_mL7zcPN80s/V7O5MABxbiI/AAAAAAAAH6c/iwdP7xw3pVkJlb08t52ggMFcpuNRywTZQCLcB/s640/1.png)](https://4.bp.blogspot.com/-_mL7zcPN80s/V7O5MABxbiI/AAAAAAAAH6c/iwdP7xw3pVkJlb08t52ggMFcpuNRywTZQCLcB/s1600/1.png)
那該怎麼改可以避免每次改Repository又要一直改Service層呢? 這時就會運用到OO原則中的**依賴抽象**概念,當我們面對的是實體(例如:**ASupplierRepository** , **BSupplierRepository**)時也就是所謂的高耦合,如果面對的實體有所變動時,所有耦合的地方都會有高機率要一起改動的風險。 因為要面對抽象,第一步我們就要來觀察這兩個實體共通處是什麼? 其實說穿了都是提供一個叫做Get()的方法,當呼叫他時,回傳對應的POI List,這就是它們共通的地方,接下來透過抽取介面的方式來實行隔離  ** **** **
[![](https://1.bp.blogspot.com/-8ZNSvrUREs8/V7O5d9RkdPI/AAAAAAAAH6g/l9zIPDnhW2MYl-bCWyutTDSp9UeRtpDigCLcB/s640/1.png)](https://1.bp.blogspot.com/-8ZNSvrUREs8/V7O5d9RkdPI/AAAAAAAAH6g/l9zIPDnhW2MYl-bCWyutTDSp9UeRtpDigCLcB/s1600/1.png)
** ****ISupplierRepository**

1
2
3
4
5
6
7
8
9
10
11
12
/// <summary>
/// Interface ISupplierRepository
/// </summary>
public interface ISupplierRepository
{
/// <summary>
/// 取得POI資料
/// </summary>
/// <returns>List&lt;POI&gt;.</returns>
List<POI> Get();
}

ASupplierRepository , BSupplierRepository都套上介面

[![](https://3.bp.blogspot.com/-Jcpo5G8HHb0/V7K7uHZpu4I/AAAAAAAAH5I/uBTimpbb1_YxzjwVDH3ApMka-KNPy-fBwCLcB/s640/1.png)](https://3.bp.blogspot.com/-Jcpo5G8HHb0/V7K7uHZpu4I/AAAAAAAAH5I/uBTimpbb1_YxzjwVDH3ApMka-KNPy-fBwCLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-e3WLuMtpxh4/V7K7kaOISdI/AAAAAAAAH5E/ZLhqJrVmBcMxApVZcO82qEAbAGXsNTcaACLcB/s640/1.png)](https://2.bp.blogspot.com/-e3WLuMtpxh4/V7K7kaOISdI/AAAAAAAAH5E/ZLhqJrVmBcMxApVZcO82qEAbAGXsNTcaACLcB/s1600/1.png)
將兩個類別都實作了ISupplierRepository介面後,來修改**Service層**,讓它能面對抽象而非直接面對實體

Service層

首先先運用簡單工廠模式,請它來幫我們建造SupplierRepository的實體
[![](https://2.bp.blogspot.com/-sUw1JL2z37U/V7K9OulK-EI/AAAAAAAAH5Y/VeGijYdLK7ElsbQb0Cie22U4PnGOex-BwCLcB/s1600/1.png)](https://2.bp.blogspot.com/-sUw1JL2z37U/V7K9OulK-EI/AAAAAAAAH5Y/VeGijYdLK7ElsbQb0Cie22U4PnGOex-BwCLcB/s1600/1.png)
```csharp public class SupplierFactory { public static ISupplierRepository Generate(SupplierEnum supplierType) { //因為A跟B的SupplierRepository都有實做ISupplierRepository //所以這邊可以直接回傳介面,也間接地規範以後新增SupplierRepository都要實作ISupplierRepository介面 switch (supplierType) { case SupplierEnum.A: return new ASupplierRepository(); case SupplierEnum.B: return new BSupplierRepository(); } return null; } }

```

修改原本的POIService,讓它透過工廠來取得Repository,而不是直接去New實體

[![](https://1.bp.blogspot.com/-yqxvmh12FqM/V7K97eMgJOI/AAAAAAAAH5c/jeYldicOfYYSTvZJeGJRKOkPtJ7OETKXwCLcB/s640/1.png)](https://1.bp.blogspot.com/-yqxvmh12FqM/V7K97eMgJOI/AAAAAAAAH5c/jeYldicOfYYSTvZJeGJRKOkPtJ7OETKXwCLcB/s1600/1.png)

瞬間乾淨很多,而且POIService不再直接對ASupplierRepository , BSupplierRepository,而是面對ISupplierRepository,達成了所謂的面對抽象。

這時候思考一下,以後新增供應商的Repository時POIService都不用改動了,只要Repository層新增好後,將Factory改一下即可,但是否又跳出另一個問題了。這樣Repository層更動時還不是要打開Service層去做修改。 因此在這我會將Factory搬Repository層去封裝起來

SupplierFactory搬到Repository層,並將實體Repository改成Internal封裝起來

[![](https://2.bp.blogspot.com/-WrLZP8-uAWU/V7LAj2T_ieI/AAAAAAAAH5s/uZ2PH9JQGX4X1aOwYavGDKaCHKfXC95MQCLcB/s1600/1.png)](https://2.bp.blogspot.com/-WrLZP8-uAWU/V7LAj2T_ieI/AAAAAAAAH5s/uZ2PH9JQGX4X1aOwYavGDKaCHKfXC95MQCLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-6shnp4lJN7Q/V7LA6VF0OmI/AAAAAAAAH5w/7eVfEwIFG38IGqtWYLxr-QDBdIAHtw1_wCLcB/s400/1.png)](https://2.bp.blogspot.com/-6shnp4lJN7Q/V7LA6VF0OmI/AAAAAAAAH5w/7eVfEwIFG38IGqtWYLxr-QDBdIAHtw1_wCLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-OMNNg00W0qE/V7LBG6V83fI/AAAAAAAAH50/1stqCDyKtRMLmxt-QBvl3vXZ9khxZOFpACLcB/s400/1.png)](https://2.bp.blogspot.com/-OMNNg00W0qE/V7LBG6V83fI/AAAAAAAAH50/1stqCDyKtRMLmxt-QBvl3vXZ9khxZOFpACLcB/s1600/1.png)
換句話說,因為改成Internal的關係,現在Service層也碰不到**ASupplierRepository** , **B****SupplierRepository**這兩個實體了,完全只能依賴Factory來取得介面的對應實體,未來不管怎麼新增Repository都不會再改**Service層**,到這邊就是重構並且實踐多型了
目前UML結構圖如下
[![](https://1.bp.blogspot.com/-fUMrd2aOxZs/V7LGMRcCTpI/AAAAAAAAH6I/lgwJmCO6LYse8DXoFEJKw3NqwtL7Z7f7QCLcB/s1600/1.png)](https://1.bp.blogspot.com/-fUMrd2aOxZs/V7LGMRcCTpI/AAAAAAAAH6I/lgwJmCO6LYse8DXoFEJKw3NqwtL7Z7f7QCLcB/s1600/1.png)