2009/9/22

設計一個從物件導向為出發點的網站

在這裡我先假設你已經知道所謂物件導向程式設計的原理, 所以我不會浪費篇幅重複的解釋 OOP 是什麼。但是如果你來到這裡的目的是希望看到最基礎的 OOP 原理, 那麼我建議你去書局買一本適合的書回去慢慢研究, 或者上網從 WIKI 開始看起。

我在這裡想要介紹給 ASP.NET 新手的, 主要在於 OOP 的實踐方式, 以及應用時機。

應該如何下手?

我相信大多數程式設計師都或多或少知道 OOP 的原理, 或許有些還曾經花下心血研究它。但是到了現實生活中, 還是覺得不知從何下手。我想這也難免; 畢竟你就算完全不懂得 OOP, 你同樣可以把程式寫好、把工作做完。或許你看到這裡, 心裡剛好也正想說「對啊, 這就是我」。

此外, 在坊間的一般 OOP 書籍裡, 以及網路上許多對 OOP 的入門介紹文章中, 多半都拿什麼狗、貓、人、汽車等等讓人摸不著邊際的 Case 當作範例。然而, 有程式設計師會寫狗、貓、人或汽車的程式嗎? 對照到現實中的狀況, 書中的範例恐怕很難直接拿出來用。

因此, 我在這裡要以直接切入專案的方式, 使用對程式設計師而言更為「白話」的方式來介紹 OOP 在 ASP.NET 中的實踐方法。

我相信大家對於封裝、繼承這些 OOP 的基本觀念都已經可以朗朗上口了。不過在 ASP.NET 環境中, 並不是你開始下手去寫程式就會自動用上了 OOP 的精神。你必須在真正下手之前花點心思去規畫一下網站的做法, 才能寫出具有 OOP 精神的網站。

或許你對我上述說法存有疑義: 「.NET 在架構上本來就遵循 OOP 的精神, 我怎麼可能在這種平台中寫出非 OOP 的程式呢?」事實上, .NET 的底層架構確實完全是 OOP 導向的, 但是你還是可能在這種架構下寫出完全沒有 OOP 精神的東西出來, 就好像你可以在鋼筋水泥屋頂上再蓋出一個完全是木頭釘的小茅蘆一樣。

從自訂類別著手

先拿 OOP 的基本精神「繼承」為例好了。繼承的主要目的在於資訊的封裝以及程式碼的共用, 而它的基本單位是類別。如果你從來沒有在網站的 App_Code 子目錄下寫過程式, 那麼我認為你可能很難稱得上寫過真正的符合 OOP 精神的網站。

不過, 在你開始撰寫自訂類別之前, 你應該自問一下, 為什麼要撰寫類別? 以及要寫些什麼?

當然, 如果你要寫的網站很簡單, 差不多只有兩三頁而已, 那麼你真的可以不需要再往後面看下去了。但如果不是, 那麼你只要在不同網頁上、甚至同網頁的不同地方會用到相同的功能, 那麼你就可以考慮撰寫自訂的類別以達到共用程式碼的目的。

我提醒你盡量不要把所有的東西都寫在 .aspx.cs 或 .aspx.vb 裡面。如果你堅持的話, 你當然還是可以一直這麼做, 我並不是說這樣會讓你的程式出什麼大問題。但是經驗告訴我們, 當你遇到以下幾種情況的時候, 麻煩就會出現:

  • 當你在不同網頁中有需要共用程式碼的時候
  • 當你有需要自訂型別的時候
  • 當你覺得程式太長而需要分段以方便維護的時候

在以上情況中, 撰寫類別程式會是一個極為自然而又方便的解決方法。請往下看看一些範例, 你自然會明白。

此外, 在比較近代的觀念中, 我們會比較建議程式設計師把商業邏輯 (Business Logic) 包在網頁以外的地方; 這是一種稱為多層式架構 (Multi-tier) 的觀念。換句話說, 我們最好是在網頁程式中單純的處理僅跟網頁相關的事情, 例如檢驗某個僅能輸入數字的欄位是否真的輸入了數字; 但是至於輸入的數字是否落在商業邏輯允許的範圍內 (例如年齡限制在 18 到 35 歲) 則應該交給對應的類別來判斷。

在自訂類別中應該寫些什麼?

還沒有把 OOP 當做呼吸一樣自然的新手可能會有這樣的問題, 那就是不知道應該寫些什麼類別。我認為自訂的類別至少可以用來撰寫以下幾種東西:

  • 程式庫 (Class Library)
  • 自訂型別 (Custom Type)
  • DataSet

