使用 FP 將有不同的實現方式

Decorator Pattern 是 OOP 中著名的 Design Pattern,尤其可在不改變 interface 的前提下,動態對原有物件增加功能,隨著 FP 逐漸受到重視,Decorator Pattern 在實作上也有了新的面貌。

Version


macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
JetBrains Rider 2017.3.1
C# 7.2

User Story


假設你在處理訂單,訂單的折扣方式有兩種

  • 超過 1000 元,則 全館八折滿千送百
  • 不到 1000 元,則 全館八折

Task


先使用一般 if else 寫法完全需求,最後再分別以 OOP 與 FP 手法重構成 Decorator Pattern。

Definition


Decorator Pattern

在不改變原有 interface 的前提下,動態增加原有的功能

ecorator00

  • ClientContext 的 user,實務上可能是 component 或 controller
  • Context:提供 client 呼叫的 class,實務上可能是 service
  • ComponentInterface:定義 ConcreteComponentConcreteDecorator 的共同 interface,只有 Operation()
  • AbstractDecorator:負責處理 ConcreteDecorator 間共用的 constructor
  • ConcreteDecorator:實際要 decorate 的功能

適用時機

  • 在不改變原有 interface 的前提下,動態增加原有的功能
  • Decorator 的功能常會排列組合變動

優點

  • 無論怎麼增加新功能,interface 都不會改變
  • 可以隨意的組合 decorator,方便維護

缺點

  • 若 decorator 過多,容易造成 decorator class 數量爆炸

Architecture


ecorator00

  • OrderService 相當於 Context
  • PriceInterface 相當於 ComponentInterface,定義 DiscountComponentRebateDecorator 的共同 interface
  • DiscountComponent 相當於 ConcreteComponent,實作 全館八折 功能
  • AbstractDecorator 實作 ConcreteComponent 間共用的 constructor
  • RebateDecorator 相當於 ConcreteDecorator,實作 滿千送百 功能

Implementation


ecorator00

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using OrderLibrary;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var orderService = new OrderService();
var originalPrice = 1200;
var realPrice = orderService.GetPrice(originalPrice);
Console.WriteLine("Original price:{0}, Real price:{1}", originalPrice, realPrice);

originalPrice = 800;
realPrice = orderService.GetPrice(originalPrice);
Console.WriteLine("Original price:{0}, Real price:{1}", originalPrice, realPrice);
}
}
}

10 行

1
2
3
var orderService = new OrderService();
var originalPrice = 1200;
var realPrice = orderService.GetPrice(originalPrice);

將商業邏輯都寫在 OrderService,當 originalPrice 傳入 GetPrice() 後,應回傳 滿千送百全館八折 後的 realPrice

If Else

ecorator00

OrderService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace OrderLibrary
{
public class OrderService
{
public double GetPrice(double price)
{

if (price < 1000)
{
return price * 0.8;
}
else
{
return price * 0.8 - 100;
}
}
}
}

使用 if else 很直覺的寫出程式碼,在不同 price 條件下,會有不同的計算 price 商業邏輯。

在以上的程式碼我們發現了幾件事情:

  • 無論任何價錢都是 全館八折,所以 price * 0.8 已經重複
  • - 100 只有發生在 超過 1000

事實上在電子商務領域,所有的促銷折扣都可能根據需求而排列組合動態調整,目前看起來 全館八折 算是基本,但 滿千送百 算是附加上去的促銷折扣,且隨時可能調整。

對於 滿千送百來說,算是由 全館八折 附加上去的,因此我們可以將 滿千送百 看成是 全館八折 的 decorator,也就是在原有的 全館八折 的基礎下,附加 滿千送百 的功能,這就是 Decorator Pattern。

Unit Test

在重構之前,必須要有測試保護,才能確保沒把原本的商業邏輯重構壞,因此我們先準備好 OrderService 的 Unit Test,確保每個 if else 的 path 都有測到。

