0%

上一篇【Unit Test】Fake System.DateTime 寫到如何Fake DLL,在本機建置與進行測試都沒問題,唯獨放到CICD機器去做自動化部屬時,一直鬼擋牆的跳建置方案錯誤,錯誤內容如下

The type or namespace name ‘EventSourceCreatedEventArgs’ does not exist in the namespace ‘System.Diagnostics.Tracing’ (are you missing an assembly reference?) 

上網查了一下找到以下解法

  • 打開mscorlib.fakes,改成以下內容```csharp

```

自動化佈署機器這樣去建置佈署就過關了!!!!

在寫程式的時後,常常會用到DateTime.Now來判斷目前時間,依照時間邏輯去撈取不同的資料,但碰到單元測試要驗證的時候就是個大麻煩,因為DateTime.Now每次執行的時候時間都會改變,所以以前的做法都是這樣

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static class SystemTime
{
//Internal For UnitTest
internal static Func<DateTime> SetCurrentTime = () => DateTime.Now;

public static DateTime Now
{
get
{
return SetCurrentTime();
}
}
}

然後在在程式碼裡面使用的時候不直接使用DateTime.Now,改用SystemTime.Now

1
2
3
4
5
6
7
8
9
10
11
12
void Main()
{
if (SystemTime.Now == new DateTime(2016,10,17,12,0,0))
{
"時間到了".Dump();
}
else
{
"時間不對".Dump();
}
}

之後單元測試驗證時,動態去改寫SetCurrentTime,就能確保拿到自己要的時間

1
2
SystemTime.SetCurrentTime = () => new DateTime(2016,10,17,12,0,0);

雖然這樣可以解決問題沒錯,但實在太麻煩,為了單元測試要多寫一堆Code之外,System底下很多東西有用到時,都要因為可以單元測試的關係而擴充出來。