其中 DataSet 在 Visual Studio 有其輔助設定工具可用, 我個人認為它應該算是 Data Layer 的一環, 所以在這裡就不再繼續討論它。

至於程式庫 (現在的 Visual Studio 在 Add New Item 視窗裡已經不再有這個 Template 了, 都僅稱為 Class), 我的意思是僅僅用來儲存共用程式碼的檔案。例如在我的專案中有如下的類別程式:

using System.Web;
...
using Johnny.Util;
namespace Johnny.Util
{
    public class parse
    {
        // Properties
        public string tobeParsed {get; set;}
        public string parsed {get; set;}
        ...
        // Methods
        public parse() { ... } // Constructor
        public parse(string input) { ... } // Constructor
        public static bool isInputValid(string input) { ... }
        ...
    }
}

如果在這種類別中有些方法很明顯是跟繼不繼承無關的 (就是說它是單純的功能函式), 那麼你可以考慮把寫成 static (在 VB 中稱為 Shared) 方法。

另一種類別則是自訂的型別, 例如以下的範例程式:

using System;
...
using Johnny.Util;
namespace Johnny.Management
{
    public interface iMyDataType
    {
        DataTable queryData();
        bool insert();
        bool update();
        bool delete();
        string ToString();
    }
    public enum eGendre : int
    {
        male = 0,
        female = 1
    }
    public class personel : iMyDataType, IComparable, ICompare<object>, IEqualityComparer<object>
    {
        // Properties
        ...
        // Methods
        public personel() { ... } // Constructor
        ...
    }

在以上程式中, 我定義了一個稱為 personel 的自訂型別, 此外也定義了一個叫做 iMyDataType 的 Interface 和一個叫做 eGendre 的 Enum。這個 personel 自訂型別實作了 iMyDataType, IComparable, ICompare 和 IEqualityComparer 等等幾個介面, 意思就是它還必須定義像 queryData, insert, update, delete, ToString, CompareTo, Compare, Equals, GetHashCode 等等幾個方法。

請別誤會, 自訂型別不一定非得實作什麼介面不可; 你也可以完全不實作任何介面。要實作什麼介面, 端視你自己的需要而決定; 那不是強制的。

我實作 iMyDataType 介面的目的在於強制要求型別必須提供那些方法, 而實作其它幾個介面的目的在於提供陣列或 Collection 物件的 Sort 的可行性等等。這個部份稍嫌複雜, 容我不在這篇入門文章裡做太多的介紹。(如果你真的對這個部份有興趣, 你可以另外參考「讓自訂類別的陣列物件具有排序與搜尋的能力」這篇文章, 在這裡就不另外說明了。)

不過, 請特別注意一點, 把類別區分為程式庫和自訂型別, 這是我個人的講法, 也是我個人的習慣; 在 .NET 中其實並沒有這樣的區分法。事實上, 像 .NET 內建的 Array 型別就是一個典型的例子: Array 本身是個型別, 但是在 Array 之下還有很多的 Static 方法跟非 Static 方法可以使用。不過話說回來, Array 類別下提供的各個方法都只跟 Array 型別和其實體化物件有關係, 所以我們在撰寫自訂類別時最好還是注意一下是否維持合理的邏輯關係。

此外, 你最好記得幾件事: 首先, 在 App_Code 下面, 你可以無限制的建立子目錄, 所以你可以妥善的使用子目錄以將不同的類別檔案分類; 其次, 沒有人規定你必須把所有的類別都寫在同一個檔案裡, 所以你大可以把不同的類別寫在不同的檔案裡 (檔案名稱可以隨便取。不過, 當然, 你最好不要真的隨便取, 以免未來連自己都搞混了); 如果有一個類別程式太大, 你甚至可以把它拆在兩個以上的檔案裡。還有, 你最好像我在以上範例一樣給予各個類別適當的 Namespace, 以後當你要引用這些類別時可以為你帶來很大的方便性。

所以, 其實在我的實際專案中, iMyDataType 這個介面和 eGendre 並不是和 personel 類別擺在同一個檔案裡面的, 而是放另一個包容更大架構的命名空間的其它類別之下。事實上只要命名空間取得有條理, 你把這些元件放在哪一個檔案、哪一個子目錄下面 (只要不移到 App_Code 之外即可), 都不會造成任何影響。例如我在我的 Johnny.Utils 命名空間之下可能建立了二十個類別, 而它們被存放在八個資料夾之下的二十五個檔案裡面, 但一經 Compile, 它們都會被放入同一個 .dll 檔案裡面, 而在撰寫程式時, 我還是只需要鍵入 Johnny.Utils, 然後 Visual Studio 的 Intellisense 功能會自動帶出這二十個類別出來。

就我個人而言, 我事實上是把各個類別程式盡可能的分散, 藉由各資料夾名稱進行歸納 (根據我對命名空間的區分規則)。所以我個人在 App_Code 之下放的不只程式庫和自訂型別兩種, 我甚至有些檔案只放 Enum 或公用欄位。對你而言, 你也可以視需要而做更靈活的規劃。

繼承及擴充

我已經提醒過你, 最好在撰寫網站之初就先規畫一下整體的架構。舉個例子, 假設你在開發一個人事管理的網站, 經過分析, 你發現公司的員工可以分做管理階層 (Manager)、一般職員 (Employee) 以及雇員 (Contractor) 三種 (我相信如果你有先使用 UML 做需求分析的話, 那麼你至少在畫 Use Case Diagram 時就可以分析出這幾'種角色)。這三種員工身分的大部份屬性和方法都是相同的, 只有少部份有差異。在這種情況下, 你應該可以先建立一個 personel 自訂型別, 然後再各別建立 manager, employee 與 contractor 三個類別以繼承 personel 類別, 如下範例:

public class personel : iMyDataType, IComparable, ICompare<object>, IEqualityComparer<object>
{
    // Properties
    public int employeeId {get; set;} // 員工編號
    public string name {get; set;} // 員工姓名
    public double salary {get; set;} // 薪資
    ...
    // Methods
    public double pay() { ... } // 發薪
    ...
}

public class manager : personel
{
    // Properties
    public stock options {get; set;} // 股票選擇權
    // Methods
    public stock vast(stock newStock) { ... } // 配發股票選擇權
    ....
}

public class manager : employee
{
    // Properties
    public double bonus {get; set;} // 主管加給
    // Methods
    public double getBonusThisMonth() { ... } // 取得常月的主管加給
    ....
}

public class contractor : personel
{
    // Properties
    public int workHours {get; set;} // 工時
    public double hourFee {get; set;} // 時薪
    // Methods
    public override double pay() { ... } // 發薪 (覆寫)
    ....
}

在以上的繼承結構中, employee 與 contractor 繼承了 personel 類別, 而 manager 又繼承了 employee 類別。在 manager, employee 和 contractor 這三個類別中, 各自再加入了新的屬性或方法, 而 contractor 則覆寫了 personel 的 pay 方法。由於 manager, employee 和 contractor 這三個衍生的類別乃衍生自 personel 類別, 所以它們也都自動實作了 iMyDataType, IComparable, ICompare 和 IEqualityComparer 這幾個介面。

如果你使用 Visual Studio 做為開發工具的話, 你可使用它所內建的 Class Diagram 做為輔助設計類別的工具。這個工具可以協助你製作出類別架構, 如果你是從大架構來設計網站的話, 這個工具蠻好用的。

當你設計好這幾個類別之後, 我們可以在網頁程式中透過以下的方式來使用它們:

personel p;
double b = (double) 0;
switch (DropDownList1.SelectedIndex)
{
    case 0:
        p = new employee();
        break;
    case 1:
        p = new manager();
        p.bonus = p.getBonusThisMonth();
        b = p.bonus;
        break;
    case 2:
        p = new contractor();
        break;
}
p.employeeId = txtPID.Text;
p.salary = double.Parse(txtSalary.text);
p.salary += b;

展現物件導向的魔力

上述的範例為求簡化, 我把商業邏輯和使用者輸入的處理部份放在一起; 你的程式最好能進一步把商業邏輯抽離出網頁程式之外。例如, 你可以把程式中 switch ... 這一段抽出來單獨寫成一個類別, 這樣會使得這整個範例程式剛好變成了一個典型的 Design Pattern 裡面著名的 "Factory Method"。如果你對這方面感到興趣, 你不妨另外找時間深入研究。我想我應該不會在 ASP.NET 2Share 裡針對在這個主題上花太多篇幅, 因為 Design Pattern 在坊間早有成山一般的書籍可供參考, 在網路上則有更多。(順道補充一下, 在 .NET Framework 裡面, 它內建的 Provider Model 就有很多此種 Design Patern 的影子)

如果你早已熟悉 OOP 的基本原理, 你或許會認為這裡所舉的例子平淡無奇。但是, 請不要小看這種設計方法在實際案例上所能帶給我們的好處。事實上, 在未來, 假設針對所有 personel 的某個方法或屬性 (例如 calcTaxRate 或  annualLeaveDays) 有需要變更, 那麼我們只需要修改 personel 類別的程式即可; 如果僅有 contractor 的某個方法或屬性有變更, 我們只需要修改 contractor 類別即可; 而如果原本 manager 的 vast 方法是繼承 employee 的, 但某一天需求突然又被改了, 那麼我們還是只需要在 manager 類別覆寫這個方法即可。用簡單的話說, 我們可以把修改程式的幅度減少到最低的程度, 也同時把程式間交互影響的程度降到最低, 這是不是一舉數得呢?

在現實世界中, 就算專案規劃得再好, 你也不能保證客戶 (這裡所指的客戶恐怕也包括你自己) 不會更改需求。當然, 雖然 OOP 也不是什麼能治百病的萬靈丹, 但它已被證實是能夠讓程式設計師生活得更愉快的方式之一。 所以, 你既然決定在 .NET 架構下撰寫程式, 那麼就好好運用它的優點吧!

再以這個範例中的情境為例吧! 假設, 就算你的系統分析師已經確信他做過有史以來最完善的需求訪談了, 但是專案經理仍然在快要接近驗收關卡的時候告訴你, 客戶告訴他, 因為客戶的老闆的小姨子的未婚夫的爸爸突然被安插進來擔任公司的資深顧問, 所以針對這個新增的職務, 其薪資結構和股票選擇權的計算公式要另外設計。不幸的, 總經理急著驗收領錢, 所以不同意結案時間有任何異動。現在, 請問應該如何處置?

對付這種情況, 以物件導向方式設計的系統和以非物件導向方式設計的系統可能有很迴異的處理方法。在我們的例子中, 只要加入一個繼承 manager 類別的 seniorManager 類別, 把差異部份覆寫一下, 然後在網頁程式稍為改一下, 再重新測試一下系統, 我相信還是有很大的機會能夠如期驗收的。

Interface 也能這麼玩

在以上範例中, 假設 A1, A2 與 A3 三個類別都是繼承了 A 類別, 那麼你在其它程式中先宣告

A a;

然後你再視情況需要讓 a 實體化為 A1, A2 或 A3 型別:

a = new A1(); // 或 
a = new A2(); // 或
a = new A3();

如此一來, 你可以執行 a 的大部份方法而無需擔心這個 instance 到底是 A1, A2 還是 A3:

a.DoThings(); // a 如果是 A1 的 instance, 就會執行 A1.DoThings(); 如果是 A2, 就會執行 A2.DoThings(); 依此類推

事實上, 介面 (Interface) 也可以這樣子玩。現在假設 B1, B2 與 B3 這三個都同時實作了 iMyInterface 這個介面, 而 iMyInterface 這個介面都規範有 Method1() 與 Method2() 這兩個方法, 那麼, 同樣的, 你也可以宣告一個 iMyInterface 物件:

iMyInterface b;

然後視情況讓 b 實體化為 B1, B2 或 B3:

b = new B1(); // 或 
b = new B2(); // 或
b = new B3();

同樣的, 之後你就可以任意的執行 b.Method1() 或 b.Method2() 而無需擔心它到底是 B1, B2 或 B3 型別; 因為它如果是 B1 型別, 它會自動去執行 B1.Method1() 或 B1.Method2(), 它如果是 B2 型別, 它會自動去執行 B2.Method1() 或 B2.Method2(), 依此類推。

如果你的網站或程式還沒有寫得很大, 你或許無法體會 Interface 到底對你有些什麼用。是的, 你現在只要了解 Interface 也能這麼用就好了; 你可以先去體驗一下建立自訂類別及衍生類別的優點, 未來再慢慢研究 Interface, 猶時也不晚。

善用組頁工具

我原本打算在這篇文章裡面介紹的是 OOP, 但是既然你已經耐心的看到這裡, 我想你一定可以忍受我再囉嗦一點你可能不認為是 OOP 的部份。ASP.NET 從最早的版本開始就已經提供了包括 Master Page, Theme, User Control, Custom Control 等等可以幫我們共享程式碼的東西。如果你還不會、或尚未用過這些功能的話, 我建議你盡早把它們加入你的網站 (Custom Control 可以例外, 因為它畢竟不是對所有人都適用)。一旦你能夠掌握這些功能, 你會立即發現它們能夠帶給你的方便性, 並且讓你的網站看來更專業。

不過不管是 Master Page, Theme, User Control 等等, 這些都可以算是 ASP.NET 的顯學, 隨便一本入門書籍都會有很大篇幅的介紹, 你不可能會不知道的 (當然, 或許你還沒有意會到它們的作用... 如果真的如此, 好吧, 我要再度推薦一下它們), 因此我覺得就不用在這裡佔篇幅去介紹它們了。

Technorati 的標籤:,,,

沒有留言:

張貼留言