網上看了篇文章,覺得很不錯,抄在下面。因原文沒注明作者,在這也不知道作者是誰了。
編程概述
編程術可以看作實用魔法的一個分支。編程魔法師用時間和精力做祭品,與生活在計算機中的精靈訂立契約,以換取駕馭代碼的能力。
---fmddlmyy
要提高編程水平,唯有多看多寫。這篇文章看似經驗之談,實質還是灌水。本次灌水,擬定了以下幾個主題:
§??????
編程概述
§??????
軟件開發中的方法論
§??????
編程實踐
§??????
嵌入式編程的特點
編程概述
本文將從不同角度討論
“
什么是編程
”
,或者
“
什么是編程的本質問題
”
。這些討論并不是要得出什么定義,我們實際上是在表述各種編程思想,以期加深對編程的理解。
1
編程不是藝術
我們的世界是模糊的、連續的、不精確的,但軟件是精確、離散的、形式化的,這就注定了軟件不能完全描述現實世界。因此我們需要知道描述哪些部分,忽略哪些部分,這就是軟件的本質問題。
--- Tom Demarco
編程不是藝術。編程不追求完美,它的目的是解決問題。
和藝術上的
“
只能意會,不可言傳
”
相反,編程甚至不能忍受自然語言的模糊性,它要求問題被表述成可編譯、可運行的代碼,文字和圖表只是輔助交流的工具。
每個程序員有兩個面具:職業的和專業的。戴上職業面具后,程序員會用能找到的最好用的工具,以盡可能簡單的方式,在合理的成本內解決問題中必須解決的部分。
而戴上專業面具的程序員,會不厭其煩地學習各種編程知識(很多都不是職業需要的),積累經驗值,吸收可復用的模式和思想。他們會用大量時間去理解程序的表象和
CPU
的匯編代碼之間究竟發生了什么。他們有著探索未知領域和練功升級的強烈欲望。
他們在不斷接近技藝的完美,而這個技藝本身是以不談完美、但求有效的方式解決問題。他們付出了大量的努力,而這些努力的驅動力是好奇心和進取心。如同《魔法學徒》中描述的魔法師,一個魔法師所追求的東西只有志趣相投的魔法師才能理解,而不管他們出于哪個陣營。了解事物真相本身帶來的滿足就可以作為一切努力的回報,
2
控制復雜性
任何一個正在構建大型系統的人,天天面對的中心議題就是:如何剔除不必要的、人為的、自找的復雜部分,并控制好剩下的,無可逃避的復雜性。
--- Betrand Meyer
編程可以被看成一種管理工作,管理的對象是代碼,控制的對象是代碼的復雜性。
中國的傳統思維比較喜歡談本質,追求一種稱作
“
道
”
的東西。而在編程上,表象和本質同樣重要。所有程序說到底不過是一些匯編語句的組合,但了解這個本質在大多數場合都不能有助于解決實際問題。
寫代碼是為了解決實際問題。當代碼的數量增加到一定程度,對代碼自身的控制也會成為一個重要的問題。數量改變了本質。
管理的要訣是削弱、孤立被管理的對象,
“
使民無知
”
,
“
使民
”“
雞犬之聲相聞
”
卻
“
至老死
”
而
“
不相往來
”
。每個被管理的對象在完成自身工作的前提下,對其它對象的了解應當盡可能少。通過盡量降低對象間的耦合程度來控制復雜性。
只有有效地控制復雜性,我們才能使用越來越大的信息塊,駕馭越來越多的代碼,用這些
0
和
1
的操作去實現前人沒有,甚至無法完成的工作。
3
復用
編程技藝的核心是代碼的復用。復用已有的知識是積累、提高的前提,否則就會像誰誰誰那樣每天推石頭上山,而不能累進。在上帝看來,重復是一種懲罰的手段。
編程這個職業知識更新比較快,可以學習的東西也很多。有些人覺得累,但有些人卻覺得其樂無窮,覺得從事這個職業是很幸運的事情。
編程的實質是建筑,根據應用的需求,不斷建筑更大的信息塊。我們所寫的所有程序,都可以被看作對語言的擴充。事實上,我們在不停地開發新的語言,我們所寫的每個函數、每個類都是在為現有語言增加新的功能。我們按照適合特定應用的模式,組合各種信息塊,完成實際的應用。這些信息塊有的是我們自己做的,有的是拿來的。
信息塊可以被組合的關鍵就是簡單、明確的接口。所以,我們應當針對接口編程,而不是針對應用編程。應用意味著變化和不可復用。將應用合理分解為模塊,定義好模塊間的接口,然后按照接口構建模塊。模塊分解的原則是:
§??????
模塊的耦合程度盡可能得低(有人稱此為最少知識原則);
§??????
接口盡可能簡單(有人稱此為接口隔離原則);
§??????
如果將變化的因素封裝到模塊中,每個模塊應該只封裝一個因素(有人稱此為單一責任原則);
§??????
使特定于應用的代碼盡可能得少
。
針對接口編程的好處有兩個方面:
§??????
信息塊可以被復用;
§??????
復雜性可以被更好地控制。
人們用各種方式復用知識,其實
C
語言本身又何嘗不是知識的復用呢?其它復用方式包括:庫函數、新的語言和編譯器、新的腳本、類庫、程序框架、設計模式、面向對象、面向組件、面向服務、面向方面、面向領域、各種開源代碼、開發環境提供的各種
Wizard
和糖衣、各種代碼生成工具等等。充分了解編程環境,善于復用各種資源,是程序員的基本功。
?
軟件開發中的方法論
?
1
項目管理的方法論
1.1
方法論
方法論的英文為
Methodology
,編程的方法論應該是指軟件開發的一整套方法、過程、規則、實踐、技術。不過我們一般提到的方法論都偏重于項目、過程和人員的管理。
《
Agile Software Development
》的作者
Alistair Cockburn
提出方法論具有以下要素:角色、個性、技能、團隊、技術、活動、過程、產品、里程碑、標準、質量、工具、團隊價值,它們的關系可以用一幅圖來表示:
?
雖然將這幅圖貼在這里,事實上我不了解這些要素及其關系的確切定義,對于這些不能精確描述的東西,我接受起來比較困難。
其實,項目管理的核心是溝通和反饋。只要能夠保證良好的溝通和即時的反饋,開發團隊即使并沒有采用先進的方法論,一樣可以成功。從另一個角度說,過程和工件能輔助,但不能保證開發人員、項目經理和客戶的良好交流。
1.2
重型方法
有的方法論規定了大量的中間文檔和復雜的過程管理。那些中間文檔被稱為
artifact
,或工件。需要大量
artifact
和軟件開發方法被稱作重型(
Heavy Weight
)方法。
這些復雜的方法來源于恐懼。
在中大型的項目中,項目經理往往遠離代碼,他們無法有效的了解目前的工程的進度、質量、成本等因素。為了克服未知的恐懼感,項目經理制定了大量的中間管理方法,希望能夠控制整個項目,最典型的莫過于要求開發人員頻繁地遞交各種報告。重型方法中的基本假設是過程(及各種
artifact
)比個人可靠。
雖然很多輕型方法都將重型方法作為反面例子,但對于大多數大型項目,重型方法是管理所必需的。
1.3
輕型方法
為了解決重型方法存在的問題,業界出現了很多輕型(
Light Weight
)方法論。提出這些方法論的部分作者結成了一個聯盟:敏捷軟件開發。他們還有一個宣言:
§??????
Individuals and interactions over processes and tools.
§??????
Working software over comprehensive documentation.
§??????
Customer collaboration over contract negotiation.
§??????
Responding to change over following a plan.
在這些宣言后面還有很多原則。概括起來主要是:尊重個人,強調溝通和反饋,與客戶緊密合作,保持設計的簡單性等等。敏捷方法在重型方法論和無管理狀態之間尋求一個平衡點,希望用低成本的管理活動帶來最大的產出。
2
編程的方法論
作為程序員,我更感興趣的是可以指導編程的方法論。
2.1
測試驅動開發
2.1.1
未謀進,先謀退
在開始一件事情之前,必須先明確什么時候可以停止。什么
“
止于至善
”
,在編程工作中可以一腳踢開。我們必須明確要做什么,做到什么樣子就算完成了。
怎樣才算明確呢?最理想的方法是先寫一個測試程序,然后再編寫代碼,讓測試通過。測試程序規定了停止的必要條件。
測試程序是針對接口編寫的。要寫出測試程序,必須先定義好接口,然后針對接口編程。測試程序同時示范了接口的使用。
如果有一個運行很方便的測試程序,我們在維護代碼時,就放心得多。可以經常跑一跑,測試一下,以保證測試要求的功能還沒有被破壞。相反,如果沒有測試程序,我在修改代碼的
bug
時,心里就很不踏實,因為我不清楚我的修改是否會引入新的
bug
。
2.1.2
保持可運行,可調試的狀態
“
先寫測試程序
”
適合沒有
UI
,功能相對簡單的模塊。對于功能復雜的軟件系統,需要測試的方面很多,測試時間很長,在開發前就建立一個比較全面的測試,并且隨時使用,有時是不可能的。
但我們至少應該保證:寫任何程序,都要盡量保持在可運行、可調試的狀態。即使要為此寫一些額外的程序,都是值得的。在不能運行的情況下,編寫大量代碼是不可思議的事情。
2.1.3
可測試性
IC
設計上有一個
“Design For Test”
的說法,即
IC
的設計中必須要考慮到如何測試,留好測試的接口。軟件開發也是一樣的,我們寫每段代碼,都要考慮一下這段代碼是否可以測試,為了保證可測試性,必要的時候可以修改設計。
2.2
重構
《重構》這本書內容樸實、但對我個人影響很大。在了解
“
兩頂帽子
”
和
“
小步前進
”
的方法后,我敢于修改任何代碼,甚至是我不熟悉底層邏輯的代碼。
2.2.1
兩頂帽子
我們有兩頂帽子:一頂是不改變功能的前提下,改善現有現有程序的設計;另一頂是增加新的功能,以適應需求變化。我們在任意階段,應該只戴一頂帽子,絕對不能同時戴兩頂帽子。
在增加新功能的時候,往往需要先改進現有的代碼結構,使之能更好地適應變化。如果功能有較大變動,我們應該將這變動分解成盡可能小的步驟,并讓改進代碼和新增功能的小步驟交替進行。將原有代碼平滑地演變到新代碼,既增加了功能,又改善原有代碼的設計。
重構的正式定義應該是
“
在代碼寫好之后改進它的設計
”
。但對我而言,重構的思想已經融入了我的開發過程。在開發中,我同樣按照
“
兩頂帽子
”
和
“
小步前進
”
的方針平滑地演變代碼,從無到有,逐步完善。
2.2.2
代碼的壞味道
重構的對象是消除
“
代碼的壞味道
”
,保持新鮮、健康的代碼。《重構》中列舉了一些典型的壞味道,例如重復代碼、太長的函數、太長的參數列等等。
2.2.3
設計和重構
存在這么一種說法:
“
設計不再是一切動作的前提,而是在整個開發過程中逐漸浮現出來
”
的。
事實上,在沒有任何軟件工程理論的時代,編程基本上是寫到哪兒算哪兒。歷史是螺旋上升的,在投影上很接近的兩個點在鉛垂線方向上是處于不同高度的。
重構是要糾正過度設計的傾向,告訴程序員:即使開始的設計有缺陷,也沒關系,更不要僵化地保持,我們可以重構代碼,平滑地修改原有設計。這并不是說前期設計不重要。
“
設計是在整個開發過程中逐漸浮現出來
”
,這是在說隨著開發的深入,我們會對很多問題考慮地更加清楚。在必要的時候,我們應該用重構的方法靈活地改變設計,適應變化。
?
?
編程實踐
鳩集遺失,鑒玩整理,晝夜精勤,每獲一卷,遇一畫,畢孜孜葺綴,竟日寶玩,可致者必貨敝衣
,
減糧食。妻子童仆切切嗤笑,或曰:終日為無益之事何補哉。既而嘆曰:若復不為無益之事,則安能悅有涯之生。
---
唐
張彥遠
《歷代名畫記》
1
編程的要素
編程有
3
個要素:語言、環境和思想。
1.1
語言
有人喜歡爭論語言的優劣。其實,除了匯編語言,各種語言、腳本、標準庫、類庫、框架都蘊含著大量成熟的編程經驗和思想。程序員應該多熟悉一些語言,特別是有代表性的語言。
個人覺得,一個程序員應該掌握一兩種匯編語言(
CISC
的
X86
、
RISC
的
ARM
)、一種面向過程語言(
C
)、兩、三種面向對象語言(
C++
、
Java
、
Delphi
)、一兩種腳本語言(
perl
、
python
、
ruby
)。如果有時間,可以再學習一些學術性較強的語言,例如
Scheme
。
學習新的語言,不僅可以吸收語言中蘊含的設計理念,還可以打開連接新空間的大門,使我們可以學習、復用使用該語言的各種資源,例如源代碼、文章、書籍。
1.2
環境
1.2.1
開發時和運行時
環境可以被理解為程序所有外界環境的總和,包括開發時環境、運行時環境。我們在寫一段程序時,應該對該程序的相關環境有清楚的了解。
開發時環境包括我們使用的編譯(鏈接)環境、復用的代碼(框架、類庫、控件等),系統的邏輯結構、代碼的文件組織、需要的工具軟件、調試環境等等。
運行時環境包括程序運行時環境中所包含、發生的一切,特別是與我們的程序相關的部分。從小處看,我們應該知道如何確定每段代碼運行的
context
(線程和堆棧),每個變量所使用的空間的信息。從大處看,我們應該了解系統運行的來龍去脈,各個模塊(邏輯概念)如何相互配合,各個線程(調度單位)如何相互通信,什么時候可能發生調度,系統中有哪些不確定的因素等等。
1.2.2
軟件類型
比較
“
典型
”
的軟件類型包括:
1.??
Windows
操作系統;
2.??
Linux
操作系統;
3.??
編譯器;
4.??
虛擬機;
5.??
調試器;
6.??
Windows
的單機應用程序;
7.??
Windows
的驅動程序;
8.??
Linux
的應用程序和驅動程序;
9.??
基于
socket
的客戶端
-
服務器程序;
10.??
數據庫應用程序;
11.??
使用數據庫的
Web
應用程序;
12.??
使用
RTOS
的嵌入式軟件;
13.??
不使用
RTOS
的單片機程序;
14.??
DSP
程序;
15.??
嵌入式環境的第三方程序(
J2ME
應用、
BREW
應用、
symbian
的應用程序等等);
16.??
各種中大型程序的腳本環境,插件的開發、運行環境;
17.??
Office
應用程序開發;
18.??
flash
編程;
軟件的種類實在太多,所謂
“
典型
”
只是我個人的理解,肯定還有很多軟件類型沒有被列出來。程序員應該了解開發和運行這些軟件時,究竟發生了什么。
對于
Windows
、
Linux
操作系統,我們應該有個概要的了解:從
BIOS
程序讀
Master Boot Record
,到系統的裝載運行;應用程序或動態連接庫(
Linux
的
SO
)的裝載;操作系統的基本模塊(包括內核)的功能;
Windows
如何通過
COM
機制將各個功能模塊組合起來等等。這些內容是
PC
程序的基本運行環境。
嵌入式環境相對
PC
環境要簡單一些,特別是用
NOR flash
,代碼直接從
ROM
運行的系統。一方面,程序員通常可以看到系統運行的所有代碼;另一方面,嵌入式環境有時不提供第三方程序運行機制,有時提供比較簡單的機制,有時用
java
虛擬機當作第三方程序運行機制。不過,智能手機的應用處理器一般使用
NAND flash
,從
BIOS
運行一小段啟動代碼,將系統裝載到
RAM
運行,同時支持應用程序裝載運行,已經很接近
PC
環境。
編寫單機應用程序,除了語言外,主要要熟悉各種庫、框架、組件。一些通用庫提供了常用函數、各種容器和算法、
GUI
、典型的程序框架,系統
API
的封裝等等,例如:
§??????
移植性比較好的
C
標準庫、
STL
、
boost
、
TK
等;
§??????
Windows
上的
MFC
、
VCL
(
Delphi/C++ Builder
);
§??????
Linux
上的
ncurses
、
X Windows
、
GTK
、
Qt
等;
§??????
訪問數據庫:
VC++
的
ODBC
、
DAO
、
ADO
,
Borland
的
BDE
等;
§??????
Borland
做了一些在源代碼級跨平臺的庫:
Delphi/Kylix
的
CLX
、
dbExpress
;
§??????
CPAN
上的大量
perl
模塊、
java
的類庫等等;
上面列出的是一些比較通用的庫。還有用于各種語言的大量專用庫,各種提供二進制接口的組件、控件。
編譯器、虛擬機也是單機應用程序(在嵌入式環境,虛擬機可能是系統軟件的一個模塊)。不過它們的地位比較特殊。作為程序員,我們應該了解編譯器、集成開發環境、軟件框架、虛擬機、操作系統分別為我們做了什么。
作為程序員,我們同樣應該理解調試器也是一個應用程序,調試器的基本原理,它能做什么,有什么限制。如果調試器與目標程序運行在不同的
CPU
上,調試是如何實現的,有哪些不同的實現方式?例如
JTAG
調試利用了目標
CPU
的調試接口直接調試目標程序;串口調試要求將一段調試代碼和目標程序鏈接在一起下到目標
CPU
中,由嵌入的調試代碼與
PC
機的調試器通信實現調試。不同的實現手段決定了調試器的能力和限制。
目前最熱門的軟件類型就是使用數據庫的
Web
應用了,例如各種網站、網絡游戲、各種企業、行業、政府機構的管理系統,在這個領域集中了處于食物鏈不同環節的大量廠商,各種
Web
服務器、基礎平臺、應用開發框架。隨便列一下,也能列出一堆名詞:
§??????
HTML/CSS
和
CGI
;
§??????
java applet
、
java script
、
ActiveX
控件;
§??????
php
、
asp
、
jsp
、
servlet
;
§??????
.net
家族:
asp.net
、
ado.net
等;
§??????
J2EE with/without EJB
、
Spring
、
Struts
、
Hibernate
;
§??????
Ruby on Rails
、
Plone
等;
在一種軟件類型上,集中了這么多開發技術、框架、模式,也可以稱得上蔚為壯觀了。不過,這個領域里確實是各種最新的編程思想、方法、設計模式的演武廳,如同當年的編譯器,值得所有程序員研究、學習。
1.3
思想
COM
可以被看作
OLE
發展的衍生品。但它的重要性遠遠超過了
OLE
。它首被獨立出來,成為
OLE
、
ActiveX
的基礎,然后逐步成為在
Windows
進行二進制集成的基礎。
COM
和
RPC
的結合產生了
DCOM
。
DCOM
和
MTS
的結合產生了
COM+
。雖然這些技術都是用于
Windows
平臺的,但組件技術的基本思想是獨立于具體環境的。也就是說,對程序員而言,存在著獨立于語言和環境之外的領域,這就是編程的思想。
例如看看
Qualcomm
的
BREW
,就會發現它從
COM
中學習了多少東西。嵌入式平臺的程序員使用
PC
平臺的技術,這就是編程思想的價值。對于程序員來說,各種編程思想、設計模式,是最為寶貴的東西。這里所說的設計模式并不局限于
GOF
的《設計模式》,任何慣用的手法都可以被看作模式。
一些基本的編程思想起源于更普遍的智慧。例如:
心智的活動,除了盡力產生各種簡單的認識之外,主要表現在以下三個方面:
1)
將若干簡單認識組合成一個復合認識,由此產生出各種復雜的認識。
2)
將兩個認識放在一起對照(在這樣做時并不將它們合而為一),不管它們如何簡單或者復雜,由此得到有關它們的相互關系的認識。
3)
將有關認識與那些在實際中和它們同在的所有其他認識隔離開,這就是抽象。所有具有普遍性的認識都是這樣得到的。
--- John Locke,
有關人類理解的隨筆
程序員從這段
1690
年的文字中能看到什么。組合、對照、抽象,這些基本的思維工具同樣也是程序員最基本的工具。
2
編程規則
編程是一門技藝,如同我們必須跳到水里才能學會游泳,我們必須多看、多寫程序,才能學會編程。
任何技藝的學習都包括知識和能力兩個部分。知識是明確的,可以用時間換取。能力是說不清的,不易掌握的。一般而言,能力高的人能夠更快地積累知識,善于從已有的實踐中總結規律,善于復用已有的模式,他們能更快地對復雜的環境形成清晰的認識,用最簡潔、優雅的方式解決問題。如何提高編程能力應該是因人而異,每個人應該找到自己的學習路徑,有自己的規劃。
編程的表象千變萬化,但在表象背后,還是有一定規律的。在某些條件下,這樣做會比那樣做更好一些。我們可以將這些東西稱作經驗、原則、規則,或者其它任何名詞。
下面松散地列舉了一些規則。我們應該在實踐中檢驗已知的規則,總結自己的規則。
規則一
這世界上唯一的真理就是不要盲目相信真理
從某種意義看,原則、規矩這類東西就是用來打破的。所謂原則,就是對歷史上某種經驗的總結。如果我們踏入的河流和前人非常接近,我們可以參考前人的經驗。我們是主動地拿來,而不是被動的遵守。
規則二
未蘊而變,自欺也;知律而變,智者之道也
這句話談的是學詞,必須先了解格律,然后才談得上變化,否則就是自欺欺人了。在編程上,也是一樣。在有資格打破一條原則前,首先要了解這條原則。先了解事物的規律,然后才是變化和靈活的應用。不要打倒自己不了解的東西。
規則三
尋找結構和成本的平衡點。當結構不能容納變化時,重構代碼到新的平衡點
開閉原則要求:一個軟件實體應該對擴展開放,對修改關閉。即軟件的結構要達到:允許在不修改軟件的前提下擴展軟件功能。
但好結構是要花成本的。程序員總是在折衷,尋找結構和成本的平衡點。我們會放一些余量,讓結構能承受一定的變化。
規則四
要針對接口編程,不要針對應用編程
前面談過了。我們總希望通過工作得到一些收獲,積累一些經驗。只有將代碼分解成盡可能獨立的模塊,然后針對接口編程,才能保護我們的成果,讓我們有所積累。
規則五
最少知識原則:模塊對外界的了解應當盡可能少
前面也談過了。所謂
“
圣人之治
”
,經常是
“
虛其心,實其腹;弱其志,強其骨,常使民無知無欲
”
。讓被管理的對象盡可能得弱,對象間的關系盡可能得簡單,可以降低管理的成本。
在面向對象編程中
“
組合優于繼承
”
,就是因為繼承將基類的實現細節暴露給子類,兩者的耦合性太強了。另外,繼承的實現是靜態的,而組合可以更靈活地實現。
規則六
尊重習慣
在任何領域編程,應該盡量了解這個領域的習慣,盡量符合這些習慣。這樣,別人可以更容易地理解和使用我們的程序,更不容易出錯。
規則七
程序中不要出現
magic number
或其它神奇的東西
magic number
就是諸如
17
、
42
這類數字。請用有描述性的常數名替代它們。常數名、變量名、函數名就是最好的注釋。結構清晰的代碼甚至可以不需要其它注釋。
“
需要很多注釋的代碼
”
是代碼壞味道的一種,常常意味著需要重構代碼,使它更容易被理解。
規則八
“
兩頂帽子
”
和
“
小步前進
”
“
兩頂帽子
”
前面講過了。面對復雜的問題,我們不僅要從結構上分解它,還要將實現步驟盡可能地分小,在一時一地以壓倒性優勢解決問題的一小部分。
n-1
個總比
n
個好對付,我們越來越強大,敵人越來越弱小,我們要做的只是將優勢保持到最后。
“
保持優勢
”
的關鍵是正確的理解、合理的劃分。
規則九
適當地引入中間層,可以解決所有問題
這條規則是實質是
“
抽象
”
。在遇到問題時,多引入一層接口,將問題封裝起來。
抽象本身并沒有消除系統的復雜性,但它減少了在某個時刻需要處理的細節的數量。或者說,抽象將復雜性控制在接口和模型的后面,可以推遲我們處理這些復雜性的時刻。
這條規則的另一個說法是
“
多做白日夢
”
。假設我們已經得到了需要的東西,然后會怎么樣?通過
“
白日夢
”
我們可以在更高的層次考察問題,可能找到解決問題的線索,也有可能發現這個問題毫無價值。即使毫無收獲,我們也的損失也很小。
規則十
不要依賴調試,盡可能地依賴分析和推理
我經常會在頭腦中預演調試的過程,計算我要做什么,我能得到什么。
“
預演
”
的結果經常是我沒有必要這樣調試,或者應該試一試另一個方法。
根據測不準原理,任何實際的測量、調試都會改變、影響被研究的對象。用調試輔助、驗證我們的推測,但不要依賴調試。
?