編程能力七段論(上)
前言
程序員的編程技能隨著經驗的積累,會逐步提高。我認為編程能力可以分為一些層次。
下面通過兩個維度展開編程能力層次模型的討論。
一個維度是編程技能層次,另一個維度是領域知識層次。
編程技能層次
編程技能層次,指的程序員設計和編寫程序的能力。這是程序員的根本。
0段—非程序員:
初學編程者,遇到問題,完全是懵懵懂懂,不知道該怎麽編程解決問題。也就是說,還是門外漢,還不能稱之為“程序員”。計算機在他面前還是一個神秘的黑匣子。
1段—基礎程序員:
學習過一段時間編程後,接到任務,可以編寫程序完成任務。
編寫出來的代碼,正常情況下是能夠工作的,但在實際運行中,碰到一些特殊條件就會出現各類BUG。也就是說,具備了開發Demo軟件的能力,但開發的軟件真正交付給客戶使用,恐怕會被客戶罵死。
程序員程序是寫好了,但到底為什麽它有時能正常工作,有時又不行,程序員自己也不知道。
運行中遇到了bug,或者需求改變,需要修改代碼或者添加代碼,很快程序就變得結構混亂,代碼膨脹,bug叢生。很快,就連最初的開發者自己也不願意接手維護這個程序了。
2段—數據結構:
經過一段時間的編程實踐後,程序員會認識到“數據結構 算法=程序”這一古訓的含義。他們會使用算法來解決問題。進而,他們會認識到,算法本質上是依附於數據結構的,好的數據結構一旦設計出來,那麽好的算法也會應運而生。
設計錯誤的數據結構,不可能生長出好的算法。
記得某一位外國先賢曾經說過:“給我看你的數據結構!”
3段—面向對象:
再之後,程序員就會領略面向對象程序設計的強大威力。大多數現代編程語言都是支持面向對象的。但並不是說,你使用面向對象編程語言編程,你用上了類,甚至繼承了類,你就是在寫面向對象的代碼了。
我曾經見過很多用Java,Python,Ruby寫的面向過程的代碼。
只有你掌握了接口,掌握了多態,掌握了類和類,對象和對象之間的關系,你才真正掌握了面向對象編程技術。
就算你用的是傳統的不支持面向對象的編程語言,只要你心中有“對象”,你依然可以開發出面向對象的程序。
如,我用C語言編程的時候,會有意識的使用面向對象的技巧來編寫和設計程序。用struct來模擬類,把同一類概念的函數放在一起模擬類。如果你懷疑用C語言是否能編寫出面向對象的代碼,你可以看一下Linux內核,它是用C語言編寫的,但你也可以看到它的源代碼字裏行間散發出的濃濃的“對象”的味道。
真正掌握面向對象編程技術並不容易。
在我的技術生涯中,有兩個坎讓我最感頭疼。
一個坎是Dos向Windows開發的變遷過程中,框架的概念,很長一段時間我都理解不了。Dos時代,都是對函數庫的調用,你的程序主動調用函數。Windows時代,則換成了框架。就算是你的main程序,其實也是被框架調用的。UI線程會從操作系統獲取消息,然後發送給你的程序來處理。Java程序員熟悉的Spring框架,也是這樣一個反向調用的框架。
現在因為“框架”這個術語顯得很高大上,因此很多“類庫”/“函數庫”都自稱為“框架”。在我看來這都是名稱的濫用。
“類庫”/“函數庫”就是我寫的代碼調用它們。
“框架”就是我註冊回調函數到框架,框架來調用我寫的函數。
另一個坎就是面向對象。很長一段時間我都不知道應該怎麽設計類和類之間的關系,不能很好的設計出類層次結構來。
我記得當時看到一本外國大牛的書,他講了一個很簡單、很實用的面向對象設計技巧:“敘述問題。然後把其中的名詞找出來,用來構建類。把其中的動詞找出來,用來構建類的方法”。雖然這個技巧挺管用的,但也太草根了點,沒有理論依據,也不嚴謹。如果問題敘述的不好,那麽獲得的類系統就會是有問題的。
掌握面向對象思想的途徑應該有很多種,我是從關系數據庫中獲得了靈感來理解和掌握面向對象設計思想的。
在我看來,關系數據庫的表,其實就是一個類,每一行記錄就是一個類的實例,也就是對象。表之間的關系,就是類之間的關系。O-Rmapping技術(如Hibernate),用於從面向對象代碼到數據庫表之間的映射,這也說明了類和表確實是邏輯上等價的。
既然數據庫設計和類設計是等價的,那麽要設計面向對象系統,只需要使用關系數據庫的設計技巧即可。
關系數據庫表結構設計是很簡單的:
1、識別表和表之間的關系,也就是類和類之間的關系。是一對一,一對多,多對一,還是多對多。這就是類之間的關系。
2、識別表的字段。一個對象當然有無數多的屬性(如,人:身高,體重,性別,年齡,姓名,身份證號,駕駛證號,銀行卡號,護照號,港澳通行證號,工號,病史,婚史etc),我們寫程序需要記錄的只是我們關心的屬性。這些關心的屬性,就是表的字段,也就是類的屬性。“弱水三千,我取一瓢飲”!
4段—設計模式:
曾經在網上看到這樣一句話:“沒有十萬行代碼量,就不要跟我談什麽設計模式”。深以為然。
記得第一次看Gof的設計模式那本書的時候,發現雖然以前並不知道設計模式,但在實際編程過程中,其實還是自覺使用了一些設計模式。設計模式是編程的客觀規律,不是誰發明的,而是一些早期的資深程序員首先發現的。
不用設計模式,你也可以寫出滿足需求的程序來。但是,一旦後續需求變化,那麽你的程序沒有足夠的柔韌性,將難以為繼。而真實的程序,交付客戶後,一定會有進一步的需求反饋。而後續版本的開發,也一定會增加需求。這是程序員無法回避的現實。
寫UI程序,不論是Web,Desktop,Mobile,Game,一定要使用MVC設計模式。否則你的程序面對後續變化的UI需求,將無以為繼。
設計模式,最重要的思想就是解耦,通過接口來解耦。這樣,如果將來需求變化,那麽只需要提供一個新的實現類即可。
主要的設計模式,其實都是面向對象的。因此,可以認為設計模式是面向對象的高級階段。只有掌握了設計模式,才能認為是真正徹底掌握了面向對象設計技巧。
我學習一門新語言時(包括非面向對象語言,如函數式編程語言),總是會在了解了其語法後,看一下各類設計模式在這門語言中是如何實現的。這也是學習編程語言的一個竅門。
5段–語言專家:
經過一段時間的編程實踐,程序員對某一種常用的編程語言已經相當精通了。有些人還成了“語言律師”,擅長向其他程序員講解語言的用法和各種坑。
這一階段的程序員,常常是自己所用語言的忠實信徒,常在社區和論壇上和其他語言的使用者爭論哪一種語言是最好的編程語言。他們認為自己所用的語言是世界上最好的編程語言,沒有之一。他們認為,自己所用的編程語言適用於所有場景。他們眼中,只有錘子,因此會把所有任務都當成是釘子。
6段–多語言專家:
這一個階段的程序員,因為工作關系,或者純粹是因為對技術的興趣,已經學習和掌握了好幾種編程語言。已經領略了不同編程語言不同的設計思路,對每種語言的長處和短處有了更多的了解。
他們現在認為,編程語言並不是最重要的,編程語言不過是基本功而已。
他們現在會根據不同的任務需求,或者不同的資源來選擇不同的編程語言來解決問題,不再會因為沒有使用某一種喜愛的編程語言開發而埋怨。
編程語言有很多種流派和思想,有一些編程語言同時支持多種編程範式。
靜態類型編程範式
采用靜態類型編程範式的編程語言,其變量需要明確指定類型。代表語言:
這樣做的好處是:
1、編譯器可以在編譯時就能找出類型錯誤。
2、編譯器編譯時知道類型信息,就可以提高性能。
這種範式認為,程序員肯定知道變量的類型,你丫要是不知道變量的類型,那你就別混了!編譯時,程序會報錯。
Swift和Go語言都是靜態類型編程語言,但它們都不需要明確指定類型,而是可以通過推斷由編譯器自動確定其類型。
動態類型編程範式
采用靜態類型編程範式的編程語言,其變量不需要明確指定類型。任意變量,可以指向任意類型的對象。代表語言:Python,Ruby,JavaScript。
動態類型的哲學可以用鴨子類型(英語:ducktyping)這個概念來概括。JamesWhitcombRiley提出的鴨子測試可以這樣表述:“當看到一只鳥走起來像鴨子、遊泳起來像鴨子、叫起來也像鴨子,那麽這只鳥就可以被稱為鴨子。”
這種範式認為,程序員肯定知道變量的類型和它支持的方法和屬性,你丫要是不知道變量的類型,那你就別混了!運行時程序會崩潰!程序崩潰怨誰?怨你自己唄,你不是合格的程序員!
動態類型的好處是:
不需要明確定義接口和抽象類型。只要一個類型支持需要的方法和屬性,那麽就OK。程序會相當靈活和簡單。Java,C#視之為命脈的接口/基類,在動態語言這裏都視如無物!
缺點是:
1、如果類型不對,編譯器也無法找到錯誤,而是運行時程序崩潰。
2、因為編譯器不知道變量的類型,因此無法優化性能。
面向對象編程範式
面向對象編程範式,從上世紀70年代末開始興起。它支持類和類的實例作為封裝代碼的模塊。代表語言:
早期編程語言都是面向過程的。就是順序,條件,循環,構成一個個函數。隨著代碼規模的增大,人們發現有必要對代碼進行模塊化。一個概念對應的代碼放在一個文件中,這樣便於並發開發和進行代碼管理。
人們還發現了“程序=數據結構 算法”的規律。因此,一個概念對應的數據結構和函數應該放在一個文件中。這就是類的概念。
面向對象編程範式,確實極大地提高了生產效率,因此得到了廣泛的應用,因此在語言層面支持面向對象編程範式的語言是極多的。
C語言盡管在語言層面上並不支持面向對象編程範式,但現代的C語言開發都會應用面向對象的模塊化思想,把同一類的數據結構和函數放在一個文件中,采用類似的命名方式。
畢竟C語言沒有在語言層面上支持面向對象,因此就有很多程序員想給C語言添加面向對象支持。其中的代表是和Objective-C。
是一種新的語言,但大部分語言元素是和C兼容的。
Objective-C是完全兼容的C的。Objective-C是給C添加了薄薄的一層語法糖以支持接口(就是其他語言的類)和協議(就是其他語言的接口)。甚至,Objective-C一開始的實現,就是一個C語言的預編譯器。Objective-C坦白講,除了添加的語法不太符合C流外,實際上其面向對象系統設計是相當精妙的。喬布斯早年慧眼識珠,把Objective-C收人囊中,因為封閉於Apple/NextStep系統內,因此少有人知。隨著iOs系統的普及,Objective-C近幾年才名滿天下。
函數式編程範式
函數式編程範式,是一些數學家發明的編程語言,他們認為程序就是數學函數嘛。代表語言:Lisp,Erlang,JavaScript,OCaml,Prog。
有很多大牛極力鼓吹過函數式編程語言,認為其極具革命性。但我認為他們過高估計了函數式編程範式的威力,我並不認為函數式編程範式相對於面向對象編程範式有何高明之處。
函數式編程語言,核心就是函數,它們沒有Class類的概念。但它的函數又不是傳統面向過程語言的函數,它的函數支持“閉包”的概念。
在我看來,函數式編程語言的函數,也就是“閉包”,說白了,其實就是“類”。編程語言發展到今天,就是需要模塊化,就是需要把“數據結構”和“算法”結合起來。不論何種語言,不把它們結合起來的編程方式,都是沒有出路的。
面向對象編程語言,用類把“數據結構”和“算法”結合起來。類的核心是“數據結構”,也就是其“屬性”,而不是“算法”,其“函數”。在類中,是函數依附於屬性。
而函數式編程語言,用閉包把“數據結構”和“算法”結合起來。是函數能夠抓取外部的字段。是“屬性”依附於“函數”。
“類”本質上和“閉包”是等價的。現在很多面向對象編程語言都加上了對閉包的支持。觀察其代碼,我們可以發現,它們實際上都是用“類”來實現“閉包”的。
“類”和“閉包”誰更易用?明顯是“類”。
而“閉包”更簡潔一些,因此“閉包”在面向對象編程語言中常用來替換匿名類。只有一個函數的類,寫成一個類太麻煩,不如寫成閉包,更加簡潔。
吐槽一下OCaml語言,其前身Caml語言本身是一種挺好的函數式語言,硬生生添加了一套完整的面向對象機制,同時支持面向對象和函數式編程範式,很容易像C 一樣腦裂的。
也有很多面向對象語言控看著JavaScript嫌煩,總是想把面向對象支持添加到JavaScript上。ActionScript就是其中一種嘗試。我用過,真的是和Java沒多少區別了。
再吐槽一下ExtJS。當初選型Web前端開發框架時比較了ExtJS和JQuery。
ExtJS明顯是Java高手開發的,硬生生用JavaScript模擬Swing的設計思想,搞了一套UI庫。
JQuery開發者明顯是領悟了JavaScript的函數式編程範式,依據JavaScript的動態函數式編程語言的特點打造了一套UI庫,立刻秒殺ExtJS。
由ExtJS和JQuery的故事,我們可以看到多語言編程能力是多麽的重要。ExtJS的作者精通並喜愛Java,因此他把手術刀JavaScript當做錘子Java使,一通亂敲,費力不討好。
函數式編程語言,還有尾遞歸等一些小技巧。尾遞歸可以不用棧,防止遞歸調用時棧溢出。
模板編程範式
模板編程,就是把類型作為參數,一套函數可以支持任意多種類型。代表語言:C 。
模板編程的需求,是在C 開發容器庫的時候發明的。因為容器需要保存任意類型的對象,因此就有了泛型的需求。
C 的模板編程,是在編譯時,根據源碼中的使用情況,創建對應類型的代碼。除了C 這種方式,Java,C#也有類似的機制,叫做“泛型”,但它們的實現方式和C 的模板很不同。它們的編譯器不會生成新的代碼,而是使用強制類型轉換的方式實現。
在沒有模板/泛型的編程語言中,怎樣在容器中存放對象呢?存取公共基類類型(Java,C#)的對象,或者void*指針(C)即可,取出時自己強制類型轉換為實際類型。動態類型語言,不關心類型,更是無所謂了,隨便什麽對象直接往容器裏扔進去,取出來直接用即可。
一些C 高手又在模板的基礎上搞出了“模板元編程”。因為模板編程,就是C 的編譯器搞定的嘛,模板元編程就是讓編譯器運算,編譯完結果也就算出來了。我不知道除了研究和炫技,這玩意有啥用?
小結
一門語言是否值得學習,我認為有幾個標準:
1、是否要用,要用就得學,這麽沒有疑問的。畢竟我們都要吃飯的嘛。
2、其語言特性是否給你耳目一新的感覺。如果是,那就值回票價了。如Go語言廢掉了異常,改用返回多值。我深以為然。我其實已經主動不用異常好多年了。因為,我覺得既然C不支持異常也活得很好,為什麽需要異常呢?出錯了,返回錯誤碼。無法挽回的錯誤,直接Abort程序就可以嘛!而且,異常實際上是違反面向過程編程原則的。一個函數應該只有一個入口一個出口。拋出異常就多了出口了。
3、是否擅長某一個領域。如果你手裏只有一把錘子,那麽你就只能把所有任務都當做釘子猛錘一通。但如果工具箱裏有多種工具,那面對不同的任務就得心應手多了。
7段—架構設計
還需要掌握架構設計的能力,才能設計出優秀的軟件。架構設計有一些技巧:
1、分層
一個軟件通常分為:
表現層–UI部分
接口層–後臺服務的通訊接口部分
服務層–實際服務部分
存儲層—持久化存儲部分,存儲到文件或者數據庫。
分層的軟件,可以解耦各個模塊,支持並行開發,易於修改,易於提升性能。
2、SOA
模塊之間通過網絡通訊互相連接,松耦合。每一個模塊可以獨立部署,可以增加部署實例從而提高性能。每一個模塊可以使用不同的語言和平臺開發,可以重用之前開發的服務。SOA,常用協議有WebService,REST,JSON-RPC等。
3、性能瓶頸
1)化同步為異步。
用內存隊列(Redis),工作流引擎(JBpm)等實現。內存隊列容易丟失數據,但是速度快。工作流引擎會把請求保存到數據庫中。
通過化同步請求為異步請求,基本上99.99%的性能問題都可以解決。
2)用單機並行硬件處理。
如,使用GPU,FPGA等硬件來處理,提高性能。
3)用集群計算機來處理。
如,Hadoop集群,用多臺計算機來並行處理數據。
自己的軟件棧中,也可以把一個模塊部署多份,並行處理。
4)用cache來滿足請求。常用的內容加熱cache後,大量的用戶請求都只是內存讀取數據而已,性能會得到很大的提升。
cache是上帝算法,記得好像它的性能只比最佳性能低一些,就好像你是上帝,能夠預見未來一樣。現在X86CPU遇到了主頻限制,CPU提升性能的主要途徑就是增加高速Cache了。
4、大系統小做
遇到大型系統不要慌,把它切分成多個模塊,用多個小程序,通過SOA協作來解決。這秉承了Unix的設計思想。Unix上開發了大量單一目的的小程序,它主張用戶通過管道來讓多個小程序協作,解決用戶的需求。當然,管道方式通訊限制太多,不夠靈活。因此,現在我們可以通過URI,通過SOA的方式來讓多個程序協作。Andorid和iOS上的應用程序,現在都是通過URI實現協作的。這也算是Unix設計思想的現代發展吧?!
5、Sharding切片
現在有一個潮流,就是去IOE。I-IBM大型機,O-Oracle數據庫,E-EMC存儲。之前,大型系統常用IOE去架構,在大型機上部署一個Oracle數據庫,Oracle數據庫用EMC存儲保存數據。IOE是當今最強的計算機,數據庫和存儲。但他們面對海量系統也有抗不住的一天。
Oracle數據庫是Shareeverything的,它可以在一個計算機集群(服務器節點不能超過16個)上運行。計算機集群都共用一個存儲。
去IOE運動,標誌著ShareEverything模式的破產。必須使用ShareNothing,系統才能無限擴展。
用MySQL數據庫就可以應付任意規模的數據了。前提是,你會Sharding分片。把大系統切分成若幹個小系統,切分到若幹臺廉價服務器和存儲上。更Modern一些,就是切分到大量虛擬機上。
如,鐵道部的12306網站。我們知道火車票都是從屬於某一列列車的。那麽我們把每一個列車作為一個單元來切分,就可以把12306網站切分成幾千個模塊。一臺虛擬機可以承載若幹個模塊。當某些列車成為性能瓶頸之後,就可以把它們遷移到獨立的虛擬機上。即使最終有部分列出服務不可用,系統也不會完全不可用。
12306網站,只有一個全局的部分,就是用戶登錄。這個可以交給第三方負責。如可以讓用戶用微信,微博,qq等賬戶登錄。
也可以自己實現用戶登錄服務。還是用切片的方式用多臺Redis服務器提供服務。Redis服務器存儲每一個登錄用戶的sessionId和userId,角色,權限等信息。sessionId是隨機生成的,可選擇其部分bit用於標識它在哪一個Redis服務器上。用戶登錄後,把sessionId發給客戶。用戶每次請求時把sessionId發回給服務器。服務器把sessionId發給Redis服務器查詢得到其用戶信息,對用戶請求進行處理。如果在redis服務器上找不到sessionId,則讓用戶去登錄。即使所有註冊用戶同時登陸,也不需要太多的內存。而且,可以在session內存過多時,刪除最早登陸的用戶的session,強制他再次登陸。同時活躍的用戶數不會太多。
轉:http://gad.qq.com/article/detail/34123?sessionUserType=BFT.PARAMS.238546.TASKID&ADUIN=55664815&ADSESSION=1507605722&ADTAG=CLIENT.QQ.5503_.0&ADPUBNO=26634
編程能力七段論(上)