UnitTest1.cs

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
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace OrderLibrary.Tests
{
[TestClass]
public class UnitTest1
{
[TestMethod]
public void 當價錢為1200打八折再送百後為860()
{
// Arrange
var target = new OrderService();

// Act
var actual = target.GetPrice(1200);

// Assert
var expected = 860;
Assert.AreEqual(expected, actual);
}

[TestMethod]
public void 當價錢為800送百後為640()
{
// Arrange
var target = new OrderService();

// Act
var actual = target.GetPrice(800);

// Assert
var expected = 640;
Assert.AreEqual(expected, actual);
}
}
}

由於本文重點不是在講 Unit Test,因此就不浪費篇幅解釋以上程式碼。

ecorator00

Decorator Pattern

PriceInterface

ecorator00

PriceInterface.cs

1
2
3
4
5
6
7
namespace OrderLibrary
{
public interface PriceInterface
{
double calculatePrice(double price);
}
}

定義 PriceInterface 必須有 CalculatePrice(),將來其他 component 與 decorator 必須遵守此 interface。

DiscountComponent

ecorator00

DiscountComponent.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public class DiscountComponent : PriceInterface
{
public double calculatePrice(double price)
{

return price * 0.8;
}
}
}

遵守 PriceInterface 下的 DiscountComponent,實現 全館八折

AbstractDecorator

ecorator00

AbstractDecorator.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace OrderLibrary
{
public abstract class AbstractDecorator: PriceInterface
{
protected PriceInterface _component;

protected AbstractDecorator(PriceInterface component)
{

_component = component;
}

public abstract double CalculatePrice(double price);
}
}

第 3 行

1
public abstract class AbstractDecorator: PriceInterface

Decorator 也要遵守 PriceInterface,因此對 client 而言,無論是 DiscountComponentRebateDecorator ,都視為 PriceInterface 物件,這樣 client 就可以在 interface 沒有變動的前提下,替 component 增加 decorator 功能,符合 開放封閉原則 要求。

第 5 行

1
2
3
4
5
6
protected PriceInterface _component;

protected AbstractDecorator(PriceInterface component)
{

_component = component;
}

所有 decorator 必須由其 constructor 傳入 component 或 decorator,但因為這兩個 class 都是實作 PriceInterface, 可以抽象化視為 PriceInterface 型別物件。

由於每個 decorator 都會使用相同的 constructor 處理 PriceInterface 型別物件,所以特別抽出來寫在 AbstractDecorator

Decorator Pattern 之所以會特別有 AbstractDecorator 設計,主要也是要避免各 decorator 的 constructor 程式碼重複問題

12 行

1
public abstract double CalculatePrice(double price);

caluculatePrice()PriceInterface 所定義,因為各 Decorator 會有自己的 calculatePrice() 方式,因此不需由 AbstractDecorator 實作,宣告為 abstract 即可,交由 ConcreateDecorator 自行實作。

RebateDecorator

rchitectur

RebateDecorator.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
namespace OrderLibrary
{
public class RebateDecorator: AbstractDecorator
{
public RebateDecorator(PriceInterface component) : base(component)
{

}

public override double CalculatePrice(double price)
{

return _component.calculatePrice(price) - 100;
}
}
}

第 3 行

1
public class RebateDecorator: AbstractDecorator

因為 AbstractDecorator 已經幫我們處理共用 constructor 部分,因此要繼承 AbstractDecorator,但別忘了 AbstractDecorator 也是實作 PriceInterface,根據 里式替換原則,因此 RebateDecorator 也還是 PriceInterface 型別。

第 5 行

1
2
3
public RebateDecorator(PriceInterface component) : base(component)
{

}

當 component 或 decorator 透過 constructor 傳入時,再透過 base 傳入 AbstractDecorator 的 constructor,因為共用的 constructor 已經抽到 AbstractDecorator 了。

第 9 行

1
2
3
4
public override double CalculatePrice(double price)
{

return _component.calculatePrice(price) - 100;
}

真正實現 滿千送百 部分,但別忘 滿千送百 是 decorator,是依附在 全館八折 下,因此必須先執行 _component.calculatePrice(price),也就是先計算 全館八折 後,再 -100 實現 滿千送百

OrderService

ecorator00

