使用 FP 方式處理共用

無論使用 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.csNintendo.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 為了要使用 virtualoverride ,使用同一個 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 原則,且更為靈活
2018-05-04