本文轉(zhuǎn)自:http://www.cocoachina.com/swift/20150803/12881.html
原作者:Hector Matos
原發(fā)表日期:2015-07-13
<b>本文的代碼示例改用swift3.0际邻,原文使用的是swift2.0</b>
<h2>Swift的核心</h2>
我們可以通過等式的傳遞性來理解swift:
Swift 的核心是面向協(xié)議的編程。
面向協(xié)議的編程的核心是抽象和簡化芍阎。
所有swift的核心就是抽象和簡化
<small>
你可能對我的標(biāo)題感到詫異世曾。我并不是說子類沒有價(jià)值,尤其在使用單一繼承的情況下谴咸,類和子類當(dāng)然是強(qiáng)有力的工具轮听。然而我想說的是骗露,<b>iOS日常開發(fā)的問題是對類和繼承的過度使用</b>。作為面向?qū)ο蟮木幊陶?object-oriented programmer血巍,后面統(tǒng)一換為OOP編程者)我們總是會(huì)自然的去傾向于引用類型和類去解決問題萧锉,但是我個(gè)人還是認(rèn)為應(yīng)該反過來,傾向于<b>用值類型來代替引用類型</b>述寡。我們還是要去寫模塊化的驹暑,可伸縮的并且可重用的代碼,這一點(diǎn)不會(huì)變辨赐。<b>swift 中有強(qiáng)大的值類型就可以幫我們實(shí)現(xiàn)模塊化這一目的优俘,且不會(huì)對引用類型有過度依賴。</b>我認(rèn)為不僅面向協(xié)議的編程 (protocol oriented programming掀序,后面統(tǒng)一為POP) 可以幫我們實(shí)現(xiàn)這點(diǎn)帆焕,另外兩種類型也可以,且都具有抽象和簡化的核心思想不恭,這兩種分別是:面向值的編程和 函數(shù)式編程
先說清楚叶雹,我絕不是這種編程類型 (POP,面向值的編程 和 函數(shù)式編程) 的專家换吧。和你一樣折晦,從MMM時(shí)代(收到內(nèi)存管理)開始我就是一個(gè)OOP編程者。通過自學(xué)沾瓦,從開始就很重視值的抽象和簡化思想满着。我都沒有意識到自己是一個(gè)傾向于函數(shù)式編程的OOP編程者,而且很多時(shí)候都是在用面向值的編程和POP的思路贯莺。這可能是我為什么在第一天就興高采烈的加入了swift的浪潮之中的原因风喇。在WWDC的一整周里,swift的核心理念與我認(rèn)為的該怎樣去編程是如此之契合缕探,這個(gè)感受一直充斥在我腦海中魂莫。通過這篇文章,我希望能幫助你(OOP的編程者)打開思路爹耗,去考慮該如何用更加Non-OOP(非OOP)的方式去解決問題耙考。
</small>
<h2>OOP的問題(和我不得不學(xué)它的原因)</h2>
我會(huì)是第一個(gè)跳出來說的:不用OOP的話做出iOS應(yīng)用很難。Cocoa的核心就是OOP潭兽。沒有OOP的話你根本寫不出來一個(gè)iOS應(yīng)用倦始。有時(shí)候我會(huì)幻想這不是真的。如果你有不同觀點(diǎn)讼溺,趕快證明我是錯(cuò)的吧楣号。我真的需要這樣最易,求你了怒坯,證明我是錯(cuò)的吧炫狱!
不管怎么樣,你總會(huì)遇到必須用對象剔猿、用引用類型解決問題的時(shí)候视译,然后由于Cocoa的規(guī)定而被迫使用類(classes)。這種情況下你碰到的問題都是我們大家熟知并熱愛的
<small>
- 傳遞class的實(shí)例這個(gè)做法好像總是有種不可思議的能力:你想用一個(gè)實(shí)例的時(shí)候讓這個(gè)實(shí)例的狀態(tài)和你所期望的不一樣归敬。<b>這是由于可變狀態(tài)導(dǎo)致酷含,你這個(gè)對象的另一個(gè)享有者在它覺得合理的地方改變此對象的屬性。</b>
- 如果不用多繼承的話汪茧,從一個(gè)很棒的class派生出子類從而獲得它的擴(kuò)展功能椅亚,妨礙了你使用另外一些class的更多功能,而且還增加了復(fù)雜性舱污。(舉個(gè)例子來說呀舔,把兩個(gè)UITextField的子類結(jié)合起來生成一個(gè)擁有這兩者功能的UITextField子類,難)
- 上面一條的另外一個(gè)問題是會(huì)引出意外行為扩灯。如果你遇到了類似上面一條所描述的情況媚赖,你就陷入到了一個(gè)依賴問題中:你連接了兩個(gè)superclass各自特性,對其中一個(gè)superclass的移除改動(dòng)可能會(huì)給另外一個(gè)superclass代理不良影響珠插。這就是class之間緊耦合所帶來的問題
- 單元測試中的mocking惧磺。有些class在系統(tǒng)中的耦合過于緊密,想完全測試這些class就需要你創(chuàng)建每一個(gè)class的假象表捻撑。我都不用告訴你本質(zhì)上你并沒有真正的測試了這個(gè)class磨隘,你不過是在假裝測試它。這里就不提很多Mocking的庫是用運(yùn)行時(shí)的小把戲來造一個(gè)假的class了顾患。
- 并發(fā)問題琳拭。這和上面提到的可變狀態(tài)是伴隨出現(xiàn)的。你從多個(gè)線程中同時(shí)改變一個(gè)引用就會(huì)引起這個(gè)問題描验,運(yùn)行時(shí)使對象之間的同步發(fā)生異常白嘁。
-
很容易導(dǎo)致出現(xiàn)像上帝類(God classes - 承擔(dān)著很多subclasses需要的重要高層級代碼的所有責(zé)任),Blobs(有過多職權(quán)的classes)膘流,Lava Flow(因?yàn)楹刑嗟姆欠ùa導(dǎo)致任何人都不敢碰的classes)等等這些種反面模式
</small>
ggg
<h2>POP 面向協(xié)議的編程</h2>
陷入OOP的反面模式特別容易絮缅。多半時(shí)間我們(包括我)就是太懶而不愿意去點(diǎn)File>New File。結(jié)果是在現(xiàn)有class的基礎(chǔ)上添加一個(gè)函數(shù)是如此輕松呼股,我們就不愿意從零開始建一個(gè)新的class了耕魄。如果你一直這么干,而且一直非常懶的從一個(gè)"很重要"的class派生subclass的話彭谁,你就把上帝類/死星類給弄出來了吸奴。實(shí)際上我之前就這么干過:我給一個(gè)app里的每個(gè)view Controller都加了能呈現(xiàn)一個(gè)指向navigationController的navigationBar的error view的功能。唉,我可真蠢则奥。直到要改動(dòng)那個(gè)Error上帝類行為的時(shí)候考润,我不得不把整個(gè)app都改一遍。這不是聰明的做法读处,你真應(yīng)該看看那些bug糊治。
如果使用了POP,這個(gè)Error上帝類很大程度上就能很容易的抽象出來罚舱,以后改進(jìn)它也方便井辜。
這是一個(gè)能展示(之前的方式)有多殘暴的例子:
class PresentErrorViewController: UIViewController {
var errorViewIsShowing: Bool = false
func presentError(message: String = "Error!", withArrow shouldShowArrow: Bool = false, backgroundColor: UIColor = UIColor.red, withSize size: CGSize = CGSize.zero, canDismissByTappingAnyWhere canDismiss: Bool = true) {
// 寫下了復(fù)雜的,脆弱的代碼
}
override func viewDidLoad() {
super.viewDidLoad()
}
}
隨著項(xiàng)目的進(jìn)行管闷,事情馬上變的明了:并不是每一個(gè)UIViewController需要這個(gè)error邏輯粥脚,或者真的需要這個(gè)class 所提供的每一個(gè)功能。我們團(tuán)隊(duì)里任何一個(gè)人都可以請用的在這個(gè)superclass里改點(diǎn)什么包个,從而影響整個(gè)app阿逃。這就讓代碼變得很脆弱。還是代碼呈現(xiàn)出多態(tài)赃蛛。本應(yīng)該有子類覺得它自己的行為恃锉,這里的superclass卻給幫著決定了。下面是在swift3.0中的我們?nèi)绾斡肞OP來更好的構(gòu)建這段代碼:
protocol ErrorPopoverRenderer {
func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool)
}
extension UIViewController: ErrorPopoverRenderer {
//使所有遵從于ErrorPopoverRenderer協(xié)議的UIViewController具有一個(gè)presentError的默認(rèn)實(shí)現(xiàn)
func presentError(message: String, withArrow shouldShowArrow: Bool, backgroundColor: UIColor, withSize size: CGSize, canDismissByTappingAnywhere canDismiss: Bool) {
// 加上呈現(xiàn)error視圖的默認(rèn)實(shí)現(xiàn)
}
}
class KrakenViewController:UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
func methodThatHasAnError() {
// ...
// 拋出error呕臂,原因是Kraken海妖今天吃人會(huì)感到不適破托。
presentError(message: "", withArrow: true, backgroundColor: UIColor.red, withSize: CGSize.zero, canDismissByTappingAnywhere: true)
}
}
看,這里發(fā)生了很炫酷的事情歧蒋。我們不僅消除了上帝類存在土砂,還讓代碼更加的模塊化并增強(qiáng)了它的擴(kuò)展性。通過創(chuàng)建一個(gè) ErrorPopoverRenderer協(xié)議谜洽,就會(huì)讓任何則遵循了該協(xié)議的class具有呈現(xiàn)出一個(gè)ErrorView的能力萝映。還不止這些,我們的KrakenViewController class 不用必須實(shí)現(xiàn)presentError這個(gè)函數(shù)阐虚,因?yàn)槲覀償U(kuò)展了UIViewController序臂,讓它提供了一個(gè)默認(rèn)實(shí)現(xiàn)。
唉不過等下实束!這有個(gè)問題奥秆!我們每次想要呈現(xiàn)一個(gè)ErrorView的時(shí)候都不想要去實(shí)現(xiàn)每一個(gè)參數(shù)。這就有點(diǎn)兒讓人不爽了咸灿,因?yàn)槲覀儾荒茉賞rotocol協(xié)議函數(shù)聲明中為參數(shù)提供默認(rèn)值构订。
我還挺喜歡這些參數(shù)的!更槽糕的是在讓買賣根據(jù)模塊化特征的過程中我們引入了復(fù)雜度避矢。還是繼續(xù)吧悼瘾,用swift3.0中新加的一個(gè)小妙招來多少的補(bǔ)償一下:
protocol ErrorPopoverRenderer {
func presentError()
}
extension ErrorPopoverRenderer where Self: UIViewController {
func presentError() {
// 在這里加默認(rèn)實(shí)現(xiàn)囊榜,并提供ErrorView的默認(rèn)參數(shù)。
}
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
func methodThatHasAnError() {
//...
// 拋出error亥宿,原因是Kraken海妖今天吃人會(huì)感到不適
presentError()
}
}
好了卸勺,現(xiàn)在看起來已經(jīng)很不錯(cuò)了。我們不僅消除了這些煩人的參數(shù)箩绍,還用swift3.0的新特性在protocol的層級上用Self給了presentError一個(gè)默認(rèn)實(shí)現(xiàn)。用Self意味著當(dāng)且晉檔協(xié)議的遵循者是繼承自UIViewController的情況下尺上,這個(gè)擴(kuò)展才會(huì)有效材蛛。這就讓我們能夠把ErrorPopoverRenderer真的當(dāng)做是一個(gè)UIViewController,而不需要對后者做擴(kuò)展怎抛!更棒的是卑吭,從現(xiàn)在開始,Swift的運(yùn)行時(shí)是以靜態(tài)調(diào)度而非動(dòng)態(tài)調(diào)度去調(diào)用presentError()方法马绝。大致的意思就是我們在函數(shù)調(diào)用點(diǎn)給presentError()方法增強(qiáng)了一點(diǎn)性能豆赏。
唉,不過還是有個(gè)問題富稻。到這里我們POP的旅途暫時(shí)告一段落掷邦,但對于它的完善依舊不會(huì)停止。我們的問題是如果只想對一部分參數(shù)使用默認(rèn)值椭赋,對生效的不用默認(rèn)值該怎么做抚岗?在這方面POP的話基本幫不上什么忙,但是我們可以尋求另外一種方法∧恼現(xiàn)在宣蔚,我們使用面向值的編程(VOP)吧。
<h2>面向值的編程(Value-oriented-programming)</h2>
看到了吧认境,POP和VOP總是伴隨出現(xiàn)胚委。在WWDC視頻中,Crusty提出了一下大膽的論斷:我們用struct 和 enum 類型就可以做到一切class能做到的事叉信。我很大程度上同意這點(diǎn)亩冬,但是沒這么極端。依我看硼身,protocol本質(zhì)上是吧VOP粘合在一起的膠水鉴未,這點(diǎn)我和Crusty吃相同太大。實(shí)際上既然我們說的了Swift的核心理念以及VOP鸠姨,我想給你們看看從Andy Matuschak的精彩訪談中關(guān)于Swift中的VOP
的話題里面摘出來的一張極好的圖:
能看出來Swift的標(biāo)準(zhǔn)庫中铜秆,僅有的4個(gè)class,和余下的95個(gè)struct和enum的實(shí)例共同構(gòu)建了Swift功能的核心讶迁。
Andy如此闡述道:用Swift編程的時(shí)候我們要考慮用一層很薄的對象層连茧,和一個(gè)很厚的值類型層核蘸。Class是有它們的地方啸驯,但是我想盡最大程度的去認(rèn)為它們的位置只應(yīng)該處于對象層中的一個(gè)很高的級別上客扎,在這里通過操縱值類型層中的邏輯來管理各種行為。
"把邏輯和行為分開"——Andy Matuschak
和你所了解的一樣罚斗,值類型被賦給一個(gè)變量或者常量徙鱼,抑或是傳給函數(shù)做參數(shù)時(shí)是它的值被拷貝的。這就讓值類型在任何時(shí)候只有一個(gè)享有者针姿,從而降低復(fù)雜度袱吆。和引用類型相反,在賦值過程中引用類型會(huì)有很多享有者距淫,其中一部分你甚至都沒意識到绞绒。在任何時(shí)間點(diǎn)使用引用的話會(huì)帶來一些副作用:引用的享有者會(huì)搗蛋,在背后偷偷改變這個(gè)引用榕暇。Class = 高復(fù)雜度蓬衡,值 = 低復(fù)雜度。
通過利用值類型的簡約特性彤枢,咱們實(shí)現(xiàn)一下之前提過的默認(rèn)參數(shù)的設(shè)計(jì)吧狰晚。我們用的是 Brian Gesiak的value options paradigm方法:
struct ErrorOptions {
let message: String
let showArrow: Bool
let size: CGSize
let candismissByTap: Bool
let backgroundColor: UIColor
init(message: String = "Error", shouldShowArrow: Bool = true, backgroundColor: UIColor = UIColor.red, size: CGSize = CGSize.zero, canDismissByTappingAnywhere canDismiss: Bool = true) {
self.message = message
self.backgroundColor = backgroundColor
self.size = size
self.candismissByTap = canDismiss
self.showArrow = shouldShowArrow
}
}
使用上面的選項(xiàng)型struct(是值類型!)就使我們的POP帶上了一些VOP的色彩缴啡,如下:
protocol ErrorPopoverRenderer {
func presentError(_ errorOptions: ErrorOptions)
}
extension ErrorPopoverRenderer where Self: UIViewController {
func presentError(_ errorOptions: ErrorOptions) {
// 在這里加默認(rèn)實(shí)現(xiàn)家肯,并提供ErrorView的默認(rèn)參數(shù)。
}
}
class KrakenViewController: UIViewController, ErrorPopoverRenderer {
func methodThatHasAnError() {
//...
// 拋出error盟猖,原因是Kraken海妖今天吃人會(huì)感到不適
presentError(ErrorOptions(message: "Oh noes! I didn't get to eat the Human!", size: CGSize(width: 1000.0, height: 20)))
}
}