任何一種面向對象語言都有它的庫。任何一種面向對象的語言也都離不開庫的支持。用我們熟悉的 面向對象語言為例子,C++有STL,java有API函數,具體到開發工具,Visual C++提供了MFC,Borland C++提供了OWL。也有很多第三方提供的庫。我們在開發應用程序的時候,也發覺我們也許需要某些特定的庫來完成特定的功能。那么,如何編寫自己的庫呢? 利用Java的面向對象特性,如封裝,繼續,和一些設計模式,我們可以用標準的方法來建立自己的庫。需要明白的一點:在你需要完成某個功能的時候,不要用專有的、特定的方法去編寫代碼,而要全盤考慮,用通用的方法來完成,這樣,在積累了一定數量的庫以后,你就能重用這些庫來完成新的功能,而不用每回都重頭編寫代碼。這也是面向對象語言提供給我們的好處。也可以用J2EE的 規范為例子,J2EE提供了一個CBT(Component Based Transaction),所有的組件都尊崇J2EE規范,在CBT中運行,這樣,編寫開發并且重用標準的通用的組件庫,可以縮短開發周期節約成本,并且可 以在任何符合J2EE規范的應用程序服務器(application SERVER)中運行,并且可以繼續,擴展已 有的組件庫完成新的任務或者適應新的變化。 在本文中,我將先討論如何建立自己的庫,需要根據哪些標準,然后給出一個簡單的例子。在第二 部分中,我將通過一個功能比較完善的庫來做進一步的討論。 什么是庫 什么是庫? 庫是一個可以重用的組件,它采用通用的設計,完成通用的任務,可以節約開發者的時間,縮短開發周期節約開發成本。一個設計完善的庫,并不只是為了完成某一個特定的任務,而是可以完成各種不同的任務。設計一個庫是困難的。寫一個算法并不難,但是設計庫的時候需要一種比較好的結構,它能夠被用在各種需要的環境下,完成各種不同的任務,但是還不能影響使用它的 程序代碼結構。 為什么要重用代碼? 重頭開發一個新的軟件,工作量是非常巨大的,不論你用什么工具什么語言。而代碼重用能夠節約大部分時間,而把時間花在新的功能的開發上。從一定的意義上來說,寫一個新的軟件是利用了現有的代碼,重新拼裝以實現新的功能。從另外一個角度上來講,即使你沒有打算把你寫的代碼變成一個通用的庫并分發給其他人使用,從設計的角度來講,采用一種全盤的通用的設計方法也能讓你對所要完成的任務有更好的理解,并且優化你的設計過程,從而優化你的代碼結構。 采用開發庫并且讓別人來使用它的方式,能夠幫助你在使用它的時候發現它的設計上的缺陷或者代碼中的錯誤,并幫助你改正它。比方說,你寫了一個庫讓別人來使用,你不得不考慮通用的設計,因為你并不能預見別人將在什么環境下使用和使用的目的。在其他人使用你的庫的過程中,可能會碰到一些問題,有的可能是你的文檔寫得不夠清楚明白,有的也可能是你程序上的錯誤,也有可能是使用者覺得在結構上使用起來不方便或者不正確。那么你可以繼續作一些修改工作,在保持結構 和接口不變化的情況下,做一些調整。 如何設計庫 在設計庫的時候,你需要以一個使用者的眼光來看問題,考慮如何設計和實現它。你需要明白: 1、需要解決的問題是什么?需要達到一個什么目的? 2、使用者關心的問題是什么?使用者需要得到一個什么結果? 3、使用者不需要關心的問題是什么?什么細節是可以對使用者隱藏的? 下面,我們用一個簡單的例子來說明如何設計和實現一個有用處的庫。 設計一個網絡服務程序,我們需要考慮幾點: 1、監聽一個端口 2、接受連接 3、讀取或者寫入連接的流 4、處理輸入的數據,并且返回一個結果 對于我們將要實現的庫來說,需要完成的是前三點,而最后一點我們留給使用者去實現,這也是使用者需要完成和關心的地方。 庫的主要類叫做Server,測試的類叫做EchoServer. EchoServer實現了一個簡單的服務,從客戶端讀取數據,并且返回同樣的數據。 設計原則一:封裝 一個好的庫必須是一個緊湊的關系緊密的整體,而不是一個分散的關系松散的對象的集合。 package是Java提供的一種類庫的封裝機制。一個package是一個Java類文件的集合,存放在同一個目錄中。package有專有的名字空間。 專有的名字空間的一個好處是,你不用擔心名稱的沖突。因為,假如你的類的名稱和別人的類的名稱沖突,但是他們不在同一個package中,利用這一點可以避免名字的沖突。 每一個package都有一個字符串來代表,比如java.lang,或者javax.swing.plaf.basic.實際上每一個類的全名都是由package的名字加上類的名字來代表的,這樣就避免了名字的沖突,比如,java.lang.Object或者javax.swing.plaf.basic.BasicMenuBarUI。 注重,有一個非凡的package叫做default package。假如你不聲明你的類屬于任何一個package,那么它就被假定屬于default package。 每一個package的名字都對應一個目錄。比如,java.lang.Object存放在java/lang/Object.java中,每一 個.對應一個/. default package存放的目錄是當前目錄。 聲明一個package. // Server.java package mylib; public class Server implements Runnable { // ... 假如有import語句,必須放在package語句的后面。 當然你也可以引入別的package. 例如: import mylib.Server; // ... Server server = new Server( portNum ); Java答應你決定package中的哪些類對外部是可見的。public類可以被包外的代碼使用,而PRivate類 則不行。 比如,讓Server類能被外部的代碼使用: // Server.java package mylib; import java.io.*; import java.net.*; public class Server implements Runnable { 假如你不想讓類被外部的代碼使用,可以用缺省的屬性,去掉public. 例如: // Reporter.java package mylib; class Reporter implements Runnable { 設計原則二:繼續 在我們的例子中,Server是主要的類。假如你看這個類的代碼,就能看到,它本身其實什么也不做。主循環用來監聽連接。當連接建立以后,它把處理連接的任務交給一個叫做handleConnection() 的函數。 // subclass must supply an implementation abstract public void handleConnection( Socket s ); 因為沒有實現這一函數,所以這個類被聲明為abstract,使用者必須實現這個函數。 // This is called by the Server class when a connection // comes in. "in" and "out" come from the incoming socket // connection public void handleConnection( Socket socket ) { try { InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream(); // just copy the input to the output while (true) out.write( in.read() ); } catch( IOException ie ) { System.out.println( ie ); } } 可以說,這一繼續的過程叫做定制。因為在Server類中,并沒有定義該函數的動作,而是把這個定 義的過程留給使用者,讓他們來完成所需要的特定的功能。 另外一個定制函數:cleanUp(). 在設計類的時候,往往你能考慮到使用者需要的功能,例如上面的handleConnection().但是,也需要 考慮另外一種定制,例如在這里,在Server退出后臺運行方式的時候,調用了這個cleanUp()函數, 在Server類中的實現為空,什么都不做,這把機會留給使用者,使用者可以用這個函數來做一些清 除工作,這種函數也可以稱之為"鉤子"。 設計原則三:調試 沒有人能夠做到寫出一個絕對完美的程序,沒有任何的錯誤。所以,調試是不可缺少的。有時候, 使用者可能會碰到一個問題,從而需要知道在庫的代碼中發生了什么問題。這個錯誤可能是庫代碼 的問題,也可能是使用者的代碼在庫代碼中引起的問題。 假如你提供了庫的源代碼,使用者可以用debugger來調試錯誤。但是,你不能完全依靠于調試器。 在庫代碼中加入打印調試信息的語句,是一個好習慣。它可以幫助使用者明白,什么地方發生了錯 誤。 下面的例子說明了這一技術。使用者的代碼使用Server.setDebugStream(),指定一個PrintStream對 象。然后,調試信息就被輸出到這個流中。 // set this to a print stream if you want debug info // sent to it; otherwise, leave it null static private PrintStream debugStream; // call this to send the debugging output somewhere static public void setDebugStream( PrintStream ps ) { debugStream = ps; }
當使用者使用了調試的流的時候,你的庫代碼可以打印錯誤:
// send debug info to the print stream, if there is one static public void debug( String s ) { if (debugStream != null) debugStream.println( s ); }
下面,來完整的看一看這個具體的例子:
EchoServer // $Id$ import java.io.*; import java.net.*; import mylib.*; public class EchoServer extends Server { public EchoServer( int port ) { // The superclass knows what to do with the port number, we // don't have to care about it super( port ); } // This is call