1. 程式人生 > >大話面向物件的前世與今生

大話面向物件的前世與今生

作者:Gary Chan   來源:CCW

歸根結底,計算機的發展史可以歸納為“抽象”兩個字。應用儲存程式的理論,人們從最早的現代計算機抽象出軟體(Software)和硬體(Hardware)兩個獨立部分。為了讓軟體各司其職,軟體又被抽象成專門與硬體打交道的作業系統(Operating System)和建築在作業系統之上的應用軟體(Application)。資料處理又是許多應用軟體必須的前提,從而抽象出資料庫系統(Database System)。到了網路時代,為了更好地適應網路軟體的開發,應用軟體中又抽象出應用伺服器(Application Server)提供各種服務。

程式語言的發展亦復如是。讓我們在這回的咖啡館中看看程式語言的發展簡史,從頭認識Java中的面向物件程式設計技術。

一、前世

1946年2月15日,隨著第一臺現代電子計算機ENIAC轟鳴著來到這個世界,編寫程式也成為三百六十行之外的一個嶄新職業。我們稱編寫程式的工程師為程式設計師或者開發者。

ENIAC是一臺重達30噸的龐然大物,由19000多個電晶體、1500多個繼電器組成。為了給它下達指令,程式設計師必須通過不同的連線線組合進行程式設計。要編制執行新的程式,還必須拔掉連線重新來過。整天面對二進位制程式設計的工作相當枯燥乏味,而且是直接對程式地址讀寫,自然出錯頻繁。

閱讀由連線表達的程式更不亞於揣摩天書,維護和改造程式的價格成本居高不下。更要命的是,早期的計算機制造價格相當昂貴,而在程式編制除錯完成之前,計算機不得不一直空轉,導致軟體開發的費用竟然遠遠超過硬體的投入。

為了解決軟體開發的難題,電腦科學家發明了組合語言,通過一些助記符來減輕二進位制編碼的開發壓力。這的確是行之有效的方法,直到現在,程式設計師在開發中還常常使用嵌入式彙編來提高軟體執行速度,遊戲引擎更是如此。然而,組合語言太依賴程式設計師的素質,而且無法適應大規模的開發。

由於一次傳奇般的投資,Mark I計算機把IBM從生產製表機、肉鋪磅秤、咖啡碾磨機等亂七八糟玩意的行業,領入了計算機制造業的領地,最終成為如今的藍色巨人。本系列文章中曾介紹過Mark I三個程式設計師之一的數學家Grace Hopper是如何創造了“BUG”和“DEBUG”這兩個計算機史上著名的兩個名詞的。而這位Hopper女士,實在是一個不得了的人物。

1952年,Hopper覺得用機器碼程式設計比較原始,為什麼不能用類自然語言編寫程式,然後再用一個工具把它轉換成機器碼呢?不久,她就開發出世界上第一套編譯器A-0,是現代編譯技術的原型。1956年她在第一臺儲存程式的商業電子計算機UNIVAC I、II上開發出B-0,之後叫做FLOW-MATIC,它導致了計算機商用語言COBOL(COmmon Business Oriented Language)的誕生。雖然Hopper有著“電腦之母”的美譽,但是傳說她辦公室有一個倒著走的鐘,以及一面秀著骷髏頭的海盜旗。

到了六十年代,FORTRAN (FORmula TRANslating)、COBOL、LISP、ALGOL 60等現代高階語言的出現了。程式設計師可以用接近自然語言的程式語言編制軟體,然後通過編譯器轉換成機器可執行的程式碼。由於使用精確的形式語言來定義程式語言本身,並且通過對硬體的抽象使得程式與計算機平臺無關,導致高階語言生產效率大大提高,維護費用自然降低不少,計算機軟體業終於得以蓬勃發展。