OrderService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace OrderLibrary
{
public class OrderService
{
public double GetPrice(double price)
{

if (price < 1000)
{
PriceInterface discountComponent = new DiscountComponent();
return discountComponent.CalculatePrice(price);
}
else
{
PriceInterface discountComponent = new DiscountComponent();
PriceInterface rebateDecorator = new RebateDecorator(discountComponent);

return rebateDecorator.CalculatePrice(price);
}
}
}
}

第 9 行

1
2
PriceInterface discountComponent = new DiscountComponent();
return discountComponent.CalculatePrice(price);

計算 全館八折

建立 DiscountComponent 物件,並執行 CalculatePrice() 計算。

值得注意 discountComponent 的型別為 PriceInterface

Q : PriceInterface 可用 var 取代嗎?

這裏 var 會將 discountComponent 推導為 DiscountComponent 型別,雖然不會執行錯誤,但 intention 不對。

因為這裡所要表現的就是 ConcreteComponentConcreteDecorator 都是相同的 ComponentInterface,也就是物件導向的 多型,但卻被 var 的 Type Inference 推導為 PriceInterface,與我們預期不合。

var 適合用在 primitive type,如 (intstring …),或具體的 class type,這些都能被 Type Inference 所正確推導,但不適合用在使用 interfaceabstract class 展現多型 時,就算執行不會錯,但 intention 與 語意 不佳

14 行

1
2
3
4
PriceInterface discountComponent = new DiscountComponent();
PriceInterface rebateDecorator = new RebateDecorator(discountComponent);

return rebateDecorator.CalculatePrice(price);

計算 全館八折 + 滿千送百

先建立 DiscountComponent,此為 全館八折

再建立 RebateDecorator,並將 DiscountComponent 傳入,此為以 全館八折 為基底,再附加 滿千送百 計算。

值得注意的是無論怎麼 decorate,最後都還是 PriceInterface 型別,因此可使用相同的 CalculatePrice() 繼續計算。

Decorator Pattern 可貴之處就在於沒破壞原本 interface,就能增加新功能,如可以將 component 與經過 decorator 裝飾過的物件都放在 List 內,因為 interface 都相同,所以型別也相同,可抽象化廣義是為 同型別 物件加以操作,符合 開放封閉原則 要求

Dependency Injection

很多 Design Pattern 都是以 constructor 作為傳入 初始值,在 Decorator Pattern 也不例外,直接將 component 或 decorator 傳入 ConcreteDecorator

但在目前 DI 的世界則面臨挑戰,Decorator Pattern 不再適合使用 constructor 傳入任何物件,而必須將 constructor 讓給 DI。

ecorator00

新增 DecoratorInterface描述 Decorator()AbstractDecorator 必須同時實現 PriceInterfaceDecoratorInterface 兩個 interface。

DecoratorInterface

ecorator01

DecoratorInterface.cs

1
2
3
4
5
6
7
namespace OrderLibrary
{
public interface DecoratorInterface
{
PriceInterface Decorate(PriceInterface component);
}
}

定義 DecoratorInterface 必須有 Decorate(),將來其他 decorator 必須遵守此 interface。

原本 Decorator Pattern 是透過 constructor 傳入原有物件,現在要改透過 Decorate()

值得注意的是 Decorate() 的 input 為 PriceInterface 型別,回傳也是 PriceInterface 型別,也就是經過 decorate 的物件,仍然有相同的 interface,不會改變型別,可順便做 fluent interface 操作。

Q : 為什麼不在 PriceInterface 新增 Decorate() 即可,還要新增 DecorateInterface

由於 DiscountComponentRebateDecorator 共用 PriceInterface,且 Decorate() 主要是為了 decorator 所用,加在 PriceInterface 會造成 DiscountComponentDecorate() 的空實作,這違反了 介面隔離原則 ,所以必須另外開新的 interface。

AbstractDecorator

ecorator01

AbstractDecorator.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace OrderLibrary
{
public abstract class AbstractDecorator: PriceInterface, DecoratorInterface
{
protected PriceInterface _component;

public PriceInterface Decorate(PriceInterface component)
{
_component = component;
return this;
}

public abstract double calculatePrice(double price);
}
}

第 3 行

1
public abstract class AbstractDecorator: PriceInterface, DecoratorInterface

AbstractDecorator 除了必須實作原有的 PriceInterface 外,為了解決 DI 問題,還必須同時實作 DecoratorInterface

