在平時(shí)的iOS開發(fā)中,我們大部分還是依靠著最早的MVC的思想開發(fā)著咳燕,最多也是進(jìn)行了部分變種季蚂,比如MVP茫船,MVVM琅束,這些都是完全的面向?qū)ο蟮乃枷耄谔幚硪恍┖唵蔚膱鼍暗臅r(shí)候還是非常有效的算谈,但是在某些場合狰闪,我們總會(huì)覺得難以解耦,難以進(jìn)行模塊化拆分濒生,這是由于目前大部分架構(gòu)中,在業(yè)務(wù)邏輯部分依然是過程性的幔欧,整個(gè)過程中還是需要依賴大量的其他模塊罪治。
這次我們來看看函數(shù)式編程的思想,以另一種角度來看問題是否能夠去解決部分問題礁蔗。
概念
函數(shù)式編程對于我們這種熟悉了面向?qū)ο蟮娜藖碚f觉义,是非常別扭和難以理解的。首先我們需要了解幾個(gè)概念浴井,這里不討論嚴(yán)格的概念晒骇,可以說這是我結(jié)合iOS開發(fā)的部分特性所做的一些理解。
函數(shù)是一等公民
這是函數(shù)式編程中最重要的思想基礎(chǔ)磺浙。萬物都是對象洪囤,包括函數(shù)。這個(gè)思想的引入撕氧,讓我們可以將函數(shù)作為參數(shù)瘤缩、返回值這一類原本對象才可以做的事情。在objc中的block
伦泥,swift中的closure
都可以認(rèn)為是函數(shù)式編程的基礎(chǔ)剥啤。
高階函數(shù)
參數(shù)或者返回值中存在函數(shù)對象的,都認(rèn)為是高階函數(shù)不脯「樱可以說高階函數(shù)是函數(shù)式編程中的連接器,他同樣也可以實(shí)現(xiàn)比如函數(shù)修飾器
這樣的功能防楷。
純函數(shù)
沒有副作用的函數(shù)牺丙,簡單的說就是給以相同的輸入,必定有相同的輸出复局。這也是函數(shù)式編程思想中非常重要的一個(gè)點(diǎn)赘被,因?yàn)闆]有副作用可以讓我們的設(shè)計(jì)變得更加簡單。
monad
關(guān)于monad這個(gè)概念肖揣,很多人都有不同的理解民假,但基本上都是把純函數(shù)連接起來的一種設(shè)計(jì)模式,同時(shí)這也是純的龙优。這個(gè)概念在業(yè)務(wù)開發(fā)中基本上不太可能實(shí)現(xiàn)羊异,但是在業(yè)務(wù)的部分場景還是可以做到的事秀。
優(yōu)勢
由于函數(shù)式編程中要求大量的純函數(shù),那么我們就可以很容易的進(jìn)行單元測試野舶。
同時(shí)在多線程編程中易迹,由于其獨(dú)立性和無副作用,我們也不需要思考數(shù)據(jù)競爭平道,死鎖等這類問題睹欲。也能更靈活的進(jìn)行功能拆分和執(zhí)行分配。
函數(shù)式編程是一套比較全面的編程思想一屋,對于我們面向?qū)ο蟊容^熟悉的人來說窘疮,有些地方確實(shí)會(huì)比較奇怪。雖然說我們也可以用面向?qū)ο髞韺?shí)現(xiàn)類似的功能冀墨,但是函數(shù)式編程會(huì)更加簡單和直接闸衫。
框架
函數(shù)式編程是專門為了計(jì)算這類場景而生的,目前由于腳本語言的興盛诽嘉,這類思想也同時(shí)融入了很多面向?qū)ο笳Z言中蔚出。
promise
promise可以說是我最早接觸到的函數(shù)式編程相關(guān)框架。promise的初衷是為了解決異步callback的多次調(diào)用而開發(fā)的虫腋。
step1(function (err) {
if (err) print(err)
else step2(function (err) {
if (err) print(err)
else step3(function (err) {
// go on ...
})
})
})
那么通過promise骄酗,我們可以改寫為:
step1()
.then(step2)
.then(step3)
.then(/* go on ... */)
.catch(print)
ReactiveX
同樣類似的ReactiveX
也具有相同的效果,不過RX把注意點(diǎn)更加放在了對數(shù)據(jù)的處理上面悦冀,這也就正式的把我們從邏輯的思維中脫離開來酥筝,從而關(guān)注數(shù)據(jù)的流動(dòng)上。后面會(huì)來討論這種思維的變化給我們帶來了什么雏门。
RX中signal
, observer
, observable
, subject
主要負(fù)責(zé)數(shù)據(jù)的流動(dòng)嘿歌,scheduler
負(fù)責(zé)執(zhí)行線程相關(guān)操作。
Redux
參考ReSwift
可以說這是我接觸到的第一個(gè)以函數(shù)式編程為基礎(chǔ)的茁影,專注于數(shù)據(jù)流動(dòng)的框架宙帝。
關(guān)于這個(gè)框架的詳細(xì)介紹,我相信有很多人都比我更加了解募闲,也比我講的好步脓,所以這里就不做介紹了。
數(shù)據(jù)流動(dòng)
一個(gè)請求的例子
在我們平時(shí)的開發(fā)過程中浩螺,基本上關(guān)注的是業(yè)務(wù)的流程實(shí)現(xiàn)靴患,按照邏輯的順序進(jìn)行思考和設(shè)計(jì),如果我們按照標(biāo)準(zhǔn)的面向?qū)ο髞碓O(shè)計(jì)一個(gè)請求要出,那么很有可能是這樣的鸳君。
- (void)request {
Request *req = [Request new]
req.delegate = self;
[req start];
}
- (void)requestComplete:(Request *)req {
Request *req = [Request new];
[req startWithComplete:^(Request *req) {
if (req.error == nil) {
NSDictionary *json = [JSON parse:req.data];
ClassA *a = [ClassA from:json];
// ...
}
else {
}
}]
}
這是一個(gè)非常典型的設(shè)計(jì)方式,當(dāng)我們只有一個(gè)請求的時(shí)候這樣做也非常簡單患蹂,但是一旦擁有多個(gè)請求的時(shí)候或颊,事情就變得很復(fù)雜砸紊。那么如果我們使用函數(shù)式來改寫,就比如AF所做的那樣囱挑。
- (void)request {
Request *req = [Request new];
[req startWithComplete:^(Request *req) {
if (req.error == nil) {
NSDictionary *json = [JSON parse:req.data];
ClassA *a = [ClassA from:json];
// ...
}
else {
}
}]
}
這樣在出現(xiàn)多個(gè)請求的時(shí)候醉顽,回調(diào)也能保證其唯一性。但是這個(gè)依然還是有一些問題平挑。
- 數(shù)據(jù)解析和model化等操作依然需要在回調(diào)時(shí)自己去調(diào)用游添,如果將這些放入Request中,又難以保持低耦合性通熄。
- 自動(dòng)化測試?yán)щy唆涝,因?yàn)檎埱蠓祷睾蟊厝皇墙又晥D的更新或者業(yè)務(wù)邏輯的執(zhí)行,自動(dòng)化測試一旦涉及這些會(huì)顯得很臃腫棠隐。
- 當(dāng)多個(gè)請求在需要并發(fā),而回調(diào)需要保持同步的時(shí)候檐嚣。
- 流量控制只能依賴于網(wǎng)絡(luò)層的來控制助泽,同時(shí)線程控制依然需要手動(dòng)控制,可能GCD會(huì)讓這類操作稍微簡單一點(diǎn)嚎京。
那么如果我們只關(guān)注數(shù)據(jù)來改寫呢嗡贺,可能會(huì)是這樣的,為了更適合函數(shù)式編程鞍帝,這里采用js來改寫:
Request().then(data => {
let json = JOSN.parse(data)
let a = ClassA.from(json)
// ...
}).catch(err => {
})
那么我們來看看如何解決上述的幾個(gè)問題诫睬。
為了保證低耦合性,我們不希望將解析等操作放入網(wǎng)絡(luò)層中帕涌,但也不希望在業(yè)務(wù)層過多的出現(xiàn)這類代碼摄凡。
Request().then(JSON.parse).then(ClassA.from).then(a => {
}).catch(err => {
})
而其中分離出來的中間過程,由于是純函數(shù)蚓曼,是可以完全的單元測試的亲澡,這也就增強(qiáng)了單元測試的粒度。如果你說客戶端業(yè)務(wù)代碼的單元測試是否有這個(gè)必要纫版,那么我覺得很多時(shí)候沒有這個(gè)時(shí)間和必要床绪,但是當(dāng)我們做一些基礎(chǔ)庫,或者SDK時(shí)其弊,涉及的面非常廣時(shí)癞己,我們不得不去考慮單元測試的必要性,而且覆蓋率越高越好梭伐。
如果要實(shí)現(xiàn)并發(fā)這種呢痹雅?按照數(shù)據(jù)的角度去思考其實(shí)就非常的簡單。
let req1 = Request().then(JSON.parse).then(ClassA.from)
let req2 = Reqeust().then(JSON.parse).then(ClassB.from)
Promise.all([req1, req2]).then(ab => {
// reload data
}).catch(err => {
})
由于中間過程是純函數(shù)糊识,那么我們要將他們放入子線程也就非常的簡單练慕,甚至RX自己就提供了這樣的接口惰匙。
函數(shù)修飾器
這是某些語言的特性,所以拿出來簡單說說铃将。
一個(gè)函數(shù)修飾器的參數(shù)中必然至少有一個(gè)是函數(shù)项鬼,返回的也依然是函數(shù)。這個(gè)比較難以理解劲阎,我們來舉一個(gè)例子:
function needLogin(f) {
if (login)
return f
else
return function () { print('error') }
}
那么我們需要為某個(gè)函數(shù)增加這個(gè)檢查的時(shí)候绘盟,只需要增加一層修飾即可:
let NewRequest = needLogin(Request)
NewRequest().then(...)
python:
@needLogin
def Request()
# ...
pass
這種方法的優(yōu)勢在于,我們不需要在請求里去增加這種業(yè)務(wù)代碼悯仙,同時(shí)也不需要在業(yè)務(wù)層里進(jìn)行復(fù)雜的校驗(yàn)龄毡,同時(shí)這樣表示也會(huì)更加接近自然語言。
最后
這里簡單介紹了函數(shù)式編程思想锡垄,和我們平時(shí)開發(fā)所使用的方法都不一致沦零,也沒有哪一種方法更好的定論。但是這一種以數(shù)據(jù)作為主體的方法卻是非常值得我們思考的货岭,因?yàn)榭梢哉J(rèn)為數(shù)據(jù)是不會(huì)存在高耦合這樣的問題路操,這也給我們提供了一種新的解決問題的思路。
這是一種新的思考方式千贯,可以說這里的一些內(nèi)容是無法讓你真正改變的屯仗,很多還是需要靠自身的理解和感悟,才能將其應(yīng)用于平時(shí)的開發(fā)之中搔谴。