好景不長。隨著軟體大規模的應用,程式的開發方法和管理手段逐漸無法跟上軟體規模的膨脹,從而導致了軟體危機的出現。就拿1963~1966年間的IBM 360系統來說,該系統有100萬行的程式碼量,IBM每年動用5000人來維護該系統,但是,每個版本都是從上一個版本找出1000以上個錯誤而修訂的結果,好像越改錯誤越多,根本沒有改善的跡象。有人把IBM 360系統形容為一隻逃亡的野獸落到泥潭中做垂死的掙扎,越是掙扎,陷的越深,最後仍然無法逃脫滅頂的災難。

人們不得不停下腳步思考,到底哪裡出了問題。回想自己,每個人做事情,都是列舉重點,然後細化並逐個完成。比如製造自行車,肯定是先把自行車按照功能分塊,先造車架,然後是兩個車輪,接著是踏板等傳動裝置,最後才是坐墊、車鈴等零件。而製造車輪,肯定是要分別製造鋼圈、鋼絲、輪胎,而輪胎有分內外胎。如果軟體開發能夠遵循這種從大到小、逐步精確的思想,是不是能夠解決這個軟體危機呢?

沒錯,這種結構化的抽象分析方法,導致了結構化程式設計方法的誕生。

凡是學過一點計算機知識的人大概都知道“資料結構+演算法=程式”這一著名公式。提出這一公式的瑞士電腦科學家Niklaus Wirth由於發明了多種影響深遠的程式設計語言,並提出結構化程式設計這一革命性概念而獲得了1984年的圖靈獎。

Wirth開發的PASCAL在資料結構和過程控制結構方面都有很多創造,比如Java中字元型、引用型,以及if-then-else、while、for等多種控制結構,都是從PASCAL裡面借鑑發展而來的。可以說,現代程式設計語言中常用的資料結構和控制結構絕大多數都是由PASCAL語言奠定基礎的,因此PASCAL在程式設計語言的發展史上具有承上啟下的重要里程碑意義。現在你知道為什麼很多計算機專業的學生都要學PASCAL語言了吧。
 
1971年,Wirth基於其開發程式設計語言和程式設計的實踐經驗,首次提出了“結構化程式設計”(structured programming)的概念。這個概念的要點是:不要求一步就編製成可執行的程式,而是分若干步進行,逐步求精。第一步編出的程式抽象度最高,第二步編出的程式抽象度有所降低,最後一步編出的程式即為可執行的程式。用這種方法程式設計,似乎複雜,實際上優點很多,可使程式易讀、易寫、易除錯、易維護、易保證其正確性及驗證其正確性。

結構化程式設計方法又稱為“自頂向下”或“逐步求精”法,在程式設計領域引發了一場革命,成為程式開發的一個標準方法,尤其是在後來發展起來的軟體工程中獲得廣泛應用。有人評價說沃思的結構化程式設計概念“完全改變了人們對程式設計的思維方式”,這是一點也不誇張的。

Wirth開發PASCAL的初衷是為了有一個適合於教學的語言。但一經推出,由於它的簡潔明瞭、提供豐富的資料結構和控制結構,使得程式開發大為簡便,竟然大受歡迎。在C語言問世以前,PASCAL是風靡全球、最受歡迎的語言之一,不但創下了發行拷貝數最多的世界記錄,而且成為大學資料結構教學的“惟一官方指定”語言。

Phillipe Kahn是Niklaus Wirth的學生,畢業後到美國加利福尼亞州創立了Borland公司,憑藉拳頭產品Turbo PASCAL,當時就賣出了100多萬個拷貝,成為百萬富翁。而Borland公司是程式設計師津津樂道到程式開發工具供應商,他們從最早的Turbo PASCAL、Turbo C、Turbo PROLOG等Turbo系列,到如今的Delphi、C++ Builder、JBuilder、C# Builder系列,無一不是舉足輕重的開發工具,從而在開發者心目中有著崇高的地位。

二、今生

雖然結構化程式設計使得程式設計師世界觀經歷了巨大變革,行之有效地解決了軟體開發中的許多問題,然而,結構化程式設計並不能完全解決軟體危機,人們仍然渴望生產效率更高、更可靠、易維護、易管理的開發思想和開發方法。

實際上,人們認識世界,是有一些基本的法則的:

·區分事物及其屬性,如自行車和車子的顏色。