還好之後有找到方法可以針對DLL做Fake,讓我們可以繼續安心使用DateTime.Now之餘,單元測試也能指定時間,接下來就來實作一下這個步驟

  • 首先在單元測試的專案對參考的組件System按下右鍵,新增Fake物件

    [![](https://4.bp.blogspot.com/-2CmHnA1rUMA/WARsswfjOPI/AAAAAAAAIBI/HGzYnnuIOzM6DuGEl8psJIIA4EtBmutgQCLcB/s1600/1.png)](https://4.bp.blogspot.com/-2CmHnA1rUMA/WARsswfjOPI/AAAAAAAAIBI/HGzYnnuIOzM6DuGEl8psJIIA4EtBmutgQCLcB/s1600/1.png)
  • 接著就會跑出Fake資料夾

    [![](https://3.bp.blogspot.com/-QhvjMs19Dr4/WARtB33gWvI/AAAAAAAAIBM/ptqbpm0CKTgbi5C8CWZY3DjWbmykiJa4gCLcB/s1600/1.png)](https://3.bp.blogspot.com/-QhvjMs19Dr4/WARtB33gWvI/AAAAAAAAIBM/ptqbpm0CKTgbi5C8CWZY3DjWbmykiJa4gCLcB/s1600/1.png)
  • 接著只要在單元測試的地方寫如此寫,就能設定DateTime.Now應該回傳的時間了```csharp
    [TestMethod]

      public void 取得現在時間()
      {
          using (ShimsContext.Create())
          {
              System.Fakes.ShimDateTime.NowGet= () =>
              {
                  return new DateTime(2016, 9, 25);
              };
    
               //arrange
              var expected = new DateTime(2016, 9, 25);
    
              //act
              var actual = DateTime.Now;
    
              //assert
              Assert.AreEqual(expected, actual);
          }
      }
    

```

補充2017/01/24

單元測試Fake的功能,以Visual Studio 2015來說只有Enterprise版本才有支援,所以使用的時候請特別小心,像今天公司因為授權費的關係,要求調降成Professional,之前有用到Fake的地方就都測不過了,還請特別注意

各版本比較 : Compare Visual Studio 2015 Offerings

參考文章

[Huan-Lin - Microsoft Fakes 入門](http://huan-lin.blogspot.com/2012/10/microsoft-fakes.html)

上一篇【Swagger】活著的API規格書 提到如何使用Swagger來產生規格與測試API,但遇到一個問題是,很多API會把驗證的Key放到Header傳遞,但Swagger產出來的頁面並沒有設定Header的地方,這時候就要來小調整一下已符合需求。

首先我們先把原本的API改成要吃Header的Key值才算驗證通過

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
public class TestSwaggerController : ApiController
{
/// <summary>
/// 測試Swagger的API
/// </summary>
/// <param name="parameter">The parameter.</param>
/// <returns></returns>
[HttpGet]
[Route("api/testSwagger")]
public HttpResponseMessage Get([FromUri]testSwaggerGetParameter parameter)
{

//沒有帶Appkey在Header
if (!Request.Headers.Contains("X-Key"))
{
return Request.CreateResponse(HttpStatusCode.NotAcceptable, "必須輸入AppKey");

}

var AppKey = Request.Headers.GetValues("X-Key").FirstOrDefault();
if (AppKey != "MyKey")
{
return Request.CreateResponse(HttpStatusCode.NotAcceptable, "AppKey不正確!!!");
}

if (parameter != null &&
parameter.ID == "toyo" &&
parameter.PassWord == "123456")
{
return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼正確");
}

return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼錯誤摟!!!!!");
}
}

這時候測試一下原本的API,會發現因為吃不到Header裡面的X-Key,導致輸出驗證失敗的錯誤

[![](https://1.bp.blogspot.com/-hntf0ZIC-cQ/WAB1wx9JTOI/AAAAAAAAIAU/R5nql0g5UZwtIU6pdfk82zPO7EjDp4FggCLcB/s640/1.png)](https://1.bp.blogspot.com/-hntf0ZIC-cQ/WAB1wx9JTOI/AAAAAAAAIAU/R5nql0g5UZwtIU6pdfk82zPO7EjDp4FggCLcB/s1600/1.png)

Header中也再次確認沒有帶X-key在其中

[![](https://1.bp.blogspot.com/-pevW7o5r1eg/WAB2BE9-ayI/AAAAAAAAIAY/ErtqWFRqK7k0WBAKQXBZB32ghXOn3196gCLcB/s640/1.png)](https://1.bp.blogspot.com/-pevW7o5r1eg/WAB2BE9-ayI/AAAAAAAAIAY/ErtqWFRqK7k0WBAKQXBZB32ghXOn3196gCLcB/s1600/1.png)

開始客製化吧

  • 首先先建立一個JS檔,讓Swagger的View能引用這支JS,然後透過這支JS與Jquery去改View,我將這支JS就命名為AppKey.js

    [![](https://1.bp.blogspot.com/-VdaLRHRtOlI/WAB3LAbaafI/AAAAAAAAIAc/tDB1aQ-EzzEutJ0RXZkrr4TRwvcWwFUnwCLcB/s1600/1.png)](https://1.bp.blogspot.com/-VdaLRHRtOlI/WAB3LAbaafI/AAAAAAAAIAc/tDB1aQ-EzzEutJ0RXZkrr4TRwvcWwFUnwCLcB/s1600/1.png)
  • 接著請在這支JS檔案寫下以下JS

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
$(function () {
$('#input_apiKey').hide();

//加上Header區塊
var HeaderBar = $('<div id="headerbar">' +
'<h2 class="heading">Header</h2>' +
'<table style="background-color:#E8E8D0"><thead><tr>' +
'<th style="width: 100px; max-width: 100px" data-sw-translate="">Parameter</th>' +
'<th style="width: 310px; max-width: 310px" data-sw-translate="">Value</th>' +
'<th style="width: 200px; max-width: 200px" data-sw-translate="">Description</th>' +
'</tr></thead>' +
'<tbody class="operation-params">' +
'<tr>' +
'<td class="code required"><label for="custom_appkey">appkey</label></td>' +
'<td><input class="parameter required" minlength="1" name="custom_appkey" placeholder="(required)" id="custom_appkey" type="text" value=""></td>' +
'<td class="markdown"><p>AppKey</p></td>' +
'</tr>' +
'</tbody></table>' +
'</div>');
$('#resources_container').before(HeaderBar);

//把值加到Header
$('#custom_appkey').on('change', function () {
var key = this.value;
if (key && key.trim() !== '') {
swaggerUi.api.clientAuthorizations.add("key", new SwaggerClient.ApiKeyAuthorization("X-Key", key, "header"));
}
});

});

主要內容就是抓到特定區塊,然後組Html用Jquery的方式放上去,然後透過Swagger開放的API塞到Header裡面送出

  • 接著請將這支JS修改屬性 > 建置動作 > 內嵌資源
[![](https://4.bp.blogspot.com/-BwLvO5AMcYw/WAB4kuDTvUI/AAAAAAAAIAg/QBIkgR01zCYDIxd2UjHmhHLanbuwCchqQCLcB/s320/1.png)](https://4.bp.blogspot.com/-BwLvO5AMcYw/WAB4kuDTvUI/AAAAAAAAIAg/QBIkgR01zCYDIxd2UjHmhHLanbuwCchqQCLcB/s1600/1.png)
  • 打開SwaggerConfig找到188~189行的地方把註解拿掉改成如下
    [![](https://2.bp.blogspot.com/-kG8umCQL1M0/WAB5FrxW2gI/AAAAAAAAIAo/RwoPyjGGq0YB1SE6f9jTvJ3h9mcveFFlQCLcB/s640/1.png)](https://2.bp.blogspot.com/-kG8umCQL1M0/WAB5FrxW2gI/AAAAAAAAIAo/RwoPyjGGq0YB1SE6f9jTvJ3h9mcveFFlQCLcB/s1600/1.png)
    1
    2
    c.InjectJavaScript(thisAssembly, "WebApplication1.CustomSwagger.AppKey.js");

  • 接著再次執行專案
    [![](https://2.bp.blogspot.com/-jAdPbsrj5yI/WAB5_JNiKdI/AAAAAAAAIAs/7DMJc1iTBl4kAm6_7yTv2f4pmNCfqFEQQCLcB/s640/1.png)](https://2.bp.blogspot.com/-jAdPbsrj5yI/WAB5_JNiKdI/AAAAAAAAIAs/7DMJc1iTBl4kAm6_7yTv2f4pmNCfqFEQQCLcB/s1600/1.png)
    Header欄位出現了
    試試看欄位是否有效,因為剛剛程式是改成要帶**MyKey**,所以來測試看看
[![](https://1.bp.blogspot.com/-v7nxyvIuGfs/WAB6YhN2EEI/AAAAAAAAIAw/luDPe_40Fpc-xvazlnkwp6qEPRdUbKMvgCLcB/s640/1.png)](https://1.bp.blogspot.com/-v7nxyvIuGfs/WAB6YhN2EEI/AAAAAAAAIAw/luDPe_40Fpc-xvazlnkwp6qEPRdUbKMvgCLcB/s1600/1.png)
可以抓到X-Key了

完成!!!! 因為是透過Jquery的方式去改變欄位,所以只要透過上述的方式去載入JS,要怎麼改View應該都不是問題了,以上。

參考文章

Customize Authentication Header in SwaggerUI using Swashbuckle

所謂的工程師就是,【接手別人專案時,總是問怎麼沒有規格書? 自己開發專案時卻又不喜歡寫規格書】的一群人,本人也是一個極度不喜歡寫規格書的人,簡單說就是懶到極致,懶到深處無怨尤。而且常常改版時來匆匆忙忙,寫程式的時間都不夠了,誰還管你規格書有沒有更新,就這樣恍神個兩三次忘記回去更新規格,這本規格書就光榮列入公司十大(搞不好百大?)不可思議天書,可謂極度麻煩費時討厭…..

還好同事介紹了Swagger的用法,讓你邊開發程式時,規格就產生書產生出來了,而且還是本可以使用的規格書,讓你從今以後再也不用擔心規格書與實際規格脫鉤的問題,以下就筆記一下如何使用

[![](https://4.bp.blogspot.com/-DHN0U1Vqe3U/WAA8GQA474I/AAAAAAAAH-o/idcyU1UFoPoM9sI2fs7Jga7vTRrA_Lz7gCLcB/s640/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png)](https://4.bp.blogspot.com/-DHN0U1Vqe3U/WAA8GQA474I/AAAAAAAAH-o/idcyU1UFoPoM9sI2fs7Jga7vTRrA_Lz7gCLcB/s1600/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png)
Swagger-當開發完API時,規格書也就完成了!!
[![](https://4.bp.blogspot.com/-Q_gy5BypWEQ/WAA8wzGS32I/AAAAAAAAH-s/dbAyDeDRpPgfEc-OjH9_s2GIaafUpCBjACLcB/s640/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png)](https://4.bp.blogspot.com/-Q_gy5BypWEQ/WAA8wzGS32I/AAAAAAAAH-s/dbAyDeDRpPgfEc-OjH9_s2GIaafUpCBjACLcB/s1600/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png)
輸入的參數與說明也寫得清清楚楚
[![](https://1.bp.blogspot.com/-WbKmvP1AzgE/WAA-rbXKmUI/AAAAAAAAH-8/-tLhejDldqI_HmO2FNDS1gO6ImyOrrfngCLcB/s640/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png)](https://1.bp.blogspot.com/-WbKmvP1AzgE/WAA-rbXKmUI/AAAAAAAAH-8/-tLhejDldqI_HmO2FNDS1gO6ImyOrrfngCLcB/s1600/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png)
按下Try it Out後,可以馬上測試API跟觀看結果!!

使用步驟

  • 首先我先開一個WebAPI專案,然後寫一支簡單的API讓他可以運作
開一個WebAPI專案
[![](https://3.bp.blogspot.com/-VmbQGY5UMMU/WABBEd25TPI/AAAAAAAAH_I/MGng_1nDDSEQ-zLiXyahRK5GTT9XMtSEwCLcB/s640/2.png)](https://3.bp.blogspot.com/-VmbQGY5UMMU/WABBEd25TPI/AAAAAAAAH_I/MGng_1nDDSEQ-zLiXyahRK5GTT9XMtSEwCLcB/s1600/2.png)
新增一個新的TestSwaggerController
[![](https://1.bp.blogspot.com/-AENbjbb3Ia4/WABBERDf0cI/AAAAAAAAH_M/Ea_va3RXz0kCcplepPRx-nBqaHHpLLsCwCLcB/s1600/3.png)](https://1.bp.blogspot.com/-AENbjbb3Ia4/WABBERDf0cI/AAAAAAAAH_M/Ea_va3RXz0kCcplepPRx-nBqaHHpLLsCwCLcB/s1600/3.png)

  • 開始撰寫幾個簡單的API跟輸入輸出參數

```csharp public class testSwaggerGetParameter { /// /// 帳號 /// /// /// The identifier. /// public string ID { get; set; }
  /// <summary>
  /// 密碼
  /// </summary>
  /// <value>
  /// The passWord.
  /// </value>
  public string PassWord { get; set; }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

```csharp
public class TestSwaggerController : ApiController
{
/// <summary>
/// 測試Swagger的API
/// </summary>
/// <param name="parameter">The parameter.</param>
/// <returns></returns>
[HttpGet]
[Route("api/testSwagger")]
public HttpResponseMessage Get([FromUri]testSwaggerGetParameter parameter)
{
if (parameter != null &&
parameter.ID == "toyo" &&
parameter.PassWord == "123456")
{
return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼正確");
}

return Request.CreateResponse(HttpStatusCode.OK, "帳號密碼錯誤摟!!!!!");
}
}

  • 接著先來測試API的運作是否正常
    [![](https://3.bp.blogspot.com/-NGEX0U-saSw/WABFAd9ksPI/AAAAAAAAH_c/AQuEDjoIGL4jbBYZs2PJ7B7lnAS7fB8xQCLcB/s640/2.png)](https://3.bp.blogspot.com/-NGEX0U-saSw/WABFAd9ksPI/AAAAAAAAH_c/AQuEDjoIGL4jbBYZs2PJ7B7lnAS7fB8xQCLcB/s1600/2.png)
    輸入正確的測試
    [![](https://1.bp.blogspot.com/-i923xx2aY30/WABFABbQrdI/AAAAAAAAH_Y/cvhLOl1x6xw3aJ5Yvd-T7gBcePHTb8GCwCLcB/s640/1.png)](https://1.bp.blogspot.com/-i923xx2aY30/WABFABbQrdI/AAAAAAAAH_Y/cvhLOl1x6xw3aJ5Yvd-T7gBcePHTb8GCwCLcB/s1600/1.png)
    輸入錯誤的測試

  • API都準備就緒後,來安裝Swagger吧
    首先打開Nuget搜尋Swashbuckle並安裝
    [![](https://1.bp.blogspot.com/-I6T6On4UU0I/WABGKKbp_4I/AAAAAAAAH_g/thsWbtJB9d8M-wBG3oHoBHct0d6N-7ohwCLcB/s640/1.png)](https://1.bp.blogspot.com/-I6T6On4UU0I/WABGKKbp_4I/AAAAAAAAH_g/thsWbtJB9d8M-wBG3oHoBHct0d6N-7ohwCLcB/s1600/1.png)

接著打開專案屬性,設定輸出XML說明格式

[![](https://3.bp.blogspot.com/-Q35LXg-ZAbc/WABHQqOA9II/AAAAAAAAH_o/IqpSnKPL7AIkGskPH8LPMKDBYKS-cJvNACLcB/s640/1.png)](https://3.bp.blogspot.com/-Q35LXg-ZAbc/WABHQqOA9II/AAAAAAAAH_o/IqpSnKPL7AIkGskPH8LPMKDBYKS-cJvNACLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-kTvzjXCAY7Q/WABHWnjk79I/AAAAAAAAH_s/WYApFDnj0RACcVPP3KQC9qGsgdxf3a3swCLcB/s640/2.png)](https://2.bp.blogspot.com/-kTvzjXCAY7Q/WABHWnjk79I/AAAAAAAAH_s/WYApFDnj0RACcVPP3KQC9qGsgdxf3a3swCLcB/s1600/2.png)

打開SwaggerConfig做些設定

[![](https://1.bp.blogspot.com/-3KPOfrFBs7A/WABHvcNMO4I/AAAAAAAAH_w/-tsOz_SqAGcvVxxSOOwok_Q1yTpZyDAiQCLcB/s1600/1.png)](https://1.bp.blogspot.com/-3KPOfrFBs7A/WABHvcNMO4I/AAAAAAAAH_w/-tsOz_SqAGcvVxxSOOwok_Q1yTpZyDAiQCLcB/s1600/1.png)

在第一百行的地方把註解拿掉

[![](https://2.bp.blogspot.com/-sNuzZJH1_Tk/WABH-ckCYAI/AAAAAAAAH_0/dY91muQCTvAggbaSNAxXFuf687P37-f4wCLcB/s640/1.png)](https://2.bp.blogspot.com/-sNuzZJH1_Tk/WABH-ckCYAI/AAAAAAAAH_0/dY91muQCTvAggbaSNAxXFuf687P37-f4wCLcB/s1600/1.png)

在最下面補上以下程式碼

[![](https://2.bp.blogspot.com/-LZWGCrO2m1E/WABIPqVRXuI/AAAAAAAAH_4/XDwqWQFF4zIfyBMKgjUfNqisGADqPMPvQCLcB/s640/1.png)](https://2.bp.blogspot.com/-LZWGCrO2m1E/WABIPqVRXuI/AAAAAAAAH_4/XDwqWQFF4zIfyBMKgjUfNqisGADqPMPvQCLcB/s1600/1.png)

  • 重新執行WebAPI,並且在網址列輸入……./Swagger
    [![](https://2.bp.blogspot.com/-VKr_8qKHN7Y/WABIxRpt8gI/AAAAAAAAH_8/XaVeA3uG-9whtv0DrZ7DQYIzKXjaHryYgCLcB/s640/1.png)](https://2.bp.blogspot.com/-VKr_8qKHN7Y/WABIxRpt8gI/AAAAAAAAH_8/XaVeA3uG-9whtv0DrZ7DQYIzKXjaHryYgCLcB/s1600/1.png)
    看到API被輸出成規格了,參數的註解是跟著Summary的
    來測試看看!!!
    [![](https://1.bp.blogspot.com/-651X7h-rIVw/WABJN-3Jf-I/AAAAAAAAIAA/iBthsKVDiscN_ItWjk4WUUSgemSLEfDLQCLcB/s640/1.png)](https://1.bp.blogspot.com/-651X7h-rIVw/WABJN-3Jf-I/AAAAAAAAIAA/iBthsKVDiscN_ItWjk4WUUSgemSLEfDLQCLcB/s1600/1.png)
    [![](https://3.bp.blogspot.com/-JPjRZMJusSE/WABJOPutBbI/AAAAAAAAIAE/aFON9GjhDe0HKHY_jjJ8M8JfstfCDLoLgCLcB/s640/2.png)](https://3.bp.blogspot.com/-JPjRZMJusSE/WABJOPutBbI/AAAAAAAAIAE/aFON9GjhDe0HKHY_jjJ8M8JfstfCDLoLgCLcB/s1600/2.png)
    最後,Swagger雖然已經非常好用了,也能符合大部分的情境運用,但還是有些美中不足的地方,例如如果今天API部分資訊要帶在Header裡面傳給API,在預設產生的地方是沒有提供可以輸入的地方。
    但慶幸的是Swagger可以很彈性的去改寫這張最後會產生的View,可以自己擴充需要的欄位,下一篇在來介紹如何客製化Swagger頁面!!!

參考文章

FluentValidation是個很不錯的套件,且擴充性也高,解決了一些以前常常要寫很多遍的驗證邏輯。

以前常常驗證參數時都會有很多的If..Else,搞得程式碼很長很醜之外,閱讀性不佳。自從公司同事推薦了這個套件後,用了兩三個專案發現程式碼變得簡潔易懂之外,寫了一些擴充方法也可以重複使用,不像以前常常重複造輪子

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
void Main()
{
var Parameter = new APIInputParameter
{
ID = "123",
Name = "Toyo"
};

Guid _ID;
//驗證邏輯
if (string.IsNullOrWhiteSpace(Parameter.ID) ||
!Guid.TryParse(Parameter.ID,out _ID) ||
string.IsNullOrWhiteSpace(Parameter.Name) )
{
"參數錯誤".Dump();
}
else
{
"參數正確".Dump();
}

}

public class APIInputParameter
{
//此參數應該為Guid,但為了能Log下來所以接的時候要先接成String
//否則輸入端不是傳入GUID就記錄不到了
public string ID { get; set; }
public string Name {get;set;}
}

上面的範例是以前的寫法,常常欄位多,各個欄位又有不同的要求時,總是把驗證的邏輯寫得又臭又長。

加上公司要求所有輸入輸出的欄位都要被Log下來,所以基本上參數都要是String型別,否則如果ID寫成GUID,因為傳入的時候不是GUID會接不到,自然無法被Log到,但也因此衍生了驗證的複雜度提高的問題。

想想如果各個參數錯誤要回傳的訊息會不同時,又該寫的多複雜才做得到呢……

那讓來看看如何透FluentValidation 驗證參數,

  1. 首先先把驗證邏輯寫成一個Class
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class APIInputParameterValidator : AbstractValidator<APIInputParameter>{
    public APIInputParameterValidator()
    {
    //ID - 必填,應為GUID
    this.RuleFor(x => x.ID)
    .NotEmpty()
    .WithErrorCode("X400")
    .WithMessage("ID不得為空字串")
    .NotNull()
    .WithErrorCode("X400")
    .WithMessage("ID不得為Null");

    this.RuleFor(x => x.Name)
    .NotEmpty()
    .WithErrorCode("X401")
    .WithMessage("Name不得為空字串")
    .NotNull()
    .WithErrorCode("X401")
    .WithMessage("Name不得為Null");
    }
    }

  1. 自己寫一個HasError的Extension```csharp
    ///
    /// FluentValidation 自訂驗證擴充方法.
    ///

    public static class FluentValidationExtensions
    {
    ///
    /// 驗證結果是否有 Error.
    ///

    ///
    ///
    public static bool HasError(this ValidationFailure validationFailure)
    {
    return validationFailure != null &&
    !string.IsNullOrWhiteSpace(validationFailure.ErrorMessage);
    }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20


3. 驗證的地方改成如下 ```csharp
var Parameter = new APIInputParameter {
ID = "123",
Name = "Toyo"
};
// 檢查輸入參數
var validator = new APIInputParameterValidator();

var error = validator.Validate(Parameter).Errors.FirstOrDefault();
if (error.HasError())
{
string.Format("{0}-{1}",error.ErrorCode,error.ErrorMessage).Dump();
}
else
{
"驗證成功".Dump();
}

這樣只要有參數帶入錯誤,他就會依照你要求的帶回ErrorCode跟ErrorMessage,那可能各位會發現,阿驗證是否為GUID的地方怎麼不見了?? 因為套件並沒有提供,所以這邊要自己擴充

  1. 先寫一個驗證String是否為Guid的方法 ```csharp
    ///


    /// 驗證是否為GUID
    ///

    ///
    public class GUIDValidator : PropertyValidator
    {

     /// <summary>
     /// 是否允許字串參數為空白.
     /// </summary>
     /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
     private bool AllowEmpty { get; set; }
    
         /// <summary>
     /// Initializes a new instance of the <see cref="GUIDValidator"/> class.
     /// </summary>
     /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
     public GUIDValidator(
         bool allowEmpty = false) : base("傳入參數錯誤。")
     {
         this.AllowEmpty = allowEmpty;
     }
    
         /// <summary>
     /// Returns true if ... is valid.
     /// </summary>
     /// <param name="context">The context.</param>
     /// <returns>
     ///   <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
     /// </returns>
     protected override bool IsValid(PropertyValidatorContext context)
     {
         var propertyValue = context.PropertyValue as string;
    
             if (AllowEmpty &&
             string.IsNullOrWhiteSpace(propertyValue))
         {
             return true;
         }
    
             Guid guid;
         return Guid.TryParse(propertyValue, out guid);
     }
    

    }

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


2. 接著在Extension的地方補上兩個擴充方法,分別是【應該是GUID】、【應該是GUID但允許其為空字串或Null】 ```csharp
/// <summary>
/// FluentValidation 自訂驗證擴充方法.
/// </summary>
public static class FluentValidationExtensions
{
/// <summary>
/// 驗證結果是否有 Error.
/// </summary>
/// <param name="validationFailure"></param>
/// <returns></returns>
public static bool HasError(this ValidationFailure validationFailure)
{
return validationFailure != null &&
!string.IsNullOrWhiteSpace(validationFailure.ErrorMessage);
}

/// <summary>
/// 應該是 GUID 型別.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TProperty">The type of the t property.</typeparam>
/// <param name="ruleBuilder">The rule builder.</param>
/// <returns>IRuleBuilderOptions<T, TProperty>.</returns>
public static IRuleBuilderOptions<T, TProperty> IsGUID<T, TProperty>(
this IRuleBuilder<T, TProperty> ruleBuilder)
{
return ruleBuilder.SetValidator(new GUIDValidator(allowEmpty: false));
}

/// <summary>
/// 應該是 GUID 型別, 但允許 String.Empty.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="TProperty">The type of the t property.</typeparam>
/// <param name="ruleBuilder">The rule builder.</param>
/// <returns>IRuleBuilderOptions<T, TProperty>.</returns>
public static IRuleBuilderOptions<T, TProperty> IsGUIDAllowEmpty<T, TProperty>(
this IRuleBuilder<T, TProperty> ruleBuilder)
{
return ruleBuilder.SetValidator(new GUIDValidator(allowEmpty: true));
}
}

  1. 接著在原本驗證的地方補上
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    //ID - 必填,應為GUID
    this.RuleFor(x => x.ID)
    .NotEmpty()
    .WithErrorCode("X400")
    .WithMessage("ID不得為空字串")
    .NotNull()
    .WithErrorCode("X400")
    .WithMessage("ID不得為Null")
    .IsGUID()
    .WithErrorCode("X400")
    .WithMessage("ID應為GUID");

再執行原本的驗證就會得到錯誤訊息 X400-ID應為GUID
對我來說不止讓程式可讀性增加之外,也讓驗證的地方被分離出來,做到所謂的關注點分離

以下補上幾個我常常用到的驗證擴充出來的方法供各位參考,再強調一次,因為公司要求輸入輸出都要被Log下來,所以所有參數都是從String出發去驗證

  • 驗證是否為DateTime or TimeStamp

    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
    /// <summary>
    /// 驗證是否為DateTime
    /// </summary>
    /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
    public class DateTimeValidator : PropertyValidator
    {
    /// <summary>
    /// 是否允許參數為空白.
    /// </summary>
    /// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
    private bool AllowEmpty { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="DateTimeValidator"/> class.
    /// </summary>
    /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
    public DateTimeValidator(bool allowEmpty) : base("型別錯誤")
    {
    this.AllowEmpty = allowEmpty;
    }

    /// <summary>
    /// Returns true if ... is valid.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <returns>
    /// <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
    /// </returns>
    protected override bool IsValid(PropertyValidatorContext context)
    {
    var propertyValue = context.PropertyValue as string;

    if (this.AllowEmpty &&
    string.IsNullOrWhiteSpace(propertyValue))
    {
    return true;
    }

    int value;
    bool result = int.TryParse(propertyValue, out value);
    //TimeStamp
    if (result && value > 0)
    {
    return true;
    }

    DateTime dateTimeValue;
    return DateTime.TryParse(propertyValue, out dateTimeValue);
    }
    }

擴充方法```csharp
///


/// 是 DateTime 型別 or TimeStamp .
///

///
/// The type of the t property.
/// The rule builder.
/// IRuleBuilderOptions<T, TProperty>.
public static IRuleBuilderOptions<T, TProperty> IsDateTimeOrTimeStamp<T, TProperty>(
this IRuleBuilder<T, TProperty> ruleBuilder)
{
return ruleBuilder.SetValidator(new DateTimeValidator(allowEmpty: false));
}

        /// <summary>
    /// 是 DateTime 型別 or TimeStamp, 但允許 String.Empty.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the t property.</typeparam>
    /// <param name="ruleBuilder">The rule builder.</param>
    /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
    public static IRuleBuilderOptions<T, TProperty> IsDateTimeOrTimeStampAllowEmpty<T, TProperty>(
        this IRuleBuilder<T, TProperty> ruleBuilder)
    {
        return ruleBuilder.SetValidator(new DateTimeValidator(allowEmpty: true));
    }
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


* **<span style="color: #073763;">驗證是否為GUID Array </span>**

```csharp
/// <summary>
/// 驗證是否為GUID Array
/// </summary>
public class GUIDArrayValidator : PropertyValidator
{
/// <summary>
/// 是否允許字串參數為空白.
/// </summary>
/// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
private bool AllowEmpty { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="GUIDArrayValidator"/> class.
/// </summary>
/// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
public GUIDArrayValidator(
bool allowEmpty = false) :base("傳入參數錯誤。")
{
this.AllowEmpty = allowEmpty;
}

/// <summary>
/// Returns true if ... is valid.
/// </summary>
/// <param name="context">The context.</param>
/// <returns>
/// <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
/// </returns>
protected override bool IsValid(PropertyValidatorContext context)
{
var propertyValue = context.PropertyValue as List<string>;
if (AllowEmpty &&
(propertyValue == null || propertyValue.Count == 0))
{
return true;
}

if (!AllowEmpty &&
(propertyValue == null || propertyValue.Count == 0))
{
return false;
}

Guid guid;
foreach (var item in propertyValue)
{
if (!Guid.TryParse(item, out guid))
{
return false;
}
}

return true;
}
}

擴充方法```csharp
///


/// 是 Guid Array, 但允許空集合.
///

///
/// The type of the t property.
/// The rule builder.
/// IRuleBuilderOptions<T, TProperty>.
public static IRuleBuilderOptions<T, TProperty> IsGUIDArrayAllowEmpty<T, TProperty>(
this IRuleBuilder<T, TProperty> ruleBuilder)
{
return ruleBuilder.SetValidator(new GUIDArrayValidator(allowEmpty: true));
}

        /// <summary>
    /// 是 Guid Array.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the t property.</typeparam>
    /// <param name="ruleBuilder">The rule builder.</param>
    /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
    public static IRuleBuilderOptions<T, TProperty> IsGUIDArray<T, TProperty>(
        this IRuleBuilder<T, TProperty> ruleBuilder)
    {
        return ruleBuilder.SetValidator(new GUIDArrayValidator(allowEmpty: false));
    }
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


* **<span style="color: #073763;">驗證是否為數字 </span>**

```csharp
/// <summary>
/// 驗證是否為Integer
/// </summary>
/// <seealso cref="FluentValidation.Validators.PropertyValidator" />
public class IntegerValidator : PropertyValidator
{
/// <summary>
/// 是否允許字串參數為空白.
/// </summary>
/// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
private bool AllowEmpty { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="IntegerValidator"/> class.
/// </summary>
/// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
public IntegerValidator(bool allowEmpty = false)
: base("型別錯誤")
{
this.AllowEmpty = allowEmpty;
}

/// <summary>
/// Returns true if ... is valid.
/// </summary>
/// <param name="context">The context.</param>
/// <returns>
/// <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
/// </returns>
/// <exception cref="NotImplementedException"></exception>
protected override bool IsValid(PropertyValidatorContext context)
{
var propertyValue = context.PropertyValue as string;

if (this.AllowEmpty &&
string.IsNullOrWhiteSpace(propertyValue))
{
return true;
}

int value;
bool result = int.TryParse(propertyValue, out value);
return result;
}
}

擴充方法```csharp
///


/// 是 Integer 型別.
///

///
/// The type of the t property.
/// The rule builder.
/// IRuleBuilderOptions<T, TProperty>.
public static IRuleBuilderOptions<T, TProperty> IsInteger<T, TProperty>(
this IRuleBuilder<T, TProperty> ruleBuilder)
{
return ruleBuilder.SetValidator(new IntegerValidator(allowEmpty: false));
}

        /// <summary>
    /// 是 Integer 型別, 但允許 String.Empty.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the t property.</typeparam>
    /// <param name="ruleBuilder">The rule builder.</param>
    /// <returns>IRuleBuilderOptions&lt;T, TProperty&gt;.</returns>
    public static IRuleBuilderOptions<T, TProperty> IsIntegerAllowEmpty<T, TProperty>(
        this IRuleBuilder<T, TProperty> ruleBuilder)
    {
        return ruleBuilder.SetValidator(new IntegerValidator(allowEmpty: true));
    }
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78


* **<span style="color: #073763;">檢查數字是否在要求範圍內 </span>**

```csharp
/// <summary>
/// 驗證數字是否在範圍內
/// </summary>
/// <typeparam name="TNumeric">The type of the numeric.</typeparam>
/// <seealso cref="FluentValidation.Validators.PropertyValidator" />
public class NumericBetweenInValidator<TNumeric> : PropertyValidator
where TNumeric : IComparable
{
private TNumeric compareValueUp;

private TNumeric compareValueDown;

/// <summary>
/// 是否允許字串參數為空白.
/// </summary>
/// <value><c>true</c> if [allow empty]; otherwise, <c>false</c>.</value>
private bool AllowEmpty { get; set; }

/// <summary>
/// 轉型是否成功
/// </summary>
private bool IsConvertable;

/// <summary>
/// 是否允許等於輸入的上下閥值值
/// </summary>
private bool AllowEquals;

public NumericBetweenInValidator(
string valueUp,
string valueDown,
bool allowEquals = false,
bool allowEmpty = false) : base("傳入參數錯誤。")
{
this.compareValueUp = ConvertHelper.ToT<TNumeric>(valueUp, out IsConvertable);

this.compareValueDown = ConvertHelper.ToT<TNumeric>(valueDown, out IsConvertable);

this.AllowEquals = allowEquals;
this.AllowEmpty = allowEmpty;
}

protected override bool IsValid(PropertyValidatorContext context)
{
var propertyValue = context.PropertyValue as string;

if (this.AllowEmpty &&
string.IsNullOrWhiteSpace(propertyValue))
{
return true;
}

var value = ConvertHelper.ToT<TNumeric>(propertyValue, out IsConvertable);

if (!IsConvertable)
{
return false;
}

// -1 value < compareValue
// 0 value = compareValue
// 1 value > compareValue
if (AllowEquals)
{
return value.CompareTo(compareValueDown) >= 0
&& value.CompareTo(compareValueUp) <= 0;
}

return value.CompareTo(compareValueDown) > 0
&& value.CompareTo(compareValueUp) < 0;
}
}

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
/// <summary>
/// Class ConvertHelper
/// </summary>
internal static class ConvertHelper
{
/// <summary>
/// 轉型成 T.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value">The value.</param>
/// <returns></returns>
/// <exception cref="System.ArgumentException">Convert fail.;Convert</exception>
public static T ToT<T>(string value, out bool result) where T : IComparable
{
result = false;

try
{
switch (Type.GetTypeCode(typeof(T)))
{
case TypeCode.Double:

double doubleValue;
if (double.TryParse(value, out doubleValue))
{
result = true;
}

return (T)(object)Convert.ToDouble(value);

case TypeCode.Int16:

Int16 int16Value;
if (Int16.TryParse(value, out int16Value))
{
result = true;
}

return (T)(object)Convert.ToInt16(value);

case TypeCode.Int32:

Int32 int32Value;
if (Int32.TryParse(value, out int32Value))
{
result = true;
}

return (T)(object)Convert.ToInt32(value);

case TypeCode.Int64:

Int64 int64Value;
if (Int64.TryParse(value, out int64Value))
{
result = true;
}

return (T)(object)Convert.ToInt64(value);

case TypeCode.Decimal:

decimal decimalValue;
if (decimal.TryParse(value, out decimalValue))
{
result = true;
}

return (T)(object)Convert.ToDecimal(value);

default:
return default(T);
}
}
catch (Exception ex)
{
return default(T);
}
}
}

擴充方法```csharp
///


/// 符合數字區間,但允許空值.
/// EX : (1 < x < 3)
///

///
/// The type of the property.
/// The type of the numeric.
/// The rule builder.
/// Up threshold.
/// Down threshold.
///
public static IRuleBuilderOptions<T, TProperty> IsNumericAllowEmptyOrBetweenOf<T, TProperty, TNumeric>(
this IRuleBuilder<T, TProperty> ruleBuilder,
string upThreshold,
string downThreshold)
where TNumeric : IComparable
{
return ruleBuilder.SetValidator(
new NumericBetweenInValidator(
upThreshold,
downThreshold,
allowEquals: false,
allowEmpty: true));
}

        /// <summary>
    /// 符合數字區間且允許等於閥值,但允許空值.
    /// <para>EX : (1 &lt;= x &lt;= 3) </para>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
    /// <param name="ruleBuilder">The rule builder.</param>
    /// <param name="upThreshold">Up threshold.</param>
    /// <param name="downThreshold">Down threshold.</param>
    /// <returns></returns>
    public static IRuleBuilderOptions<T, TProperty> IsNumericAllowEmptyOrBetweenOfAllowEquals<T, TProperty, TNumeric>(
        this IRuleBuilder<T, TProperty> ruleBuilder,
        string upThreshold,
        string downThreshold)
        where TNumeric : IComparable
    {
        return ruleBuilder.SetValidator(
            new NumericBetweenInValidator<TNumeric>(
                upThreshold,
                downThreshold,
                allowEquals: true,
                allowEmpty: true));
    }

        /// <summary>
    /// 符合數字區間.
    /// <para>EX : (1 &lt; x &lt; 3) </para>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
    /// <param name="ruleBuilder">The rule builder.</param>
    /// <param name="upThreshold">Up threshold.</param>
    /// <param name="downThreshold">Down threshold.</param>
    /// <returns></returns>
    public static IRuleBuilderOptions<T, TProperty> IsNumericBetweenOf<T, TProperty, TNumeric>(
        this IRuleBuilder<T, TProperty> ruleBuilder,
        string upThreshold,
        string downThreshold)
        where TNumeric : IComparable
    {
        return ruleBuilder.SetValidator(
            new NumericBetweenInValidator<TNumeric>(
                upThreshold,
                downThreshold,
                allowEquals: false,
                allowEmpty: false));
    }

        /// <summary>
    /// 符合數字區間且允許等於閥值
    /// <para>EX : (1 &lt;= x &lt;= 3) </para>
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
    /// <param name="ruleBuilder">The rule builder.</param>
    /// <param name="upThreshold">Up threshold.</param>
    /// <param name="downThreshold">Down threshold.</param>
    /// <returns></returns>
    public static IRuleBuilderOptions<T, TProperty> IsNumericBetweenOfAllowEquals<T, TProperty, TNumeric>(
        this IRuleBuilder<T, TProperty> ruleBuilder,
        string upThreshold,
        string downThreshold)
        where TNumeric : IComparable
    {
        return ruleBuilder.SetValidator(
            new NumericBetweenInValidator<TNumeric>(
                upThreshold,
                downThreshold,
                allowEquals: true,
                allowEmpty: false));
    }
1
2
3
4
5
6
7
8
9
使用方法 ```csharp
//OrderBy - 允許空值或(0、1)
this.RuleFor(x => x.OrderBy)
.NotNumericAllowEmptyOrBetweenOfAllowEquals<GetBuildingDealCaseParameter, string, int>(
"2",
"1")
.WithErrorCode("X400")
.WithMessage("OrderBy應該在1~2之間");

  • 驗證是否為數字Array

    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
    /// <summary>
    /// 驗證是否為數字 Array
    /// </summary>
    /// <seealso cref="FluentValidation.Validators.PropertyValidator" />
    public class NumericArrayValidator<TNumeric> : PropertyValidator
    where TNumeric : IComparable
    {
    /// <summary>
    /// 是否允許Array為Null或空集合.
    /// </summary>
    /// <value>
    /// <c>true</c> if [allow empty]; otherwise, <c>false</c>.
    /// </value>
    private bool AllowEmpty { get; set; }

    /// <summary>
    /// Initializes a new instance of the <see cref="NumericArrayValidator{TNumeric}"/> class.
    /// </summary>
    /// <param name="allowEmpty">if set to <c>true</c> [allow empty].</param>
    public NumericArrayValidator(bool allowEmpty) : base("型別錯誤")
    {
    this.AllowEmpty = allowEmpty;
    }

    /// <summary>
    /// Returns true if ... is valid.
    /// </summary>
    /// <param name="context">The context.</param>
    /// <returns>
    /// <c>true</c> if the specified context is valid; otherwise, <c>false</c>.
    /// </returns>
    protected override bool IsValid(PropertyValidatorContext context)
    {
    var propertyValue = context.PropertyValue as List<string>;

    if (this.AllowEmpty &&
    (propertyValue == null || propertyValue.Count == 0))
    {
    return true;
    }

    //不允許空集合或Null
    if (!this.AllowEmpty &&
    (propertyValue == null || propertyValue.Count == 0))
    {
    return false;
    }

    bool IsConvertable;
    foreach (var x in propertyValue)
    {
    ConvertHelper.ToT<TNumeric>(x, out IsConvertable);
    if (!IsConvertable)
    {
    return false;
    }
    }
    return true;
    }
    }

  • 擴充方法*```csharp
    ///

       /// 是數字 Array,但允許空陣列或Null.
       /// </summary>
       /// <typeparam name="T"></typeparam>
       /// <typeparam name="TProperty">The type of the property.</typeparam>
       /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
       /// <param name="ruleBuilder">The rule builder.</param>
       /// <returns></returns>
       public static IRuleBuilderOptions<T, TProperty> IsNumericArrayAllowEmpty<T, TProperty, TNumeric>(
           this IRuleBuilder<T, TProperty> ruleBuilder)
           where TNumeric : IComparable
       {
           return ruleBuilder.SetValidator(
               new NumericArrayValidator<TNumeric>(allowEmpty: true));
       }
    
       /// <summary>
       /// 是數字 Array.
       /// </summary>
       /// <typeparam name="T"></typeparam>
       /// <typeparam name="TProperty">The type of the property.</typeparam>
       /// <typeparam name="TNumeric">The type of the numeric.</typeparam>
       /// <param name="ruleBuilder">The rule builder.</param>
       /// <returns></returns>
       public static IRuleBuilderOptions<T, TProperty> IsNumericArray<T, TProperty, TNumeric>(
           this IRuleBuilder<T, TProperty> ruleBuilder)
           where TNumeric : IComparable
       {
           return ruleBuilder.SetValidator(
               new NumericArrayValidator<TNumeric>(allowEmpty: false));
       }
    

將之前四元樹的程式改良後寫成筆記,之後用到才不會忘記

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>
/// Class QuadTreeNode.
/// </summary>
public class QuadTreeNode<T>
where T : IQuadTreeNodeData
{
/// <summary>
/// 西北象限.
/// </summary>
/// <value>
/// The north west.
/// </value>
public QuadTreeNode<T> NorthWest { get; set; }
/// <summary>
/// 東北象限.
/// </summary>
/// <value>
/// The north east.
/// </value>
public QuadTreeNode<T> NorthEast { get; set; }
/// <summary>
/// 西南象限.
/// </summary>
/// <value>
/// The south west.
/// </value>
public QuadTreeNode<T> SouthWest { get; set; }
/// <summary>
/// 東南象限.
/// </summary>
/// <value>
/// The south east.
/// </value>
public QuadTreeNode<T> SouthEast { get; set; }

/// <summary>
/// 邊界
/// </summary>
public BoundingBox BoundingBox { get; set; }

/// <summary>
/// 節點數量
/// </summary>
public int BucketCapacity { get; set; }

/// <summary>
/// 節點
/// </summary>
public List<T> Points { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="QuadTreeNode{T}"/> class.
/// </summary>
/// <param name="boundingBox">The bounding box.</param>
/// <param name="capacity">The capacity.</param>
public QuadTreeNode(BoundingBox boundingBox, int capacity)
{
BoundingBox = boundingBox;
BucketCapacity = capacity;
Points = new List<T>();
}
}

要使用四元樹,就要將資料結點實作這個介面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// &lt;summary&gt;
/// Interface IQuadTreeNodeData.
/// &lt;/summary&gt;
public interface IQuadTreeNodeData
{
/// &lt;summary&gt;
/// Gets or sets the position.
/// &lt;/summary&gt;
/// &lt;value&gt;
/// The position.
/// &lt;/value&gt;
Coordinate Position { get; set; }
}

要搜尋範圍的DTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// <summary>
/// Class BoundingBox.
/// </summary>
public class BoundingBox
{
/// <summary>
/// 左上角節點
/// </summary>
public Coordinate LeftTop { get; set; }

/// <summary>
/// 右下角節點
/// </summary>
public Coordinate RightBottom { get; set; }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// <summary>
/// Class Coordinate.
/// </summary>
public class Coordinate
{
/// <summary>
/// 緯度
/// </summary>
public double Latitude { get; set; }
/// <summary>
/// 經度
/// </summary>
public double Longitude { get; set; }
}

四元樹搜尋邏輯

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
/// <summary>
/// Class QuadTreeService.
/// </summary>
public static class QuadTreeService
{
/// <summary>
/// Inserts the specified node.
/// </summary>
/// <param name="node">The node.</param>
/// <param name="data">The data.</param>
/// <returns></returns>
public static bool Insert<T>(QuadTreeNode<T> node, T data)
where T : IQuadTreeNodeData
{
// Bail if our coordinate is not in the boundingBox
if (!CheckBoundingBoxContainData(node.BoundingBox, data))
{
return false;
}

// Add the coordinate to the points array
if ((node.Points.Count + 1) <= node.BucketCapacity)
{
node.Points.Add(data);
return true;
}

// Check to see if the current node is a leaf, if it is, split
if (node.NorthWest == null)
{
Subdivide(node);
}

// Traverse the tree
if (Insert(node.NorthWest, data)) return true;
if (Insert(node.NorthEast, data)) return true;
if (Insert(node.SouthWest, data)) return true;
if (Insert(node.SouthEast, data)) return true;

return false;
}

/// <summary>
/// 搜尋在範圍內的所有節點
/// </summary>
/// <param name="node">四元樹節點</param>
/// <param name="range">範圍</param>
/// <param name="datas"></param>
public static void SearchDataInRange<T>(
QuadTreeNode<T> node,
BoundingBox range,
ref List<T> datas)
where T : IQuadTreeNodeData
{
//如果QuadTree完全在要搜尋的Range之內,
//那包含在它底下的分裂節點都不用判斷,一定全部都在範圍內
if (CheckBoundingBoxIncludeInRange(node.BoundingBox, range))
{
GetNodeAllPointData(node, ref datas);
return;
}

// If range is not contained in the node's boundingBox then bail
if (!CheckIntersectionBoundingBox(node.BoundingBox, range))
{
return;
}

foreach (var item in node.Points)
{
// Gather points contained in range
if (CheckBoundingBoxContainData(range, item))
{
datas.Add(item);
}
}

// Bail if node is leaf
if (node.NorthWest == null)
{
return;
}

// Otherwise traverse down the tree
SearchDataInRange(node.NorthWest, range, ref datas);
SearchDataInRange(node.NorthEast, range, ref datas);
SearchDataInRange(node.SouthWest, range, ref datas);
SearchDataInRange(node.SouthEast, range, ref datas);
}

/// <summary>
/// 建立四元樹.
/// </summary>
/// <param name="data">The data.</param>
/// <param name="boundingBox">QuadTree Bounding</param>
/// <returns></returns>
public static QuadTreeNode<T> CreateQuadTree<T>(List<T> data, BoundingBox boundingBox)
where T : IQuadTreeNodeData
{
var TreeNode = new QuadTreeNode<T>(
boundingBox: boundingBox,
capacity: 1);

foreach (var d in data)
{
Insert(TreeNode, d);
}

return TreeNode;
}

/// <summary>
/// 取得節點以及所有子節點的Point Data
/// </summary>
/// <param name="node">The node.</param>
/// <param name="datas">The datas.</param>
static void GetNodeAllPointData<T>(QuadTreeNode<T> node, ref List<T> datas)
where T : IQuadTreeNodeData
{
datas.AddRange(node.Points);

if (node.NorthWest == null)
{
return;
}
GetNodeAllPointData(node.NorthWest, ref datas);
GetNodeAllPointData(node.NorthEast, ref datas);
GetNodeAllPointData(node.SouthWest, ref datas);
GetNodeAllPointData(node.SouthEast, ref datas);
}

/// <summary>
/// 確認兩個區域是否有交集
/// </summary>
/// <param name="BoundingBox">The bounding box.</param>
/// <param name="range">The range.</param>
/// <returns></returns>
static bool CheckIntersectionBoundingBox(BoundingBox BoundingBox, BoundingBox range)
{

if (BoundingBox.LeftTop.Latitude.ToString() == BoundingBox.RightBottom.Latitude.ToString() &&
BoundingBox.LeftTop.Longitude.ToString() == BoundingBox.RightBottom.Longitude.ToString())
{
//Point
var Lng = range.LeftTop.Longitude <= BoundingBox.LeftTop.Longitude &&
BoundingBox.LeftTop.Longitude <= range.RightBottom.Longitude;

var Lat = range.RightBottom.Latitude <= BoundingBox.LeftTop.Latitude &&
BoundingBox.LeftTop.Latitude <= range.LeftTop.Latitude;
return Lng && Lat;
}
else
{
var MinCx = Math.Max(range.LeftTop.Longitude, BoundingBox.LeftTop.Longitude);
var MaxCy = Math.Min(range.LeftTop.Latitude, BoundingBox.LeftTop.Latitude);

var MaxCx = Math.Min(range.RightBottom.Longitude, BoundingBox.RightBottom.Longitude);
var MinCy = Math.Max(range.RightBottom.Latitude, BoundingBox.RightBottom.Latitude);

return (MinCx < MaxCx) && (MinCy < MaxCy);
}
}

/// <summary>
/// 確認BoundingBox是否完全包含在要搜尋的區域範圍內(小於等於).
/// </summary>
/// <param name="BoundingBox">The bounding box.</param>
/// <param name="range">The range.</param>
/// <returns></returns>
static bool CheckBoundingBoxIncludeInRange(BoundingBox BoundingBox, BoundingBox range)
{
var LTLng = (range.LeftTop.Longitude <= BoundingBox.LeftTop.Longitude &&
BoundingBox.LeftTop.Longitude <= range.RightBottom.Longitude);

var LTLat = (range.RightBottom.Latitude <= BoundingBox.LeftTop.Latitude &&
BoundingBox.LeftTop.Latitude <= range.LeftTop.Latitude);

var RBLng = (range.LeftTop.Longitude <= BoundingBox.RightBottom.Longitude &&
BoundingBox.RightBottom.Longitude <= range.RightBottom.Longitude);

var RBLat = (range.RightBottom.Latitude <= BoundingBox.RightBottom.Latitude &&
BoundingBox.RightBottom.Latitude <= range.LeftTop.Latitude);

return LTLng && LTLat && RBLng && RBLat;
}

static bool CheckBoundingBoxContainData(BoundingBox boundingBox, IQuadTreeNodeData data)
{
var CheckLng = (boundingBox.LeftTop.Longitude <= data.Position.Longitude &&
data.Position.Longitude <= boundingBox.RightBottom.Longitude);

var CheckLat = (boundingBox.RightBottom.Latitude <= data.Position.Latitude &&
data.Position.Latitude <= boundingBox.LeftTop.Latitude);

return CheckLng && CheckLat;
}

/// <summary>
/// 四元樹分裂
/// </summary>
/// <param name="node">四元樹節點</param>
static void Subdivide<T>(QuadTreeNode<T> node)
where T:IQuadTreeNodeData
{
var box = node.BoundingBox;

//中心點
double LatMid = (box.LeftTop.Latitude + box.RightBottom.Latitude) / 2.0;
double LngMid = (box.LeftTop.Longitude + box.RightBottom.Longitude) / 2.0;

//左上
var NorthWest = new BoundingBox
{
LeftTop = new Coordinate
{
Latitude = box.LeftTop.Latitude,
Longitude = box.LeftTop.Longitude
},
RightBottom = new Coordinate
{
Latitude = LatMid,
Longitude = LngMid
}
};
node.NorthWest = new QuadTreeNode<T>(NorthWest, node.BucketCapacity);

//右上
var NorthEast = new BoundingBox
{
LeftTop = new Coordinate
{
Latitude = box.LeftTop.Latitude,
Longitude = LngMid
},
RightBottom = new Coordinate
{
Latitude = LatMid,
Longitude = box.RightBottom.Longitude
}
};
node.NorthEast = new QuadTreeNode<T>(NorthEast, node.BucketCapacity);

//左下
var SouthWest = new BoundingBox
{
LeftTop = new Coordinate
{
Latitude = LatMid,
Longitude = box.LeftTop.Longitude
},
RightBottom = new Coordinate
{
Latitude = box.RightBottom.Latitude,
Longitude = LngMid
}
};
node.SouthWest = new QuadTreeNode<T>(SouthWest, node.BucketCapacity);

//右下
var SouthEast = new BoundingBox
{
LeftTop = new Coordinate
{
Latitude = LatMid,
Longitude = LngMid
},
RightBottom = new Coordinate
{
Latitude = box.RightBottom.Latitude,
Longitude = box.RightBottom.Longitude
}
};
node.SouthEast = new QuadTreeNode<T>(SouthEast, node.BucketCapacity);

}
}

————使用方法—————–
**
**

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
void Main()
{
Stopwatch timer = new Stopwatch();
timer.Reset();
timer.Start();
var YouWantToSearchData = new List<UserQuery.YourCoordinateData>();
for (int i = 0; i < 5000000; i++)
{
YouWantToSearchData.Add(
new YourCoordinateData
{
Position = new Coordinate
{
Latitude = GetRandomNumber(22,26),
Longitude = GetRandomNumber(120,122)
}
});
}
var QuadTree = QuadTreeService.CreateQuadTree(YouWantToSearchData,GetBoundingBox()); //建立四元樹
string.Format("建資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds).Dump();

//要搜尋的範圍
BoundingBox range = new BoundingBox
{
LeftTop = new Coordinate { Latitude = 24.997123, Longitude = 121.21940612793 },
RightBottom = new Coordinate {Latitude =24.958553314209,Longitude = 121.485925}
};

//一般搜尋
timer.Reset();
timer.Start();
var Result1 = Search(range,YouWantToSearchData);
string.Format("搜尋資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds).Dump();

//四元樹搜尋
timer.Reset();
timer.Start();
var Result2 = new List<YourCoordinateData>();
QuadTreeService.SearchDataInRange(QuadTree,range,ref Result2);
string.Format("四元樹搜尋資料花了: {0} ticks ({1} msec)", timer.ElapsedTicks, timer.ElapsedMilliseconds).Dump();

string.Format("一般搜尋資料筆數: {0}",Result1.Count()).Dump();
string.Format("四元樹搜尋資料筆數: {0}",Result2.Count()).Dump();
}

public List<YourCoordinateData> Search(BoundingBox range, List<YourCoordinateData> Datas)
{
return Datas.Where(x => range.RightBottom.Latitude <= x.Position.Latitude && x.Position.Latitude <= range.LeftTop.Latitude &&
range.LeftTop.Longitude <= x.Position.Longitude && x.Position.Longitude <= range.RightBottom.Longitude).ToList();
}

public class YourCoordinateData : IQuadTreeNodeData
{
public Coordinate Position{get;set;}
}

//台灣的範圍
public BoundingBox GetBoundingBox()
{
return new BoundingBox
{
//台灣範圍
LeftTop = new Coordinate
{
Latitude = 26,
Longitude = 120
},
RightBottom = new Coordinate
{
Latitude = 22,
Longitude = 122
}
};
}

//隨機取得經緯度亂數
static Random random = new Random();
public static double GetRandomNumber(double minimum, double maximum)
{
return random.NextDouble() * (maximum - minimum) + minimum;
}

分別實驗了10萬筆與500萬筆的搜尋數據,當然筆數擴張的越大,這個數據差距會越大

[![](https://3.bp.blogspot.com/-ZxXLvUMEaU8/V-jEmww86II/AAAAAAAAH98/12-o58aYKrcsVPIi6iPjGxEbt8ngHFFvwCLcB/s1600/1.png)](https://3.bp.blogspot.com/-ZxXLvUMEaU8/V-jEmww86II/AAAAAAAAH98/12-o58aYKrcsVPIi6iPjGxEbt8ngHFFvwCLcB/s1600/1.png)
10萬筆
[![](https://2.bp.blogspot.com/-Kba3y7eAdFo/V-jFYZ5hXoI/AAAAAAAAH-I/_Mk3UU4NCII26OC6RqXubc5SdEkAMawogCLcB/s1600/2.png)](https://2.bp.blogspot.com/-Kba3y7eAdFo/V-jFYZ5hXoI/AAAAAAAAH-I/_Mk3UU4NCII26OC6RqXubc5SdEkAMawogCLcB/s1600/2.png)
500萬筆

SourceCode : https://github.com/toyo0103/Demo_EditTemplate_1

這邊提供幾篇覺得很棒的文章,看完其實也可以略過這篇了XD

 1. 30天快速上手 TDD Day 12 - Refactoring - 職責分離  By 91

 2. [ASP.NET]91之ASP.NET由淺入深 不負責講座 Day17 – 單一職責原則

 3. Clean code: 無瑕的程式碼 – 書摘心得(二)

好的!!誠如上面幾篇文章提到的,我對於SRP的理解是「每個類別應該都只有一個理由被修改」。 其實看起來很簡單但卻又抽象到頭痛,之前寫程式時常常為了分離程式碼而頭痛不已,不可否認的是,全部寫成一起在開發上又快又方便,但這其實無形中堆高了技術債,為了在改動需求或修改程式時,要嘛就從頭看到尾找出其中的一兩行改,要嘛就是乾脆重寫,因為互相綁得太死了,牽一髮動全身阿(遠目)

[![](https://2.bp.blogspot.com/-cYpekfGnPkg/V7UdvdmnldI/AAAAAAAAH84/MR0-YMqszPUeIoiFGPID_Tmx_ERoWnO0wCLcB/s320/srp.jpg)](https://2.bp.blogspot.com/-cYpekfGnPkg/V7UdvdmnldI/AAAAAAAAH84/MR0-YMqszPUeIoiFGPID_Tmx_ERoWnO0wCLcB/s1600/srp.jpg)
如果想在這萬能的瑞士刀加上一個新型刀片,除了重做一個更長的基座,似乎無解阿....

一、分析

讓我們來看看原本的程式中哪邊明顯的沒有單一職責?

**
**Application層
**
**傳入Channel ID,透過IPOISerice取得對應的POI資料並且回傳資料,看起來職責非常明確

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HomeController : Controller
{
IPOIService _POIService;
public HomeController(IPOIService poiService)
{
//POIService改成依賴介面IPOIService
//並且實體是由外部來決定,也就是所謂的依賴注入DI
_POIService = poiService;
}

public ActionResult Index(string id)
{
//依賴外部注入的IPOIService介面
var POIs = _POIService.Get(id);
return Json(POIs,JsonRequestBehavior.AllowGet);
}
}

Repository層

看起來兩個供應商的Repository也都恰如其分的只撈自己所屬的資料,職責明確,要改哪個供應商撈資料的方式,打開對應的Repository檔案即可,腎好腎好 (難道肝不好嗎?)
**
**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// POI A供應商
/// </summary>
internal class ASupplierRepository : ISupplierRepository
{
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;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/// <summary>
/// POI B供應商
/// </summary>
internal class BSupplierRepository : ISupplierRepository
{
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
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
public class POIService :IPOIService
{
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 =>
{
var Repository = SupplierFactory.Generate(x);
var POIs = Repository.Get();
Result.AddRange(POIs);
});
}

//並接總合的結果回傳
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 }
}
}
};
}
}

讓我們用人話的方式說說它目前都做了啥(驚世劇場口氣)!!

  • Get() : 傳入Channel ID,先呼叫私有方法GetChannel,取得該頻道該有的類別跟對應POI廠商表,然後依據POI廠商去跟源頭Repository要資料
  • GetChannel() : 傳入Channel ID,取回該頻道該有的類別與對應的POI廠商表

如果今天我要修改取POI源頭資料的邏輯,我要打開POIService修改。

如果今天我要修改取頻道與廠商的對應表邏輯,或是接上真實的DB資料,我也要打開POIService修改

對於POIService來說,它的職責已經包含兩個不同面向的事情了,所以在這邊我們要抽離取頻道與廠商的對應表邏輯到專屬負責的類別去,來符合SRP

二、重構開始 首先在**Service層**建立專屬的類別來取頻道與廠商的對應表,所以這邊建立一個**ChannelService**,然後將原本屬於POIService的邏輯搬到這邊來
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
public class ChannelService
{
/// <summary>
/// 模擬頻道類別對應表
/// </summary>
/// <param name="channelID">The channel identifier.</param>
/// <returns>Channel.</returns>
public Channel Get(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 }
}
}
};
}
}

把原本不該屬於POIService的程式碼刪除,改成如下

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
public class POIService :IPOIService
{
public List<POI> Get(string channelID)
{
var Result = new List<POI>();

//取得ChannelService實體,取得Channel對應供應商資料
var channelService = new ChannelService();
var channel = channelService.Get(channelID);

//逐一搜尋頻道底下的各類別
foreach (var category in channel.Categorys)
{
//依據該類別所指定的供應商向Repository要資料
category.Supplier.ForEach(x =>
{
var Repository = SupplierFactory.Generate(x);
var POIs = Repository.Get();
Result.AddRange(POIs);
});
}

//並接總合的結果回傳
return Result;
}
}


接下來是不是因為達成職責分離了,所以大家可以手拉手一起去吃冰快樂的下午茶了阿。
如果是的話請到旁邊面壁思過(拍掉已經牽起來的手),**說好的高內聚低耦合呢? 說好的IOC與DI呢 ? 說好的面對抽象呢? **(左手心拍右手背)

  • 隔離ChannelService,建立Interface

    [![](https://2.bp.blogspot.com/-CbLQzZGAVWA/V7Uk_BjnrQI/AAAAAAAAH9I/2X3Wh9Igf34lYISJFe19Tu2pANOaX6tUgCLcB/s1600/1.png)](https://2.bp.blogspot.com/-CbLQzZGAVWA/V7Uk_BjnrQI/AAAAAAAAH9I/2X3Wh9Igf34lYISJFe19Tu2pANOaX6tUgCLcB/s1600/1.png)
    1
    2
    3
    4
    5
    public interface IChannelService
    {
    Channel Get(string channelID);
    }

  • POIService透過DI與IOC的概念,面對IChannelService而非實體 ```csharp
    public class POIService :IPOIService
    {

      IChannelService _ChannelService;
      public POIService(IChannelService channelService)
      {
          //DI
          _ChannelService = channelService;
      }
    
          public List<POI> Get(string channelID)
      {
          var Result = new List<POI>();
    
              //面對抽象
          var channel = _ChannelService.Get(channelID);
    
              //逐一搜尋頻道底下的各類別
          foreach (var category in channel.Categorys)
          {
              //依據該類別所指定的供應商向Repository要資料
              category.Supplier.ForEach(x =>
              {
                  var Repository = SupplierFactory.Generate(x);
                  var POIs = Repository.Get();
                  Result.AddRange(POIs);
              });
          }
    
              //並接總合的結果回傳
          return Result;
      }
    

    }

```

  • 別忘記到Unity把Interface與實體的關係註冊起來




    三、小結 看看執行的結果是否正常
    [![](https://3.bp.blogspot.com/-rn9m0xATZ2A/V7Umyyoz9_I/AAAAAAAAH9U/rAETSPwprloEh1HSjPDpP3v0Cq0iAp_gACLcB/s640/1.png)](https://3.bp.blogspot.com/-rn9m0xATZ2A/V7Umyyoz9_I/AAAAAAAAH9U/rAETSPwprloEh1HSjPDpP3v0Cq0iAp_gACLcB/s1600/1.png)
    妥妥的阿!!!
    所以這邊我們重構了Service的部分,讓它職責更為明確,而且也用了前幾篇提到的物件導向觀念,讓彼此是互相合作但是又具有高獨立性的。

如果現在真的想把取Channel的對應資料改去接DB,我們只要打開ChannelService去接對應的Repository即可,其他地方都不會動到。

如果現在是主管說Demo的時候要用假資料,正式機要接DB取真實的Channel資料,那我們只要保留原本ChannelService,另外寫一個ChannelFromDBService,然後都實做IChannelService,並且在Unity那邊做個開關動態改變註冊的實體即可

應該有漸漸感受到,物件導向對於變動這件事情的高擴展性與可維護性了,相信天下沒有不改需求的PM,所以也就不可能有不改版的軟體了阿(再次遠目)

最後記住一件事情,「別讓你的Code又臭又長」,那麼下回見啦~

[![](https://2.bp.blogspot.com/-NHl1LCbQLR0/V7UoU2H1ijI/AAAAAAAAH9g/1CJwhDpona4tNKcNlesR5HCDoruhHCvVgCLcB/s320/687474703a2f2f692e696d6775722e636f6d2f63464a6b6638692e6a7067.jpg)](https://2.bp.blogspot.com/-NHl1LCbQLR0/V7UoU2H1ijI/AAAAAAAAH9g/1CJwhDpona4tNKcNlesR5HCDoruhHCvVgCLcB/s1600/687474703a2f2f692e696d6775722e636f6d2f63464a6b6638692e6a7067.jpg)

SourceCode : https://github.com/toyo0103/Demo_EditTemplate_1

依據前一篇延續,接著我們把焦點放在Application與Service之間的狀況

[![](https://3.bp.blogspot.com/-urWNSzGdlp8/V7O6-tD1woI/AAAAAAAAH6w/z2GwsqBn9hANkslSqNpALNAuRiBIuuI4ACLcB/s400/1.png)](https://3.bp.blogspot.com/-urWNSzGdlp8/V7O6-tD1woI/AAAAAAAAH6w/z2GwsqBn9hANkslSqNpALNAuRiBIuuI4ACLcB/s1600/1.png)
前一篇有提到程式應該要面對**抽象**避免高耦合的情況發生,改一髮進而動全身,所以有用**介面**的方式去處理
[![](https://3.bp.blogspot.com/-O7kXIwvgtDk/V7O7kOH1D0I/AAAAAAAAH60/M49OysRZ7R4LxOLv4UNE9WJVLBBpsICJwCLcB/s400/1.png)](https://3.bp.blogspot.com/-O7kXIwvgtDk/V7O7kOH1D0I/AAAAAAAAH60/M49OysRZ7R4LxOLv4UNE9WJVLBBpsICJwCLcB/s1600/1.png)

然而來看看目前實際的程式狀況 : 直接耦合

[![](https://4.bp.blogspot.com/-csgLbuLY5Dw/V7O9BEEADYI/AAAAAAAAH7A/n6mzb9hYUiksdAHYuQmBXuH6aPO0Sk8-wCLcB/s400/1.png)](https://4.bp.blogspot.com/-csgLbuLY5Dw/V7O9BEEADYI/AAAAAAAAH7A/n6mzb9hYUiksdAHYuQmBXuH6aPO0Sk8-wCLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-LkVwTAXMQHc/V7O9TcpERgI/AAAAAAAAH7I/18cwMlw6YtQxVKhppexI25lzS--gxIIagCLcB/s400/1.png)](https://2.bp.blogspot.com/-LkVwTAXMQHc/V7O9TcpERgI/AAAAAAAAH7I/18cwMlw6YtQxVKhppexI25lzS--gxIIagCLcB/s1600/1.png)

這邊會談到兩個用來解開這種狀況的觀念

  • 依賴注入(Dependency Injection)簡稱DI
  • **控制反轉(**Inversion Of Control)簡稱IOC

先看看Google怎麼解釋
控制反轉(Inversion of Control,縮寫為IoC),是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI)

不知道看完有沒有懂的感覺XD,明明都是中文看完卻很想說「先生(小姐)可以講中文嗎?」。
撇開這些專業的術語不看,我們直接開始重構來體會這兩個概念

一、重構開始

抽取介面
避免直接耦合的第一步,抽取介面來隔離實體,既然這邊實際耦合了POIService,那我們就為它抽取出Interface IPOIService

[![](https://3.bp.blogspot.com/-LrWsJfLH2vU/V7PAEPoqZKI/AAAAAAAAH7Y/VCUD_9YVxnEPfAzFCKpQvHY3jQRkMBD9ACLcB/s1600/1.png)](https://3.bp.blogspot.com/-LrWsJfLH2vU/V7PAEPoqZKI/AAAAAAAAH7Y/VCUD_9YVxnEPfAzFCKpQvHY3jQRkMBD9ACLcB/s1600/1.png)
1
2
3
4
5
6
7
8
9
10
public interface IPOIService
{
/// <summary>
/// 取得屬於頻道的POI資料
/// </summary>
/// <param name="channelID">The channel identifier.</param>
/// <returns>List&lt;POI&gt;.</returns>
List<POI> Get(string channelID);
}

POIService掛上IPOIService

[![](https://2.bp.blogspot.com/-YS5Dd3-SnX4/V7PA1QAh0ZI/AAAAAAAAH7g/Av6PbiG5yYgct0SYM1kP_n8mTQTLy0N2ACLcB/s400/1.png)](https://2.bp.blogspot.com/-YS5Dd3-SnX4/V7PA1QAh0ZI/AAAAAAAAH7g/Av6PbiG5yYgct0SYM1kP_n8mTQTLy0N2ACLcB/s1600/1.png)
**讓Application依賴介面**

上一篇遇到這種狀況時,我們會做一個Factory來產生實體,並且封裝在Service層。這次我們不這麼做,改成用依賴注入(DI)的方式來實作,將IPOIService改成用建構子帶入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HomeController : Controller
{
IPOIService _POIService;
public HomeController(IPOIService poiService)
{
//POIService改成依賴介面IPOIService
//並且實體是由外部來決定,也就是所謂的依賴注入DI
_POIService = poiService;
}

public ActionResult Index(string id)
{
//依賴外部注入的IPOIService介面
var POIs = _POIService.Get(id);
return Json(POIs,JsonRequestBehavior.AllowGet);
}
}

這邊用的IPOIService是由外部注入的,實體已經不由HomeController 內部來決定了(不New實體了),也就是上面所提到的DI 依賴注入(要用的實體由建構子注入),IOC控制反轉(實體已經由內部控制改由外部控制了)

接下來面臨的問題是,雖然我們讓實體改成用外部注入的方式,但HomeController在建立的時候是由系統去產生的,它怎麼知道IPOIService的實體是要注入誰,所以現在執行程式會看到以下結果

[![](https://4.bp.blogspot.com/-0ijdWklCbSk/V7QYVfZnz_I/AAAAAAAAH7w/Bj3Sa8cECEcTJNOsObhzWMd6vMjwUJIggCLcB/s1600/1.png)](https://4.bp.blogspot.com/-0ijdWklCbSk/V7QYVfZnz_I/AAAAAAAAH7w/Bj3Sa8cECEcTJNOsObhzWMd6vMjwUJIggCLcB/s1600/1.png)

這時候就要介紹Unity這個套件來幫我們解決這個問題了

**


**何謂Unity

[![](https://1.bp.blogspot.com/-PEXphTQycyE/V7QY_Y7zGoI/AAAAAAAAH70/n8FT2stz8PQnOEXWrUSl95ilLNBrmTz1gCLcB/s400/MSUNITY.jpg)](https://1.bp.blogspot.com/-PEXphTQycyE/V7QY_Y7zGoI/AAAAAAAAH70/n8FT2stz8PQnOEXWrUSl95ilLNBrmTz1gCLcB/s1600/MSUNITY.jpg)
MSDN上的Unity套件介紹 : [https://msdn.microsoft.com/en-us/library/ff647202.aspx](https://msdn.microsoft.com/en-us/library/ff647202.aspx) ** **其實這個套件說穿了就是在Application啟動時,註冊每個Interface對應的實體是誰,當碰到要注入這個Interface時,Unity就會去找找看你有沒有跟它說對應的實體,如果有,它就把實體產生出來幫你帶入。那就一步一步做做看邊體會它的意思 **1.安裝Unity套件 ** 請在Application專案透過Nuget搜尋Unity安裝**Unity.Mvc**
[![](https://2.bp.blogspot.com/-HhIfl_RkSKA/V7QbZyR_3SI/AAAAAAAAH8M/C4VxtKVrLdUkdbq6eGIXxg0bH9m_A6DpACLcB/s640/1.png)](https://2.bp.blogspot.com/-HhIfl_RkSKA/V7QbZyR_3SI/AAAAAAAAH8M/C4VxtKVrLdUkdbq6eGIXxg0bH9m_A6DpACLcB/s1600/1.png)
** ****2.註冊Interface應該對應的實體** ** **這時候打開App_Start資料夾應該會看到多了一個檔案**UnityConfig.cs**
[![](https://2.bp.blogspot.com/-MCmsg-5dEEY/V7Qb_xiRLFI/AAAAAAAAH8U/3JMMEHww4joUlfQI3HjpW0abcn343O9KwCLcB/s320/1.png)](https://2.bp.blogspot.com/-MCmsg-5dEEY/V7Qb_xiRLFI/AAAAAAAAH8U/3JMMEHww4joUlfQI3HjpW0abcn343O9KwCLcB/s1600/1.png)
打開它會看到以下Code跟註解,教你怎麼使用它
[![](https://2.bp.blogspot.com/-B5FxVSeEW9Q/V7QcPsxDVeI/AAAAAAAAH8Y/lsWGO864ftoAVFsUH3nmO9mB4LL0lOAZwCLcB/s640/1.png)](https://2.bp.blogspot.com/-B5FxVSeEW9Q/V7QcPsxDVeI/AAAAAAAAH8Y/lsWGO864ftoAVFsUH3nmO9mB4LL0lOAZwCLcB/s1600/1.png)
請將**RegisterTypes** 改成以下
```csharp /// Registers the type mappings with the Unity container. /// The unity container to configure. /// There is no need to register concrete types such as controllers or API controllers (unless you want to /// change the defaults), as Unity allows resolving a concrete type even if it was not previously registered. public static void RegisterTypes(IUnityContainer container) {
        //TransientLifetimeManager為這個實體的創建生命週期
        //表示執行程式時,每次碰到IPOIService都會去new 一個新的POIService
        //Unity有許多生命週期可以使用,可以參考官方的文件
        //例如PerResolveLifetimeManager就是本次Request只有New一次實體,如果同個Request碰到多次
        //都還是會返回第一次New的實體,直到Reqeust結束才會被消滅掉,Singleton的概念
        container.RegisterType<IPOIService, POIService>(new TransientLifetimeManager());
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17


關於Unity創建實體的生命週期請參考官方文件 :&nbsp;[https://msdn.microsoft.com/en-us/library/ff660872(v=pandp.20).aspx](https://msdn.microsoft.com/en-us/library/ff660872(v=pandp.20).aspx)

註冊好後接著執行看看,會發現程式又活過來了,不會再出現剛剛的錯誤頁面,因為程式已經知道該Interface對應的實體是誰,當由外部注入時,Unity會自己去幫你控制實體的產生並且注入

<div class="separator" style="clear: both; text-align: center;">[![](https://4.bp.blogspot.com/-QgBTpClZA_g/V7QgW_P_VNI/AAAAAAAAH8o/EBBv1BKh0pIIbOKaZoPRQB4Gt-dmnLcmgCLcB/s640/1.png)](https://4.bp.blogspot.com/-QgBTpClZA_g/V7QgW_P_VNI/AAAAAAAAH8o/EBBv1BKh0pIIbOKaZoPRQB4Gt-dmnLcmgCLcB/s1600/1.png)</div>

透過這個重構的實例,我們可以從裡面瞭解到何謂**DI**與**IOC**的概念,但可能會想說,那這樣的好處是什麼? 直接New錯了嗎?

回到前一篇提到的,如果直接New的話也就是高耦合的狀況,如果今天面對的實體改動時,很難避免耦合的地方不會跟著變動,所以我們**依賴抽象**(Interface),用了Unity後其實又牽扯到一個大重點"**職責分離**"。

以後如果有Interface與實體的對應關係,直接就會想到要去**UnityConfig.cs**來改,所有的註冊表在這邊統一管理而且一目了然,如果哪天主管跟你說**POIService**它不喜歡,要重新寫個**NewPOIService**,那我們是不是直接把**NewPOIService實作**<span style="color: #bf9000;">IPOIService</span>後,然後打開**UnityConfig.cs**改成這樣即可

```csharp
container.RegisterType<IPOIService, NewPOIService>(new TransientLifetimeManager());

其他地方連動都不用動,就已經直接將對應的實體給換掉了!!! 統一管理之外,也能達到最小更動原則。

這次的重構就先到這邊~

以下實做的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)

寫單元測試的時候常常會使用FluentAssertions這個套件來做驗證,即便兩個巢狀結構的類別透過這個套件也能輕鬆解決比對問題。

但最近碰到當重狀結構裡面有Double這類的趨近值數值時,驗證會失敗,查了一下後找到解法,所以紀錄一下

原本UnitTest程式長這樣

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
[TestMethod]
public void Test_DoubleAssert()
{
//arrange
var stops = new List<BusStopAPIResult>
{
new BusStopAPIResult
{
StopName = new NameAPIResult
{
Zh_tw = "吊橋頭",
En = "deocioutao"
},
StopUID = "StopUID1",
StopPosition = new BusStopAPIResult.Coordinate
{
PositionLat = 24.853690,
PositionLon = 121.354917
}
},
new BusStopAPIResult
{
StopName = new NameAPIResult
{
Zh_tw = "吊橋頭",
En = "deocioutao"
},
StopUID = "StopUID2",
StopPosition = new BusStopAPIResult.Coordinate
{
PositionLat = 24.853632,
PositionLon = 121.354874
}
}
};

var expected = new List<BusStopClusterDTO>
{
new BusStopClusterDTO
{
Name = "吊橋頭",
SourceID = "StopUID1",
IDs = new List<string> { "StopUID1" , "StopUID2" },
Lat = 24.853661,
Lng = 121.3548955
}
};

//act
var actual = Program.ClusterSourceData(stops);

//assert
actual.ShouldBeEquivalentTo(expected);
}

執行結果如圖

[![](https://2.bp.blogspot.com/-LvpAUuEAqzk/V6P6OrTaInI/AAAAAAAAH3U/37TPKUzXfAI0KKdMpNDAJCpcvBrZXZH2gCLcB/s640/1.png)](https://2.bp.blogspot.com/-LvpAUuEAqzk/V6P6OrTaInI/AAAAAAAAH3U/37TPKUzXfAI0KKdMpNDAJCpcvBrZXZH2gCLcB/s1600/1.png)
[![](https://2.bp.blogspot.com/-xi043e38HW0/V6P6m4ED9_I/AAAAAAAAH3Y/T29FHNHT7h4Y51M1RTmwb1dnZJm-wMLMQCLcB/s320/1.png)](https://2.bp.blogspot.com/-xi043e38HW0/V6P6m4ED9_I/AAAAAAAAH3Y/T29FHNHT7h4Y51M1RTmwb1dnZJm-wMLMQCLcB/s1600/1.png)

因為數值是經緯度,經過計算後不需要驗證精確到小數點這麼多位數,所以可以改用以下方法去驗證

1
2
3
4
5
6
7
actual.ShouldBeEquivalentTo(
expected,
//當欄位型別為Double時,相差到0.000001之內都是OK的
option => option.Using<double>(
ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.000001))
.WhenTypeIs<double>());

這樣驗證就會過關了!!