0%

【重構系列】(三) 單一職責原則(Single Responsibility Principle , SRP)

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)