·區分整體物件及其組成部分,如區分自行車和車輪。

·不同物件類的形成及其區分,如山地自行車和兩人休閒車雖然有相當的區別,但都屬於自行車這個型別。

心理學研究表明,客觀世界由許多物件組成,物件具有其屬性和行為,之間存在著各種聯絡,這樣能夠更好的刻畫問題域,也更接近人類的自然思維方式。這就是面向物件程式開發思想的由來。

物件的概念最早出現於五十年代人工智慧的早期著作中,而OO(面向物件)的實際發展始於1966年的Kisten Nygaard和Ole-Johan Dahl開發的Simula語言。正如名字昭示的,Simula可以模擬客觀世界。比如在著名的銀行出納問題中,你可以建立若干個出納員物件,若干個客戶物件,還有若干錢物件以及交易物件(即把存款、提款等交易動作看成一個物件)—— 這個世界是由物件組成的。所有出納員物件,除了各自的狀態不同,都是屬於的出納員這個抽象類別。出納員物件和客戶物件之間通過訊息傳遞進行互動,並且最終生成若干個交易物件,而交易物件可以操縱錢物件,完成存款或者提款的動作。

你看,這個銀行櫃檯世界,是不是完全可以由物件模擬呢?從而,面向物件設計程式,主要就是設計抽象的類。

面向物件程式設計思想是一個里程碑。Alan Kay設計了世界上第一個完全面向物件的語言Smalltalk併成為圖靈獎得主,Bjarne Stroustrup明智地把面向物件和最流行的C語言結合而開發了有史以來取得最大成功的C++語言,Anders Hejlsberg把PASCAL的面向物件版本Object PASCAL結合構件的思想開發出Windows平臺上最優秀的快速程式開發(RAD)工具之一Delphi,James Gosling結合Internet背景開發了Java語言,Bill Gates把.Net體系結構完全構築在面向物件之上…… 雖然面向物件只是從語法上引入為面向物件服務的封裝、繼承、多型等概念,但是必須看到,OO並非一種特殊的規定或者行業規範,而是一個優秀的理念,學習Java,應該把OO當作指導思想。

面向物件程式設計

如果你是Java咖啡館的常客,那麼在不知不覺中你早已接觸並運用過Java的面向物件知識。在這回的咖啡館中,讓我們詳細剖析一個面向物件程式設計的例項,把知識鞏固下來。

回顧一下,類是定義了從類生成的例項(instance)中的資料和方法的關係的模板。有人喜歡把類比作圖章,圖章敲出來的圖案便是物件,的確很形象。

Java中用class關鍵字來定義類,不過我們用Eclipse來定義更加方便。仍然用Eclipse新建一個叫做Namer的類,記得不要在public static void main(String[] args)前面打勾,確定後Eclipse便生成一個新的Java原始檔Namer.java,裡面的程式碼如下:


public class Namer {

}
 

這個類非常簡單,可惜不能做任何事情。

1、封裝

面向物件程式設計中,一個非常重要的技術便是封裝,也就是把客觀事物封裝成抽象的類,並且類可以把自己的資料和方法只讓可信的類或者物件操作,對不可信的進行資訊隱藏。這樣做的好處在於可以使類內部的具體實現透明化,只要其他程式碼不依賴類內部的私房資料,你便可以安心修改這些程式碼。此外,這樣做也是出於安全方面的考慮,如果代表網上支付卡密碼的變數隨便就可以被訪問到,這樣的系統誰還敢用呢?

封裝主要依靠對類、資料和方法的訪問控制,從語法上講就是加上private、protected、public等關鍵詞,如果沒有關鍵詞修飾則預設為package。它們控制權限如下表所示:

Specifier    類 子類 包 世界

private      X

protected  X    X     *    X

public        X    X     X    X

package    X    X

注意上面的X*,父類的protected部分,只有在與父類在同一個包內的子類才能夠訪問,否則也是不可訪問的。

讓我們結合例項理解一下。稍微把Namer類改一下:
public class Namer
{
    protected String surname;
 // 姓
    protected String firstname;
 // 名