第 5 行

1
2
3
4
5
6
7
protected PriceInterface _component;

public PriceInterface Decorate(PriceInterface component)
{

_component = component;
return this;
}

PriceInterface 物件由原本的 constructor 傳入,改透過 Decorate() 傳入,最後傳回 this,可順便做 fluent interface 操作。

RebateDecorator

ecorator01

RebateDecorator.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public class RebateDecorator: AbstractDecorator
{
public override double calculatePrice(double price)
{

return _component.calculatePrice(price) - 100;
}
}
}

因為已經改由 Decorate() 傳入物件,將原有的 constructor 刪除。

OrderService

ecorator01

OrderService.cs

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

namespace OrderLibrary
{
public class OrderService
{
public double GetPrice(double price)
{

if (price < 1000)
{
PriceInterface discountComponent = new DiscountComponent();
return discountComponent.calculatePrice(price);
}
else
{
PriceInterface discountComponent = new DiscountComponent();
var rebateDecorator = new RebateDecorator().Decorate(discountComponent);

return rebateDecorator.calculatePrice(price);
}
}
}
}

16 行

1
2
3
4
PriceInterface discountComponent = new DiscountComponent();
var rebateDecorator = new RebateDecorator().Decorate(discountComponent);

return rebateDecorator.calculatePrice(price);

原本 DiccountComponent 是由 RebateDecorator 的 constructor 傳入,現在改由 Decorate() 傳入。

Delegate

從 Decorator Pattern,我們看到了 OOP 幾個缺點:

  • 原本簡單的 if ... else 被拆成很多檔案,導致 class 爆炸
  • ComponentInterfaceDecoratorInterfaceAbstractDecorator 等新增的 class 與需求無關,算是因為使用 Decorator Pattern 所產生的額外 class
  • interface 雖然有制定 spec 與 compiler 編譯檢查的優點,但是只有一個 method 的 interface,是否有有開 interface 的需要?

雖然 PriceInterface 定義了 CalculatePrice(),但整個 interface 只有一個 method,顯然使用 interface 有殺雞用牛刀之嫌,此時可將 interface 退化成 delegate

ecorator01

PriceDelegate

ecorator01

PriceDelegate.cs

1
2
3
4
namespace OrderLibrary
{
public delegate double PriceDelegate(double price);
}

定義 PriceDelegate delegate,其 signature 為 double => double,也就是 input 為 double,return 為 double

實務上 Decorator Pattern 通常只有一個 method,所以就很適合將 interface 退化成 delegate

PriceComponent

ecorator01

PriceComponent.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public static class PriceComponent
{
public static double CalculateDiscountPrice(double price)
{

return price * 0.8;
}
}
}

DiscountComponent 退化成 PriceComponent,將 CalculatePrice() 改成 CalculateDiscountPrice()

將每個 ConcreteComponent 改用 function 表示,也由於當 pure function 用,此時使用 static 即可。

PriceDecorator

ecorator01

PriceDecorator.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public static class PriceDecorator
{
public static PriceDelegate CalculateRebatePrice(PriceDelegate originalFn)
{

return price => orginalFn(price) - 100;
}
}
}

將每個 decorator 以 function 表示,此時改用 static function 即可。

orginalFn 即為原本的 function,CalculateRebatePrice() 則為 Higher Order Function。

CalculateRebatePrice() 回傳的是 PriceDelegate,所以要 return Lambda。

一般來說,OOP 都建議不要使用 static,但這裡是例外,此時是將 class 當成 module 看待,static function 是當成 FP 的 pure function 使用,也就是這種 class 將沒有 field,也不使用 OOP 的 繼承組合

OrderService

ecorator01

OrderService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
namespace OrderLibrary
{
public class OrderService
{
public double GetPrice(double price)
{

if (price < 1000)
{
PriceDelegate calculateDiscountPrice = PriceComponent.CalculateDiscountPrice;
return calculateDiscountPrice(price);
}
else
{
PriceDelegate calculateDiscountPrice = PriceComponent.CalculateDiscountPrice;
PriceDelegate calculateRebatePrice = PriceDecorator.CalculateRebatePrice(calculateDiscountPrice);

return calculateRebatePrice(price);
}
}
}
}

