C# 也能輕鬆抽出 Higher Order Function

Higher Order Function 是 JavaScript 或 FP 語言常見的語言機制,事實上 C# 也支援某種程度的 FP,如 Func、 Predicate 與 Action。

透過 Rider,我們也能將既有 method 重構出具 FP 風格的 Higher Order Function。

Version


macOS High Sierra 10.13.4
.NET Core 2.1
Rider 2018.1.2

Func


重構前

OrderService.cs

hof000

price 實務上會從 repository 而來,為了簡化起見,暫時 hardcore price100

因為 全館八折,所以 price 一律打八折。

Program.cs

hof001

Client 呼叫 OrderService.CalculatePrice()

重構後

OrderService.cs

hof002

目前 全館八折price * 0.8 是寫死的,但考量折扣的算法常常會變,寫死並不是一個好方法,因此想將 price * 0.8 整個 expression 抽成 parameter。

  1. 選擇 price * 0.8
  2. 熱鍵 ⌃ + T ,呼叫出 Refactor This
  3. 選擇 Introduce Parameter

hof003

  1. 將下方的 Select parameters to enlambda 的 parameter 打勾,parameter 型別將為 Func<double, double>
  2. 輸入 parameter 名稱為 discountStrategy

hof004

  1. 原來的 price * 0.8 重構成 discountStrtegy(price)
  2. discountStrategy parameter 型別為 Func<double, double>

Program.cs

hof005

  1. 原來的 price * 0.8 重構成 price => price * 0.8 以 Lambda 形式傳入

Q : 這樣重構有什麼好處呢 ?

若將來折扣規則改成 全館折價 100,只要傳入 price => price - 100 即可,OrderService 完全不用修改,符合 開放封閉原則 要求

Predicate


重構前

OrderService.cs

hof006

price 實務上會從 repository 而來,為了簡化起見,暫時 hardcore price100

因為 滿千送百,所以 price 超過 1000.0 會自動折價 100.0

Program.cs

hof007

Client 呼叫 OrderService.CalculatePrice()

重構後

OrderService.cs

hof008

目前 滿千送百price > 1000.0 是寫死的,但考量折扣的算法常常會變,寫死並不是一個好方法,因此想將 price > 1000.0 整個 expression 抽成 parameter。

  1. 選擇 price > 1000.0
  2. 熱鍵 ⌃ + T ,呼叫出 Refactor This
  3. 選擇 Introduce Parameter

hof011

  1. 將下方的 Select parameters to enlambda 的 parameter 打勾,parameter 型別將為 Func<double, bool>
  2. 輸入 parameter 名稱為 discountPredicate

hof009

  1. 原來的 price > 1000.0 重構成 discountPredicate(price)
  2. discountStrategy parameter 型別為 Func<double, bool>

理想上當然是抽成 Predicate<double>,但目前 Rider 只能抽成 Func<double, bool>,但因為兩個型別等價,所以不滿意但可以接受

hof010

  1. 原來的 price > 1000.0 重構成 price => price > 1000.0 以 Lambda 形式傳入

Q : 這樣重構有什麼好處呢 ?

若將來折扣規則改成 只有 1000 ~ 2000 之間有折扣,只要傳入 price => price > 1000.0 && price < 2000.0 即可,OrderService 完全不用修改,符合 開放封閉原則 要求

Action


重構前

OrderService.cs

hof012

price 實務上會從 repository 而來,為了簡化起見,暫時 hardcore price2000

因為過年活動 全館折價 100,所以 price - 100

因為過年,所以特別顯示 Happy New Year!!

Program.cs

hof013

Client 呼叫 OrderService.CalculatePrice()

重構後

OrderService.cs

hof014

目前 Happy New Year!! 是寫死的,但考量不同節慶顯示不同訊息,甚至不同顯示方式 (呼叫其他 service ?),寫死並不是一個好方法,因此想將 Console.WriteLine('Happy New Year!!') 整個 statement 抽成 parameter。

  1. 選擇 Console.WriteLine('Happy New Year!!')
  2. 熱鍵 ⌃ + T ,呼叫出 Refactor This
  3. 目前並無 Introduce Parameter 可選,只剩下 Extract MethodIntroduce Variable,試試看先 Introduce Variable 後再 Introduce Parameter

hof015

  1. 若選擇 Introduce Variable,則顯示 Expression type is void ,無法繼續重構

hof016

  1. 這裡必須自己寫 code,先自行宣告 Action 型別變數,將 Console.WriteLine('Happy New Year!!') 包在 Lambda 內,最後再用 Action.Invoke() 執行 action

hof017

  1. 選擇 action 變數
  2. 熱鍵 ⌃ + T ,呼叫出 Refactor This,選擇 Introduce Parameter

hof019

  1. 已經抓出 parameter 型別為 Action
  2. 輸入 parameter 名稱

hof020

  1. 原來的 Action action = () => { Console.WriteLine("Happy New Year!!"); }; 重構成 action parameter
  2. action parameter 型別為 Action

hof021

  1. 原來的 Action action = () => { Console.WriteLine("Happy New Year!!"); }; 重構成 () => { Console.WriteLine("Happy New Year!!"); } 以 Lambda 形式傳入

Q : 這樣重構有什麼好處呢 ?

若將來顯示訊息改變,甚至呼叫其他 service 顯示,只要傳入 ()=> {} 即可,OrderService 完全不用修改,符合 開放封閉原則 要求

Conclusion


  • 隨著 C# 支援 FP 越來越完整,Extract Higher Order Function 的需求會越來越多,而 Extract Method 卻無法抽出 Higher Order Function,必須靠 Introduce Parameter,才能抽出 Func、Predicate 與 Action
  • 目前 Rider 無法直接抽出 Predicate,所幸 Predicate 也都可以用 Func 表示,雖不滿意但尚可接受
  • 目前 Rider 無法直接抽出 Action,必須搭配一些小技巧,才能順利抽出 Action

Sample Code


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

2018-06-12