如何評價 Clean Code 作者對 Swift 與 Kotlin 的看法?

Clean Coder Blog


我不熟悉 Swift 所以不便評論,只針對文中對 Kotlin 的幾點批評發表一點個人意見。

Uncle Bob 對 Kotlin 的批評主要體現在兩點,其一是 Kotlin 默認不允許類繼承和改寫,除非程序員顯式指定 open 和 override 關鍵字;其二是 Kotlin 區別 nullable 類型並強製程序員顯式地添加 null 檢查。對於第一個問題,Uncle Bob 在文中給出了一大段反詰:

... Perhaps you believe that inheritance and derivation hierarchies that are allowed to grow without bound are a source of error and risk. Perhaps you think we can eliminate whole classes of bugs by forcing programmers to explicitly declare their classes to be open. ...

The question is: Whose job is it to manage that risk? Is it the language』s job? Or is it the programmer』s job.

他拋出的問題答案當然很明顯——不當的繼承確實是風險,而管理這些風險的自然是程序員。因此他用這個誰也不會反對的結論為立足點,又發出了一串反詰:

Consider: How do I know whether a class is open or not?……

Why are these languages adopting all these features? Because programmers are not testing their code. And because programmers are not testing their code, we now have languages that force us to put the word open in front of every class we want to derive from.

在他的觀點裡,出 bug 的原因是因為程序員不測試自己的代碼,因此語言的設計者加入了 open 關鍵字。然而這段話里 Uncle Bob 卻一口氣犯了至少兩個概念不清的錯誤。

首先,他認為 Kotlin 強迫程序員把 open 關鍵字放在每一個他們想繼承的類里。但在面向對象編碼實踐中,設計一個類和決定繼承那個類的的程序員,通常不是同一個人。而且從功能角度決定一個類是否可以被繼承的人,總是類的設計者。反之,試圖繼承那個類的程序員,或者說,類的使用者,無權決定那個類是否是 open;除非他和類的設計者溝通並試圖修改設計。把類的設計者和使用者的立場混為一談,顯然是不妥當的。

其次,從面向對象設計的角度看,即使在語言機制上允許繼承並改寫(override)任何成員函數,實際工作中使用的類通常也不會允許使用者隨意改寫任何成員,否則很容易破壞內部語義。標準的實踐應該是暴露出一部分可以改寫的方法或介面,並以文檔或者語言規則的方式明確地告訴類的使用者。在經典的 23 個面向對象設計模式中,至少 template method 這一大類模式,都依賴於這個規則才得以成立。把程序語言的語義和代碼邏輯語義混為一談,這又是一個混亂之處。

不管怎樣,在拋出了兩個和工程實踐完全相左的推論之後,Uncle Bob 自信滿滿地繪製了一幅在所有人看來都不合理的圖景,來試圖證明 Kotlin 的設計是糟糕的:

And so you will declare all your classes and all your functions open.

Uncle Bob 認為那些使用 Kotlin 寫代碼的類設計者為了滿足使用者對類繼承的要求,最終會將所有類都設計為 open,並允許所有函數可被改寫。但這顯然違反了目前大多數面向對象語言的設計實踐。即使是默認所有成員方法都可改寫的 Java,也已經早早在類修飾符里加上了 final 關鍵字和 Override 標籤。即便 Uncle Bob 非要反駁說 Java 的限制默認不啟動,但那是為了兼容早期的代碼,更別說 final 關鍵字的語義本身就是強制的。從這個意義上說,Kotlin 引入這兩個關鍵字,只是順應了如今程序開發早已廣泛認可的傳統,讓類的設計者可以用語言關鍵字的形式定義和使用者之間的契約,這跟程序員是否做了測試更沒有關係。如果還有人要爭辯說上面的分析都是可能性,那麼看看 C++ 和 C# 這兩個都使用了默認不繼承語義的社區,我們也可以很輕易地得到結論:他所說的可怕情景,從未在真實世界裡出現過。

整個討論,以含混不清的概念開始,推出了毫無道理的推論,最後列了個完全不符合工程實踐的稻草人並痛打之,讓我一時都懷疑是不是有人盜用了 Uncle Bob 的賬號發表的文章。

