代碼是如何生長的

2014年,我仍是一名大學生,在興趣的驅使下走上了編程的道路。後在各類洪荒之力的推進下於2016年7月開始耗費半年多時間編寫了一個叫作miniqueue的網站,項目雖然是失敗的,不過經驗是寶貴的。完成這個網站後,我寫下了下面這些文字,它本已被我遺忘在腦海,今天整理電腦才又發現了它,讀之,感受很有受益,在此分享,給學習編程的新手們,或是如我通常在箇中摸爬滾打過的猿們。程序員

代碼是如何生長的

這是一篇對miniqueue開發以來的總結性論文。數據庫

設計是一個險惡的問題:

軟件編程中,最重要的一環能夠說就是軟件設計,也叫軟件架構,這是決定一個軟件質量優劣的最根本一環。這一點從程序員的發展規劃中就可見一斑(頂尖程序員每每冠以「軟件架構師」一名)。經過這個軟件,我有幸體驗了一次糟糕設計下的編程,並逐步重構,轉向良好設計。編程

設計是一個險惡的問題。根據Horst Rittel和Melvin Webber的定義,「險惡的問題,就是那種只有經過解決或部分解決才能被明確的問題。」這個看似矛盾的定義實際上是在說,你必須經過把某個問題解決一遍,才能明確的定義它,而後再次解決,以造成一個可行的方案。小程序

現實世界中一個極好的例子就是Tacoma Narrows大橋。這座位於美國華盛頓州塔科馬的懸索橋,於1940年一個狂風大做的上午,因爲在風力做用下搖晃劇烈而坍塌。直到這座大橋的坍塌,工程師們才知道應該充分考慮空氣動力學因素。只有經過建造這座大橋(即解決這個問題),他們才能學會從這一問題中應該考慮的額外因素。安全

miniqueue的後臺在早期階段,沿用了製做小型程序的辦法,即沒有太多的考慮系統的層次性和模塊性,一切以實現功能爲主。在數據庫和界面之間,只有一層粗糙的數據庫操做層。這時的系統開發速度極快,由於它實際上忽視了不少關鍵問題。隨着系統的不斷擴展,這個數據庫操做層變得愈來愈臃腫,並且難以維護。好比,每一個數據庫的表在數據庫操做層都映射了一個類,但表和表的關係有時並非獨立的,有些表須要相互協做,以完成某些操做,此時聯合的行爲就被推到了界面層,這致使界面層原本用以調度頁面,卻附帶要處理不少業務邏輯的問題。架構

因而我着手重構系統,在新設計的系統裏,我在數據庫操做層之上,界面層之下,添加了一個層,叫作系統層。系統層將會調用數據庫操做層的接口,以實現數據庫各個表的相互協做,由此它能夠給界面層提供一套更加抽象的接口,這套抽象能夠很好的隱藏數據庫結構,客戶看到的將是一個完整的系統。如,節點系統(TableSystem)設計了兩個類——TableOperateSystem和TableFindSystem,其中TableOperateSystem就提供了一個建立新節點的接口createTable()。但節點系統並不映射那張叫作Table的數據庫表,由於建立新節點這個動做實際上涉及不少張表的改動。如此一來,底層的數據庫操做就和頂層的界面分離了,如今界面層僅和系統層打交道。編程語言

經過這一步重構,我還發現了一個額外的問題,系統的邏輯應該由系統層來接手。你很難想象,第一版的系統盡然是由界面來處理整個系統邏輯的。你能夠幹什麼,不能夠幹什麼,全由界面說了算。由於整個底層將數據庫操做的接口都暴露了。若是我不進行此次重構,我甚至都不會發現這個問題的存在,正是因爲系統層的出現,使得我對整個系統的理解更加深入,明瞭,才促成了這一意外發現。模塊化

然而好景不長,在系統層,那個老問題再次顯現——表與表的關係有時是相互關聯的,這意味着,底層數據庫操做層的某些類會在系統層糾纏在一塊兒,這種糾纏帶來的某些接口的聯合調用會重複的出如今系統層的不一樣位置,這顯然不是咱們所想看到的。因而另外一個層顯現了,此時思路變得很是清晰,明瞭。咱們缺一個數據模塊層,用來將底層的數據庫操做層模塊化,提供某種比離散的數據庫操做更加統一的接口,以便消除系統層裏的接口糾纏!到目前爲止,系統被設計成了四個層次,由頂向下分別是界面層,系統層,數據庫模塊層,數據庫操做層。學習

在沒有解決這個問題前,我認識不到系統分層的優點,也許從各類書籍中讀到了相關內容,但沒有實踐經驗的支撐,那些文字描述在個人腦海裏始終是個空殼,而如今,什麼是分層,什麼是模塊化,我已經有了全新的認識。測試