    public String getFirstname()
 {
        return firstname;
    }

    public String getSurname()
 {
        return surname;
    }
}
 

這個類有兩個String型別的成員變數,surname和firstname,分別用來儲存姓和名。這兩個成員變數前都有protected修飾詞,按照表格,這兩個變數僅能夠被類本身、子類以及包中其他類操作,而包外的類則無權訪問。不過,為了跟包外的程式碼進行溝通,Namer類提供了getFirstname和getSurname這兩個public的方法。從而,對包外的類而言,姓名資料是隻讀的。


2、繼承

物件是用類來定義的。通過類,你能夠充分了解物件的全貌。比如,一說起自行車,你就會聯想到自行車是有兩個輪子、車把以及腳踏板。

更進一步,面嚮物件語言的另一個特點便是允許從一個已有的類定義新的類。比如,山地車、公路賽車和兩人三輪車都是自行車。在面嚮物件語言中,你可以從一個已經有的自行車類定義山地車類、公路賽車類等等。山地車類、公路賽車類都稱為自行車類的子類,自行車類是它們的父類,而這種定義關係,便是繼承關係。

子類繼承了父類的屬性。比如,山地車、公路賽車都是有兩個輪子一個車座。子類也可繼承父類的方法,比如山地車、公路賽車、兩人三輪車都可以前進、剎車、轉彎等。

當然,子類並不限於繼承,還可以發揚光大。比如兩人三輪車便顛覆了自行車只有兩個輪子、一個座墊的屬性,使得自己更加休閒瀟灑。

讓我們看看如何運用繼承來處理名在姓之前的模式。這種模式中,由於姓和名是用空格分割的,所以程式如下:

class FirstFirst extends Namer
{
    public FirstFirst(String s)
 {
        int i = s.lastIndexOf(" ");
  // 搜尋空格
        if (i > 0)
  {
            firstname = s.substring(0, i).trim();
            surname = s.substring(i + 1).trim();
        }
    }
}

FirstFirst類通過extends關鍵詞表示對Namer類進行繼承,只有一個類方法,名字恰好是FirstFirst。這並不是一個巧合。

所有的Java類都擁有若干特殊方法用來初始化物件,它們稱為建構函式,特徵就是與類同名,可以帶有或者沒有引數。這種同名函式不同引數的現象,在面向物件中稱作過載(Overload)。拿以前使用new操作符生成隨機數的程式碼來說:
Random random = new Random();

new操作符例項化一個Random物件後,緊接著就呼叫了Random類的建構函式進行初始化,只不過這個建構函式沒有引數。沒有引數的建構函式,稱為預設建構函式。預設的建構函式是每個類都擁有的,即使沒有宣告在程式碼中,Java編譯器在編譯時也會自動加入。

回過頭來看FirstFirst類。FirstFirst類繼承自Namer類,從而也擁有自己的firstname和surname屬性。在FirstFirst類的建構函式中,通過解析引數s,通過搜尋空格的方法來解析出空格前面的名和空格後面的姓,從而執行:
FirstFirst parser = new FirstFirst("Gary Chan");

之後,我的姓和名已經解析出來並且分別儲存在firstname和surname變數中了。同時,FirstFirst類繼承了Namer的方法,從而便可以通過如下語句來返回姓——Gary了:
String mySername = parser.getSurname();

注意,我們並沒有在FirstFirst類中定義getSurname()方法,這是從父類繼承來的,這就是程式碼重用的概念,避免了無謂的重複勞動。有了上面的基礎,再來編寫名在姓之後的模式:
class FirstLast extends Namer
{
    public FirstLast(String s)
 {
        int i = s.indexOf(",");
  // 搜尋逗號
        if (i > 0)
  {
            surname = s.substring(0, i).trim();
            firstname = s.substring(i + 1).trim();
        }
    }
}

由此可見,Namer類的兩個子類擁有它全部的屬性和方法,並且在其之上更加入瞭解析姓名的能力,而程式碼卻增加不多。程式碼重用,這是面向物件的主要魅力之一!