至於對 nullable 的批評,我倒是可以部分地理解 Uncle Bob 的立場,儘管結論仍然是糟糕的。解決 NullPointerException (NPR)引起的 bug 的確是程序員的責任,但作為工具的程序設計語言在語言層面幫助程序員避開 bug 並不是罪過。實際上 NPR 問題廣泛存在於默認引用語義的許多程序設計語言中,不只是 Java,還有 C#,Python,也是廣大程序員頭痛不已的問題之一。在程序分析的基礎上強制一些啰嗦但安全的編碼規則,分擔一部分程序員之後書寫和維護測試用例的壓力,也是很有價值的事。以程序員應該寫測試為理由否決程序設計語言提供額外幫助的做法,是站不住腳的。

據實而言,我也並不滿意 Kotlin 中 nullable 的一部分設計,特別是 ?: 操作符:它實際上是給了程序員繞開檢查的便利,在我看來也是負面作用大於正面。但即使如此,這也是不知疲倦始終一致的編譯器做的檢查,比依賴可能犯懶可能疲勞的程序員自行維護測試用例,還是可靠得多。

順便解釋一下這裡的用詞:Kotlin 對 nullable 的檢查更多地是通過控制流分析進行,輔以類型系統。大家可以簡單在 kotlinc 命令行環境下執行這樣的代碼。而同樣的形式在使用 Maybe Monad 的語言里(比如 Haskell 和 Rust),是不允許的。

val canBeNull : String? = null
val breakHere : String = canBeNull // Break
val assigned : String = if (canBeNull == null) "test" else canBeNull // Pass

所以這裡各位大談靜態類型或者類型系統如何秒天秒地的朋友,不妨真的去跑跑程序試試看,三行而已,雖然花的時間可能比看完 Uncle Bob 寫的前三個自然段要長一點。

利益相關:最近正在評估用 Kotlin 做一個開源項目。


所以 @Canto Ostinato 說「動態類型金腰帶,形式方法無屍骸」

動態/靜態類型的終極爭端在於:為了達到足夠的靈活性(換言之,接受足夠多的 Term),那麼用動態類型是最簡單的辦法。動態類型系統可以很容易地接受大量的 Term,然而無法有效地在編譯期拒絕錯誤的 Term。靜態類型也可以很靈活,比如 Idris 的 Dependent Type,吊到飛起,但是這種高級的類型系統往往會導致類型推理不可判定,強迫你寫聲明。而我們至今不清楚(可能也永遠無法弄清楚)不需要寫聲明的靜態類型系統的邊界在哪(Hindley-Milner 顯然在邊界內;Implicit Calculus of Constructions 在邊界外)。

當然了這個作者很多指責往往只是因為某些語言的設計得高不成低不就,導致你用高大上的特性的時候要寫大量重複的字,浪費卡路里。比如作者指責的 Swift 異常要每一層都用 try 的問題,在 Extensible Effects 框架面前壓根不是事兒……

所以我奉勸某些在 PLT 領域沒有多少建樹的大廠好好和學術界合作,不要論文光讀一半就弄語言,結果出個四不像。


寫Kotlin應該合理使用null,而不是整天抱怨編譯器非得讓你加問號。

問號越多,說明這人越不熟悉Kotlin。他寫Kotlin弄了一堆問號出來,這本來就不是正確使用方式,就像你寫Rust,把所有類型全弄拿Option包起來,然後每次調用都來一波模式匹配,說「這語言怎麼要這麼多模式匹配,真垃圾」 = =

就事論事,我說的這篇博客啊。

Swift沒像Kotlin這樣研究過,不敢說。


評價的話,這篇博客的水平出乎意料的低。。。

如果不想用類型系統來作為防止錯誤的工具,市面上弱類型的語言有得是,因為個人偏好說人家是dark path。。。無語。


他說的關於因為人們不喜歡測試,所以才要語言加上那些防止錯誤編碼的特性,其實這是兩回事。

就像java強制處理異常的特性來說,語法上已經保證你必須catch或者throw,假設你catch了,難道就等於你對這個異常的處理正確了?test依然要做,但是能減少很多null問題的測試,kotlin在這方面做的事是合理的。

不過作者有些地方提到了一個很好的理念,好的語言不能機械性地把認為好的功能堆砌就算完事。


前面一些觀點還可以贊同,後面就開始露出測試吹的本來面目。

語言內置異常機制,為的就是能讓程序員不必在每一步都顯式處理錯誤,這是一種抽象,Golang 那樣人肉檢查返回的Error就是倒退。