看清類的合適粒度:

類應該有多大?在極端條件下,一個類能夠吃下整個系統的所有代碼,或者各個類有且僅有一個接口,這樣每一個類就至關於一個方法。這兩種情形就是類的兩種極端粒度,它們都不是咱們想要看到的。那麼問題出現了,類應該有多大?

軟件設計原則中,有一條「單一職責原則」,簡稱SRP,它描述的是,類的職責要單一,不能將太多的職責放在一個類中。在沒有經驗支撐的狀況下,它不過是一句空話,由於它什麼也沒告訴你,當你面對一個系統的時候,你仍是不知道一個類作到何種程度算是知足了單一職責原則。

這個問題在作miniqueue的時候始終困擾着我。在最初的設計中,數據操做層裏的每一個類,對應一張數據庫表,我認爲這個類就知足單一職責了,由於它只對一個表負責。在開發初期,這些類確實可用,它們的粒度算是合適的,沒有對我形成什麼困擾。然而隨着系統的發展,這個類開始「爆炸」了。因爲它處理了數據庫的增刪改查四個功能,因此每當我須要它知足一個新的功能的時候,它都須要添加一項新的接口,因而我得再一次深刻類的內部去編程。這時你會發現你違反了另外一項原則——「開閉原則」簡稱OCP。「開閉原則」說的是,軟件實體應該對擴展開放,對修改封閉。什麼是擴展,什麼是修改?這個問題我也是在不斷的開發中才切實領會的!具體來講,軟件開發應該以類爲最小單位,當一個類投入使用,它就不該該再被修改,注意這裏的修改指的是這個類的接口,也就是它全部的public成員。

將類視爲最小單位意味着,當你編寫或者修改一個類的時候,該類是不可靠的!任何在構建中的類都是不可靠的,若是該類已經投入了使用,說明它已經過測試,系統認定它是可靠的,此時若是你從新在編譯器中打開該類,試圖對它進行修改,那麼一個可靠的類將會回到不可靠階段,這對整個系統的安全性和健壯性是極大的威脅。因此OCP說要對修改封閉,要讓投入使用的類再也不發生變化。那麼如何應對系統的功能擴展呢?以類爲單位擴展!這意味着當你寫1.0版時,整個系統的架子得容許新類的加入。有了這樣的認識,類的合適粒度就有判斷依據了。一個類不能作太多事,以致於讓它宣稱,它已經完整的處理了某件事,這樣的類當需求變化時,它便不得不作出修改,從可靠回到不可靠。一個合適粒度的類應該作出這樣的宣言:我已經完成了一個小功能,但我並無完成全部工做,若是你有其餘須要,請查詢和我相似的其餘類。此時軟件實體就能夠作到OCP——能夠擴展類,但不能夠修改類。

讓人沮喪的是,類的合適粒度永遠沒有正確答案,重構是編程中一件永遠也幹不完的事,但從合適的角度思考問題,能讓你的系統工做的更舒服,讓你的代碼看起來更讓人賞心悅目。

如何在獨自編程的狀況下看出層次:

軟件的首要使命是:管理軟件複雜度。Steve McConnell在那本著名的《代碼大全》中使用了大量篇幅講述這個問題。計算機先驅Edsger Dijkstra這樣描述:「在語義的層次量上相比,通常的數學理論幾乎是平坦的。因爲提出了對很深的概念層次的須要,自動化的計算機使咱們面臨一種本質上全新的智力挑戰。」因爲軟件邏輯層次太深,現代計算機語言一路從打孔紙帶發展到了面向對象編程語言,不一樣解決方案的提出,本質上就是爲了解決一個問題——管理軟件複雜度。

新興的面向對象編程語言其最大的優點就是有效的封裝了複雜。然而在一個軟件新手面前,面向對象和麪向過程幾乎沒有區別,你能夠想象不少剛剛接觸編程的人都在使用面嚮對象語言編寫面向過程的代碼。這也難怪,學校里老師佈置的做業是固定的,需求不會更改,學生只須要寫出代碼,讓計算機得出他們預期的結果,就算完事了。而在真實的軟件編程中,需求總會發生變化,軟件老是會調整本身的功能,以適應客戶的痛點,還有一點不容忽視,那就是代碼量。小程序的代碼量一個程序員可能不出三天就能完成,這樣的程序顯然沒有必要花費好幾個小時的時間去設計層次和模塊,加之需求不變,這麼作簡直是牛刀殺雞。在miniqueue初期,我也遇到了相似的麻煩,在個人經驗內,這樣龐大的系統我尚未接觸過,曾寫過的小程序,小到5個類就能解決問題,若是你不嫌難看徹底能夠用一個類吞下全部代碼。