14 行

1
2
3
4
PriceDelegate calculateDiscountPrice = PriceComponent.CalculateDiscountPrice;
PriceDelegate calculateRebatePrice = PriceDecorator.CalculateRebatePrice(calculateDiscountPrice);

return calculateRebatePrice(price);
  • 在 OOP 是 ConcreteComponentConcreteDecorator 的型別都是 ComponentInterface
  • 在 FP 化之後,CalculateDiscountPrice()CalculateRebatePrice() 的型別都是 PriceDelegate

將原來的 CalculateDiscountPrice 傳入 CalculateRebatePrice() 後,就相當於以 CalculateRebatePrice() 去 decorate 原來的 CalculateDiscountPrice()

我們可以發現 OOP 的 Decorator Pattern,對於 FP 本質來說只是 Higher Order Function 的應用;將 function 傳入 Higher Order Function,就相當於以 Higher Order Function 加以 decorate。

Func

雖然 PriceDelegate 定義 function 的 spec 的理念很不錯,若 delegate 只使用一遍,真的需要開一個檔案建立 delegate 嗎 ?

ecorator01

PriceDelegate 刪除。

PriceDecorator

ecorator01

PriceDecorator.cs

1
2
3
4
5
6
7
8
9
10
11
12
using System;

namespace OrderLibrary
{
public static class PriceDecorator
{
public static Func<double, double> CalculateRebatePrice(Func<double, double> fn)
{
return price => fn(price) - 100;
}
}
}

直接以 Func<double, double> 取代 PriceDelegate,其餘不變。

OrderService

ecorator01

OrderService.ts

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

namespace OrderLibrary
{
public class OrderService
{
public double GetPrice(double price)
{

if (price < 1000)
{
Func<double, double> calculateDiscountPrice = PriceComponent.CalculateDiscountPrice;
return calculateDiscountPrice(price);
}
else
{
Func<double, double> calculateDiscountPrice = PriceComponent.CalculateDiscountPrice;
Func<double, double> calculateRebatePrice = PriceDecorator.CalculateRebatePrice(calculateDiscountPrice);

return calculateRebatePrice(price);
}
}
}
}

直接以 Func<double, double> 取代 PriceDelegate,其餘不變。

我們可以發現 FP 化的 Decorator Pattern,基本上已經沒有 interface,所有的 ConcreteComponent 都簡化成 PriceComponent;而所有的 ConcreteComponent 都簡化成 PriceDecorator,而且也沒用到什麼高深的技巧,就只有 FP 的基本招式:Higher Order Function。

也就是 function 的組合遠比 object 組合容易,因此並不需要動用到 interface 與 abstract class 等複雜的機制。

Summary


以 SOLID 角度重新審視經過 FP 二次重構後的 Decorator Pattern:

  • 單一職責原則:將所有的 decorator function 統一整理在 PriceDecorator,符合 SRP
  • 開放封閉原則:將來若有新的 decorator function,只要統一加在 PriceDecorator 即可,符合 OCP
  • 里氏替換原則:因為沒用到繼承,所以沒有違反 LSP 問題
  • 最小知識原則:decorator function 並沒有暴露到 client,符合 LKP
  • 介面隔離原則:因為從 interface 退化成 delegate,FP 天生符合 ISP
  • 依賴反轉原則:service 與 decorator 之間的耦合僅限於 delegateFunc<T>,而不是直接耦合與特定 function,符合 DIP

Conclusion


  • Decorator 本質就是 object 的組合,但 object 的組合沒 function 簡單直覺,所以需要搭配 interface 與 abstract class,但若純 function,只要使用 Higher Order Function 即可簡單完成
  • FP 的出現,讓 Design Pattern 的實踐方式,不再只有 OOP 一途,可視實際需求決定該使用 OOP 或 FP
  • OOP 讓我們以 抽象設計 的角度看系統,但 FP 讓我們以 簡化設計 的角度看系統,實務上建議以 OOP 做第一階段的重構,再輔以 FP 做第二階段的重構,可解決 OOP 容易 Over Design 的問題

Sample Code


完整的範例可以在我的 GitHub 上找到

2018-03-28