後面的就不值一讀了,反對一切測試吹。我們都在想著怎麼避免手動構造測試的Case,恨不得把全部業務邏輯都 Encoding 到程序邏輯中全部 QuickCheck、自動驗證。這些沒長進的測試吹還在整天想著這些,醒醒吧!大清已經亡了!

關於 QuickCheck:如何看待The Mysteries of Dropbox?


世界是static typed還是dynamic typed的啊?

我覺得有些人把dynamic type混淆成non type實在是…


Uncle Bob感覺對代碼測試已經到了癲狂的狀態。推薦所有軟體開發使用TDD。遵循他的TDD三大鐵律。所以他寫這篇也不奇怪。


反正誰對編程語言指指點點都是會惹得一地雞毛的 ,因為編程語言的特性實在是眾口難調。今天下午在微信群扯了點Rust,然後我立馬意識到會惹事情了,然後就刻意不去響應,讓話題淡下去。

不去扯什麼Swift或Kotlin的特性的不是。相反這兩門語言語法我都很喜歡,有所顧忌的是Swift目前主要用在App開發,在通用型/Web應用方面的生態還未成型;而Kotlin感覺不到什麼缺點,就是覺得在Java平台上混太久了,想玩玩其他的。對了,最近研究Rust,所有權是缺陷還是特性? ♂?


程序語言這個領域真的是阿貓阿狗都可以隨便發表不負責任的言論,所有的論點都建立在「用過」,「我覺得」,「我認為」。

覺得try catch 太多太難用?這也怪靜態類型?用Monad 嘛!