然而miniqueue不是一個課程設計式的小做業。到目前爲止,它的代碼量就已經超出了一我的的理解範圍——即,你必須依靠類的封裝性幫你安全的忽視系統的其餘部分。有一個問題浮現了,在團隊協做的狀況下,人們自然須要設計接口,以實現系統不一樣部分的銜接,即系統各個部分能夠分別同時開發,只要你們事先約定接口。但我是獨自編程,整個系統是一部分一部分完成的,不存在同時開發的問題,對於各個類的做用我很清楚,不一樣的類如何協做我也明白,此時層次就顯得異常難以察覺。如同完成一幅巨型壁畫,若是是多人配合,你發現不一樣的人能夠被分配以完成不一樣的層次,若有人畫背景,有人畫人物。但若是整幅畫都由你一人完成,你極可能會從這幅畫的某個角落開始畫起,而不是先背景後人物這樣分層實現。miniqueue的構建過程當中我就遇到了這樣的狀況,它矇蔽了個人雙眼,使得我花了很長一段時間纔看出整個系統的層次性。

過後我作了總結,如何在獨自編程的狀況下,看出系統的層次(固然這個問題只對新人有用,由於你一旦經歷過一次,這樣的問題就不會再出現,由於你的思考角度已經發生了轉變)?

首先,在編程以前,想象兩我的物,一是你(類的編寫者),一是客戶(類的使用者),固然在獨自編程的狀況下,客戶極可能就是3天后的你,但你要認爲客戶對該類的內部渾然不知,他只能看到這個類的接口,僅此而已。這樣一來你就得認真編寫類的使用說明,認真編寫接口。而當三天後你開始使用這個類的時候,不要去想類的內部是如何實現的,僅僅盯着類的接口,若是你發現類的接口讓你摸不着頭腦,說明這個類的抽象是存在問題的,這時候不要使用你對它內部的知識來強行使用該類!你應該放下手頭的工做,回到那個類的內部,看看是什麼緣由致使了接口的模糊性!這是看出層次性的第一步,即,你要將類的編寫和類的使用徹底分離,避免穿過類的接口去使用類。

其次,我要告訴你們一個淺顯但卻很是容易被忽視的事實——從接口的角度看,系統由底層到頂層是逐漸收緊的!這是一個擺在你面前你立刻就會想明白的道理,但實際構建系統的過程當中,特別是新手,很容易對它視而不見。表如今,既然這個類,頂層須要使用,爲何不讓頂層直接去調用它呢?若是你讓頂層直接調用一個很是靈活的類(如同個人界面層直接調用數據庫操做層),最後你會發現,實際上你不過是把不少事情壓縮到了頂層去作,而頂層最後會被這些不該該屬於這個層次處理的事情填的異常臃腫。而一個健康的系統,其底層應該提供數量衆多,且靈活的接口,從這裏開始,慢慢向上,每一個層處理一些事情,並告訴其上層,不用再關注它所處理過的事情,這樣一層層向上,很複雜的一個事物將會被層次,分別一點點隱藏起來。當你看到一個靈活的類時,你就應該知道它在層級結構中應該是處於下層的,而當你看到一個接口比較狹窄的類時,你就應該知道它應該處在上層。

最後,實現分層的重要一點——觀察接口的抽象!當你在設計一個類的時候,不要寫任何代碼,相反,你應該敲下一些文字,用以指導你的抽象。一個類是什麼,不是由它的名字,或者你對它寫的註釋決定的,而是它的接口所表現出來的抽象。直立行走,雜食,不能飛翔,體力極好能夠連續奔跑40千米,體長175釐米,體重70千克,智商100,對於這個對象,就算我給它命名animal,你也能看出來他是一個human。當你在編寫程序的時候,優先考慮的是類的抽象,而不是內部的具體實現,你就可以看出分層的端倪了。由於「層次」說的就是某些抽象一致的類的集合。有了類的抽象,再思考層的抽象,就會清晰許多。無論是類仍是層,它們都是爲了隱藏某些複雜性而存在的。

代碼是如何生長的:

我是個編程新手,又恰巧在這樣的狀況下,試圖去編寫一個不簡單的程序,致使我把,一段代碼從「萌芽」到「破土而出」走了一遍。我想不是任何人都有這個機會,畢竟參加工做後你們面對的是軟件架構師已經搭建好架子的程序,多數人並不須要本身重頭去架構一個程序,況且是在經驗匱乏的狀況下去架構一個程序。這一路走來,我推翻了不少設計,重寫了不少代碼,它們成爲了回收站裏的犧牲品,但在個人腦海裏,它們爲我構建了一個全新的編程世界。總結來講,永遠不要有一勞永逸的想法,代碼歷來不是固定的,它一直都在生長——何爲生長?——在迭代中精進與斷舍離。

相關文章
相關標籤/搜索