無論使用 TDD 或 Design Pattern,最後一定會經歷 Refactoring 階段,處理程式碼共用部分,避免違反 DRY 原則。
在傳統 OOP,我們會使用 Pull Member Up 將共用 method 抽到 abstract class;但若使用 FP,我們則有新的武器:將共用部分抽成 Higher Order Function,將不共用部分以 Lambda 傳入。
Version
macOS High Sierra 10.13.3
.NET Core 2.0.7
C# 7.2
User Story
Apple.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
| using System.Collections.Generic; using System.Linq; using ClassLibrary.Apple; using ClassLibrary.Interfaces;
namespace ClassLibrary.Combo { public class Apple : IPrice { private double _discount = 0.9; private readonly List<IPrice> _products;
public Apple() { _products = new List<IPrice> { new MacBookPro(), new PadAir(), new AppleWatch() }; } public double GetPrice() { return _discount * _products.Sum(product => product.GetPrice()); } } }
|
Nintendo.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
| using System.Collections.Generic; using System.Linq; using ClassLibrary.Interfaces; using ClassLibrary.Nintendo;
namespace ClassLibrary.Combo { public class Nintendo : IPrice { private double _minus = 1000; private readonly List<IPrice> _products;
public Nintendo() { _products = new List<IPrice> { new Switch(), new Zelda() }; } public double GetPrice() { return _products.Sum(product => product.GetPrice()) - _minus; } } }
|
Apple.cs 與 Nintendo.cs 都實踐相同的 IPrice,且我們已經看到有兩處程式碼非常相近:
1
| private readonly List<IPrice> _products;
|
都具有 _products List。
1 2 3 4
| public double GetPrice() { return _discount * _products.Sum(product => product.GetPrice()); }
|
與
1 2 3 4
| public double GetPrice() { return _products.Sum(product => product.GetPrice()) - _minus; }
|
也非常相近,都共用 Products.Sum(product => product.GetPrice())。
OOP Refactoring
ComboBase.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| using System.Collections.Generic; using System.Linq; using ClassLibrary.Interfaces;
namespace ClassLibrary.Combo { public abstract class ComboBase : IPrice { protected List<IPrice> Products; public virtual double GetPrice() { return Products.Sum(product => product.GetPrice()); } } }
|
抽出 ComboBase abstract class,將重複部分全放在 parent class。
但無論怎麼抽 class,一樣都實踐 IPrice interface,因此對 client 沒有影響,符合 開放封閉原則。
Apple.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
| using System.Collections.Generic; using System.Linq; using ClassLibrary.Interfaces; using ClassLibrary.Single.Apple;
namespace ClassLibrary.Combo { public class Apple : ComboBase { private double _discount = 0.9;
public Apple() { Products = new List<IPrice> { new MacBookPro(), new PadAir(), new AppleWatch() }; } public override double GetPrice() { return _discount * base.GetPrice(); } } }
|
第 8 行
1
| public class Apple : ComboBase
|
從原本實踐 IPrice interface,改繼承 ComboBase。
由於 ComboBase 也是實踐 IPrice,因此對 client 沒有影響,符合 開放封閉原則。
22 行
1 2 3 4
| public override double GetPrice() { return _discount * base.GetPrice(); }
|
由於 ComboBase.GetPrice() 已經實作 Products.Sum(product => product.GetPrice()),因此使用 base.GetPrice() 呼叫 parent class 的 GetPrice()。
Nintendo.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
| using System.Collections.Generic; using System.Linq; using ClassLibrary.Interfaces; using ClassLibrary.Single.Nintendo;
namespace ClassLibrary.Combo { public class Nintendo : ComboBase { private double _minus = 1000;
public Nintendo() { Products = new List<IPrice> { new Switch(), new Zelda() }; } public override double GetPrice() { return base.GetPrice() - _minus; } } }
|
21 行
1 2 3 4
| public override double GetPrice() { return base.GetPrice() - _minus; }
|
一樣使用 base.GetPrice() 呼叫 parent class 的 GetPrice(),只是在此是 - _minus 。
這就是 OOP 典型的手法,將共用部分 Pull Member Up 到 parent class,並將 method 開成 virtual,再有 child class 去 override method,且使用 base 去呼叫 parent class 的 method
FP Refactoring
在 FP 由於有了 Higher Order Function,解決 DRY 有了新的手法:
將共用部分抽成 Higher Order Function,將不共用部分以 Lambda 傳入
ComboBase.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| using System; using System.Collections.Generic; using System.Linq; using ClassLibrary.Interfaces;
namespace ClassLibrary.Combo { public abstract class ComboBase : IPrice { protected List<IPrice> Products; protected double GetSumOfPrice(Func<double, double> action) { return action(Products.Sum(product => product.GetPrice())); }
public abstract double GetPrice(); } }
|
12 行
1 2 3 4
| protected double GetSumOfPrice(Func<double, double> action) { return action(Products.Sum(product => product.GetPrice())); }
|
將共用部分抽成 GetSumOfPrice(),action 參數部分則傳入不共用部分。
將共用的 Products.Sum(product => product.GetPrice()) 傳入 action() 的參數。
17 行
1
| public abstract double GetPrice();
|
原本 GetPrice() 使用 abstract 即可,由 child class 負責實作。
Apple.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
| using System.Collections.Generic; using ClassLibrary.Interfaces; using ClassLibrary.Single.Apple;
namespace ClassLibrary.Combo { public class Apple : ComboBase { private double _discount = 0.9;
public Apple() { Products = new List<IPrice> { new MacBookPro(), new PadAir(), new AppleWatch() }; } public override double GetPrice() { return GetSumOfPrice(sum => _discount * sum); } } }
|
21 行
1 2 3 4
| public override double GetPrice() { return GetSumOfPrice(sum => _discount * sum); }
|
呼叫 parent class 的 GetSumOfPrice() ,回傳即為 lambda 的第一個參數 sum ,既然拿到 sum,我們就能在 => 之後做任何我們想做的運算。
Nintendo.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
| using System.Collections.Generic; using ClassLibrary.Interfaces; using ClassLibrary.Single.Nintendo;
namespace ClassLibrary.Combo { public class Nintendo : ComboBase { private double _minus = 1000;
public Nintendo() { Products = new List<IPrice> { new Switch(), new Zelda() }; } public override double GetPrice() { return GetSumOfPrice(sum => sum - _minus); } } }
|
一樣呼叫 parent class 的 GetSumOfPrice() ,回傳即為 lambda 的第一個參數 sum ,既然拿到 sum,我們就能在 => 之後做任何我們想做的運算。
Summary
Q : 兩種抽共用的方式都可行,有什麼差別呢 ?
若以結果論,的確 OOP 與 FP 方法都可行,但 FP 方式語意較佳。
OOP
1 2 3 4
| public virtual double GetPrice() { return Products.Sum(product => product.GetPrice()); }
|
OOP 為了要使用 virtual 與 override ,使用同一個 method 名稱,硬將 Products.Sum(product => product.GetPrice()) 抽到 parent class 的 GetPrice()。
實務上常會發現,為了抽共用,常將與 method 名稱不符的 code 抽到 parent class。
如 Products.Sum(product => product.GetPrice()) 與 GetPrice() 並不相符,應該要取名 GetSumOfPrice() 較為恰當。
此外,將 Products.Sum(product => product.GetPrice()) 放到 parent class 的 GetPrice(),會使得 code 被綁在 IPrice 繼承體系,這段 code 將來幾乎無法再重構。
1 2 3 4
| public override double GetPrice() { return _discount * base.GetPrice(); }
|
Child class 無法藉由 base.GetPrice() 得知語意,一定得 trace 進 parent class 才知道 base.GetPrice() 到底在幹什麼。
FP
1 2 3 4
| protected double GetSumOfPrice(Func<double, double> action) { return action(Products.Sum(product => product.GetPrice())); }
|
GetSumOfPrice() 與 Products.Sum(product => product.GetPrice()) 名實相符。
且 GetSumOfPrice() 並非 IPrice 繼承體系的一員,將來很容易將 GetSumOfPrice() 重構到其他 class,甚至重構到 static helper class,非常靈活。
1 2 3 4
| public override double GetPrice() { return GetSumOfPrice(sum => _discount * sum); }
|
Child class 也非常清楚看到 GetSumOfPrice(),且回傳值就是 sum,語意非常清楚,不必 trace 到 parent class。
Conclusion
Pull Member Up 是典型 OOP 的重構手法,但藉由 FP 的 Higher Order Function,一樣可以實踐 DRY 原則,且更為靈活