面向過程,面向?qū)ο?,函?shù)式對(duì)同一個(gè)問題的思考方式
我之所以對(duì)函數(shù)式代碼感興趣是因?yàn)楹瘮?shù)式代碼富有表現(xiàn)力,可以使用簡(jiǎn)短、緊湊的代碼完成工作,同時(shí)能對(duì)特定的問題給出優(yōu)雅的解決方案?,F(xiàn)代的編程語言不約而同的朝著面向?qū)ο?、函?shù)式、動(dòng)態(tài)、解釋執(zhí)行的方向發(fā)展,例如Ruby,Swift。而另一些語言則更加強(qiáng)調(diào)函數(shù)式編程,如F#,Scala,這種語言有著強(qiáng)大的類型推斷系統(tǒng),編寫的代碼潔程度則令人嘆為觀止。
在F#編寫一個(gè)兩個(gè)數(shù)相加的函數(shù),在F# Interactive中輸入:
1
|
let add num1 num2=num1*num2;;
|
F# Interactive為我們推斷了這個(gè)函數(shù)類型:val add : num1:int -> num2:int -> int,表示add有兩個(gè)int類型的參數(shù)得到了1個(gè)int類型。
函數(shù)當(dāng)作參數(shù):
1
2
3
4
5
6
|
//C#
private int Twice(int input,Func<int,int> f)
{
return f(f(input));
}
var result = Twice(2, n => n*n);
|
使用F#則只需要非常簡(jiǎn)潔的一個(gè)函數(shù)聲明:
1
2
3
4
5
6
|
> let twice (input:int) f=f(f(input));;
val twice : input:int -> f:(int -> int) -> int
> twice 2 (fun n->n*n);;
val it : int = 16
|
val twice : input:int -> f:(int -> int) –> int 這句話則是F# Interactive給出的推斷:twice函數(shù)需要一個(gè)int參數(shù)和一個(gè)(int->int)的函數(shù)作為參數(shù),返回一個(gè)int.
這兩個(gè)例子僅僅是熱身,并不是本篇博客的重點(diǎn),所以你覺得前兩個(gè)例子很無聊或者沒太看明白請(qǐng)繼續(xù)看下面的總結(jié)。
場(chǎng)景:某種活動(dòng)會(huì)有一個(gè)日程安排(Schedule),日程安排有3中類型,只舉辦一次(Once),每天一次(Daily),每周一次(Weekly)?;顒?dòng)會(huì)根據(jù)日程安排(Schedule)的類型不同具有不同的宣傳內(nèi)容,不同的延期舉行策略。
你對(duì)于這樣的場(chǎng)景會(huì)有怎么樣的思考呢?
一、面向過程類型的編碼方式
面向過程類型的編碼是需求的直譯過程,代碼會(huì)寫成這樣:
1.顯示活動(dòng)的宣傳內(nèi)容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public void ShowScheduleDescriptions()
{
switch (ScheduleType)
{
case ScheduleType.Once:
Console.WriteLine("this is once activity");
break;
case ScheduleType.Daily:
Console.WriteLine("this is daily activity");
break;
case ScheduleType.Weekly:
Console.WriteLine("this is weekly activity");
break;
default:
throw new InvalidOperationException("unsupported schedule");
}
}
|
這樣的代碼初次看起來沒什么問題,實(shí)際存在兩個(gè)危險(xiǎn)信號(hào):
- 違反開放封閉(OCP)原則,如果有一天需要加入一種Monthly類型,無疑需要修改這個(gè)方法;
- 這樣的代碼風(fēng)格會(huì)讓接下來的開發(fā)者不假思索的進(jìn)行延續(xù),比方說需要根據(jù)不同的活動(dòng)類型延期活動(dòng);
2. 延期活動(dòng):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public void DelaySchedule()
{
switch (ScheduleType)
{
case ScheduleType.Once:
Console.WriteLine("Delay one hour");
break;
case ScheduleType.Daily:
Console.WriteLine("Delay one day");
break;
case ScheduleType.Weekly:
Console.WriteLine("Delay one week");
break;
default:
throw new InvalidOperationException("unsupported schedule");
}
}
|
這樣的代格違反了DRY原則,相同的代碼框架卻無法重用。
二、面向?qū)ο蟮木幋a方式
對(duì)于一個(gè)有經(jīng)驗(yàn)的OO開發(fā)者,一旦看到switch,if(type=typeof(…))之類的代碼馬上會(huì)提高警惕,是不是有一些抽象類型沒有被找出來?在這個(gè)例子中則會(huì)找出下面的抽象:
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
37
38
|
public class Schedule
{
public virtual void ShowShowScheduleDescriptions()
{
}
public virtual void DelaySchedule()
{
}
}
public class OnceSchedule : Schedule
{
public override void ShowShowScheduleDescriptions()
{
Console.WriteLine("this is once activity");
}
public override void DelaySchedule()
{
Console.WriteLine("Delay one hour");
}
}
public class DailySchedule : Schedule
{
public override void ShowShowScheduleDescriptions()
{
Console.WriteLine("this is daily activity");
}
public override void DelaySchedule()
{
Console.WriteLine("Delay daily day");
}
}
//... other schedule
|
這樣的代碼很好的解決了面向過程代碼的兩個(gè)問題,看起來更加具有擴(kuò)展性,隨著新類型的Schedule引入,舊的代碼完全不用改動(dòng)。
當(dāng)然事情也不是絕對(duì)的,什么情況下需要改動(dòng)舊代碼呢?當(dāng)需要擴(kuò)展Schedule的行為的時(shí)候,例如需求升級(jí),不同的Schedule具有不同的舉辦方式,我們不得不在每種Schedule中加入一個(gè) void Hold()方法。
三、函數(shù)式解決方案
函數(shù)式語言則使用可區(qū)分聯(lián)合和模式匹配來處理此類問題。
定義一個(gè)Schedule可區(qū)分聯(lián)合:
1
2
3
4
|
type Schedule=
| Once of DateTime
| Daily of DateTime*int
| Weekly of DateTime*int
|
這個(gè)類型既說明了Schedule有三個(gè)不同的類型,同時(shí)定義了三種類型分別具有的數(shù)據(jù)結(jié)構(gòu)。像是Enum和類的綜合體,但是又顯得特別精致。
1.顯示活動(dòng)的宣傳內(nèi)容,使用了模式匹配:
1
2
3
4
5
|
let ShowShowScheduleDescriptions schedule=
match schedule with
| Once(DateTime)-> printfn "this is once activity"
| Daily(DateTime,int)->printfn "this is daily activity"
| Weekly(DateTime,int)->printfn "this is weekly activity"
|
這個(gè)方法類似于switch…case,但是通過匹配可區(qū)分聯(lián)合來實(shí)現(xiàn),而不是通過一個(gè)顯示的Enum來實(shí)現(xiàn)。
2. 延期活動(dòng):
1
2
3
4
5
|
let DelaySchedule schedule=
match schedule with
| Once(DateTime)-> printfn "Delay one hour"
| Daily(DateTime,int)->printfn "Delay one day"
| Weekly(DateTime,int)->printfn "Delay one week"
|
函數(shù)式編程的解決方案認(rèn)為可以很方便的添加新的行為,例如增加新的行為:Hold()。通過定義可區(qū)分聯(lián)合和模式匹配來完成編碼,整個(gè)解決方案像是面向過程和面向?qū)ο蟮囊环N結(jié)合體,但是側(cè)重點(diǎn)不同,實(shí)現(xiàn)的代碼也更加精致。