堅決反對。要是沒有這些東西,C++里還怎麼搞friend injection。要是沒有friend injection,SMP還能玩的這麼溜么? (逃


更新:

我不是說靜態類型語言寫DSL不好,而是說因此就黑動態類型語言寫DSL太難實在是一葉障目。

1. 現在用個parser combinator寫parser不要太簡單吧?都要寫DSL了,paser根本不是障礙。Racket雖然是括弧語言,但裡面的方言實現比如Datalog和ProfessorJ都跟S Expression沒有半毛錢關係。況且不說S Expression本身就有天然適合做DSL的優勢。


2. 我也沒有說用Closure來實現DSL(還是說你想說的是Clojure?),而是說在動態類型語言中基於(Hygiene)Macro實現的DSL實際上非常簡單,一點也不『太難』。當然Scala和Haskell也有Macro,好不好用是另一回事了。


實際上,使用macro可以更得構造出language tower,每一層都可以用來實現更高一層的語言,同時保持每一層語言在結構和語義上的簡明[4][5]。這是使用Macro來構造複雜DSL最核心和最精妙的idea。


比如Racket本身就是一個language-oriented programming language,Racket就是為了開發新的Programming language和DSL而開發的[2]。

比如Typed Racket,Gradual typing的語言,Macro實現的[3]。

比如Scribble,一個文檔處理語言,比LaTeX不知高到哪裡去了,Macro實現的[7]。

比如Slideshow,用來寫幻燈片的DSL,Macro實現的[8]。

比如Rosette,內嵌了SMT Solver的DSL,用來做Program Synthesis和Verification,Macro實現的[9]。

比如Clojure里的core.logic,一個Relational的EDSL,大部分是用Macro來實現的[6]。


……


Reference:

[1]用Racket來開發DSL的教程: Beautiful Racket: index.html

[2]Racket作為一門language-oriented programming langauge的設計哲學: The Racket Manifesto

[3]Typed Racket的設計和實現:http://www.ccs.neu.edu/racket/pubs/popl08-thf.pdf

[4]http://www.ccs.neu.edu/home/shivers/papers/ziggurat-jfp.pdf

[5]https://www.cs.utah.edu/plt/publications/macromod.pdf

[6]clojure/core.logic

[7]Scribble: The Racket Documentation Tool

[8]Slideshow: Figure and Presentation Tools

[9]The Rosette Language

===

@楊博 靜態類型的語言當然有很多好處,但這麼黑動態類型語言是不對的。

在Racket里用Macro來寫DSL不要太簡單了吧?Scribble, miniKanren, TypedRacket,PLT Redex都是很好的例子。

事實上,類型系統越簡單,寫程序越容易,因為關於程序的invariant被放寬了。用Coq/Idris寫程序容易還是用Java寫程序容易?用Java寫程序容易還是用Python寫程序容易?當你的類型系統變複雜時,便需要花更多的時間來讓程序理解程序。

至於說是static typed lang還是dynamic typed lang更容易寫出正確的程序,是另一個問題。極端得來說用Coq/Idris是不是比用Python更容易寫出正確的程序呢?It all depends.

類型系統是對程序的抽象,沒有類型系統並不意味著沒有別的抽象機制來讓程序理解程序。

對於動態類型語言程序員來說,contracts,gradual typing都是有用的工具,他們的好處是你可以在當你真正需要他們的時候才用,然而靜態類型語言卻從一開始就強制你使用類型來標明specification。順便說一句contracts也是可以靜態驗證的。


當你開發應用時,用類型系統還是全靠測試,哪種方式開發效率更高,哪種方式更健壯,其實很難說。我見過把Lua玩出花的高手,也見過C/C++一夜三千行零bug的大神。

然而,動態類型語言寫工具、框架或者DSL,實在太難。因為工具、框架和DSL歸根到底都是讓程序理解程序並操縱程序。動態類型的程序員仍然可以自我修鍊,讓程序員本人達到很高的境界。然而,如果沒有類型系統,怎麼能讓程序理解程序呢?

Bob叔作為諮詢師,要給客戶解決具體業務問題,不像我們要擼代碼寫框架。所以我們看重的「讓程序理解程序」對他一點意義都沒有。

然而,對我這樣的碼農來說,為了完成第三魔法,達到根源,只能選擇靜態類型語言。

因為靜態類型語言寫的DSL可以利用原生類型系統建模,框架開發者只用寫一點點擴展功能就行了。

舉個例子,動態類型的ReactJS為了實現數據綁定功能,需要自己重新實現一整套虛擬DOM,代碼要比Binding.scala代碼長了二十多倍,然而ReactJS的數據綁定功能還是很脆弱。這是因為ReactJS無法理解用戶寫的數據綁定表達式,無法得知狀態修改的意圖,只能寫很複雜的差分演算法來瞎猜。

詳細對比可以參見:虛擬DOM已死?

(BTW1:追求第三魔法本身可能就很危險,所以Bob叔說這是一條wrong path,我覺得沒毛病)

(BTW2:Lua高手和C/C++大神是同一個人)

(BTW3: @Belleve 最近寫了一個答案聊了一下如何達到第三魔法,值得一讀)


很low一篇文章


我基本上是贊同作者觀點的,後半部分吹噓測試的部分沒有認真看,但前半部分對語言的思考還是有點價值。

第一部分,類型系統:作者沒說哪種好,只是說了度的問題。太嚴格的類型判斷會失去靈活性,這很中立。

第二部分,kotlin的繼承靈活性的問題,沒學過,不評論。就個人而言,我不喜歡語言關鍵字中隱含太多語義和上下文,這會導致我寫代碼的時候想太多。

第三部分,null值的問題,這的確是導致很多隱性bug的罪魁禍首。首先我很不喜歡每次用到一個nullable值的時候都加一層判斷,這讓代碼很難看,並且當這種細節佔了大量篇幅,就會遮擋了真正有價值的代碼。


兩個語言都沒學過,只是看大家反對bob那麼激烈,忍不住看了下原文。

我個人認為bob討論的主要問題是:如何減少程序bug,是A:無限制地把編程最佳實踐帶入到語言層面,還是B:由程序員自己(應該指的是單元測試)保證?

bob選擇B。

bob認為把最佳實踐加入語言會加重設計的負擔,這一點我個人是不贊同的。因為做設計的時候本就是先緊後松,先嚴格控制各種許可權,有實際需求再開放。如果語言能提供方便的控制,我認為是很好的。

但是我也反對無限制地向語言加入最佳實踐,因為判斷何為最佳以及適用範圍是否寬廣太難了。不過顯然兩個語言的設計者也明白這一點,他們只是加入了幾條規則。

至於這幾條規則是否加得合理,那是需要長時間實踐才能下結論的。


kotlin噴的沒毛病,open的設定很神奇,null的檢測按照道理沒毛病,但是實現的還不夠好,一段代碼我們都知道他不是null,但是機器就是判定可能為null。


推薦閱讀:

如何規範小開發公司的測試流程。?
做介面測試的流程一般是怎麼樣的?
哪家軟體測試教學教得好?
軟體測試培訓哪個好?
最近在找實習。面試軟體測試時,面試最想聽到的答案是什麼?

TAG:編程語言 | 軟體工程 | 軟體測試 | Swift語言 | Kotlin |