無論使用 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 原則,且更為靈活