[.NET] [深入淺出物件導向分析與設計] 心得1 : 偉大軟體由此開始


[深入淺出物件導向分析與設計] 心得1 : 偉大軟體由此開始

閱讀 深入淺出物件導向分析與設計(Head First Object-Oriented Analysis and Design) 後,發現要撰寫出偉大軟體(great software)相當不容易,反思自己明明都知道書中提到的 OOA&D (Object-oriented analysis and design) ,但卻無法每次將此項能力好好使用,可能的原因是時程太短或對此項技能熟悉或是其他原因,造成常常寫出只要能動程式碼,不管後續程式維護性還是擴充性,如果能將OOA&D能力變成被動能力,只要寫程式自動觸發該有多好,此書將OOA&D以簡單與詼諧的方式進行介紹,只要能將書中提供三個步驟反覆練習,相信很快就能將此項能力從主動變成被動能力。

什麼事偉大軟體?

  1. 以客戶導向設計師:

    將客戶的想法轉換成軟體,它能夠如客戶預期產生結果。客戶是神,客戶說的都是對的

  2. 以物件導向設計師:

    沒有重複的程式碼,每個物件都控制得當,擴展也相當容易,因為設計出既穩固又有彈性(參考)。

  3. 設計大師:

    讓物件保持鬆散耦合(losely coupled),讓程式碼「禁止修改而關閉,允許擴展而開放」大師講的話,一定黑人問號冒出(參考)。有助於程式碼重用性,因此不必重作每件事情,可以一直被使用。

上述其實都是只對一部分,要注意偉大軟體都不只是件事情,需要將下述都達成

  1. 偉大軟體必須讓客戶滿意,做客戶要它做的事
  2. 偉大軟體是設計良好(well-designed)編成良好(well-coded)、並且易維護可重用易擴展

如何達成呢? 書中有提到可以反覆確認偉大軟體的三個步驟

偉大軟體的三步驟

  1. 確認軟體做客戶要它做的事:

    重點放在客戶上,確保軟體做它應該做的事。這裡就需要從客戶哪裡 收集需求(requirement)分析(analysis)

  2. 應用基本OO原則,增加軟體的彈性:

    軟體開始運行後,能找出任何疏忽造成重複的程式碼,並確認使用良好的OO編成技術。

  3. 努力達成可維護、可重利用的設計:

    有了良好的物件導向的程式後,就要運用設計模式OO原則,確保程式在日後都運行。

情境

客戶:Rick 的亂彈彈吉他店。

需求:希望可以原本丟棄紙本作業,建立吉他庫存管理應用程式,可以透過此程式進行搜尋,並幫忙客戶配對到心目中的夢幻吉他。

下列範例會適當簡化,並以書中提到重點進行介紹。完整範例(參考)

版本一

依據客戶的需求設計出 GuitarInventory 類別來管理吉他,這時候已經躍躍欲試想迎接第一個顧客(Erin)了,當顧客操作時卻收到顧客反應找不到合適的吉他,所設計偉大軟體到底發生了什麼事情呢?這時候會發現顧客再輸入製造商時輸入了小寫(fender),因為字串比對時沒有忽略大小寫,造成無法找到合適的吉他。

看起來我們離偉大軟體很遙遠,這時候想想上述偉大軟體的三步驟來逐一調整這虛假偉大軟體。 (版本二)

類別圖

Guitar.cs

public class Guitar
{
    private readonly string serialNumber, builder, model;

    private decimal price;

    public Guitar(string serialNumber, decimal price,
            string builder, string model)
    {
        this.serialNumber = serialNumber;
        this.price = price;
        this.builder = builder;
        this.model = model;
    }

    public string GetSerialNumber()
    {
        return serialNumber;
    }

    public decimal GetPrice()
    {
        return price;
    }

    public void SetPrice(decimal newPrice)
    {
        this.price = newPrice;
    }

    public string GetBuilder()
    {
        return builder;
    }

    public string GetModel()
    {
        return model;
    }
}

Inventory.cs

public class Inventory
{
    private readonly List<Guitar> guitars;

    public Inventory()
    {
        guitars = new List<Guitar>();
    }


    public void AddGuitar(string serialNumber, decimal price,
                  string builder, string model)
    {
        Guitar guitar = new Guitar(serialNumber, price, builder,
                                   model);
        guitars.Add(guitar);
    }

    public Guitar GetGuitar(string serialNumber)
    {
        foreach (var guitar in guitars)
        {
            if (guitar.GetSerialNumber().Equals(serialNumber))
            {
                return guitar;
            }
        }

        return null;
    }

    public Guitar Search(Guitar searchGuitar)
    {
        foreach (var guitar in guitars)
        {
            // 沒有忽略大小寫的問題
            string builder = searchGuitar.GetBuilder();
            if (!string.IsNullOrEmpty(builder) &&
                !builder.Equals(guitar.GetBuilder()))
                continue;

            string model = searchGuitar.GetModel();
            if (!string.IsNullOrEmpty(model) &&
                !model.Equals(guitar.GetModel()))
                continue;

            return guitar;
        }

        return null;
    }
}

Program.cs

Inventory inventory = new Inventory();
InitializeInventory(inventory);

// 買吉他的顧客輸入製造商使用小寫,造成搜尋不到合適的吉他
Guitar whatErinLikes = new Guitar("", 0, "fender", "Stratocastor");

Guitar guitar = inventory.Search(whatErinLikes);
if (guitar != null)
{
    Console.WriteLine("Erin, you might like this " +
      guitar.GetBuilder() + " " + guitar.GetModel() + " " +
      guitar.GetPrice() + "!");
}
else
{
    Console.WriteLine("Sorry, Erin, we have nothing for you.");
}

static void InitializeInventory(Inventory inventory)
{
    inventory.AddGuitar("11277", 3999.95m, "Collings", "CJ", "acoustic");

    inventory.AddGuitar("V95693", 1499.95m, "Fender", "Stratocastor";

    inventory.AddGuitar("V9512", 1549.95m, "Fender", "Stratocastor");
}

結果

Sorry, Erin, we have nothing for you.

版本二

偉大軟體的第一個步驟:確認軟體做客戶要它做的事,很明顯沒有符合哪我該如何調整軟體,需解決你些問題呢?

  1. 解決找不到合適吉他
    => string 忽略大小寫,但還有沒有更好的方式呢?
    => 如果知道列舉類型,就可以避免客戶或是自己輸入上錯誤,是一個還不錯解決方法。

  2. 客戶其實希望適合的吉他,都可以展示給顧客知道
    => 就避免增加 List 來記錄所有符合的吉他。

修改完成後客戶非常滿意,因為來找吉他的顧客都可以順利找到合適的吉他,這樣是不是就已經是偉大軟體呢? 其實我們只完成第一步驟的修改,我們還有二跟三步驟需要調整。(版本三)

Guitar.cs

public class Guitar
{
    private readonly string serialNumber, model;

    private readonly Builder builder; // 使用列舉類型

    private decimal price;

    public Guitar(string serialNumber, decimal price,
            Builder builder, string model)
    {
        this.serialNumber = serialNumber;
        this.price = price;
        this.builder = builder;
        this.model = model;
    }
    
    // ...
}

Inventory.cs

public class Inventory
{
    // ...
    public Guitar Search(Guitar searchGuitar)
    {
        // 調整成所有符合條件的吉他都顯示
        List<Guitar> matchingGuitars = new List<Guitar>();
        
        foreach (var guitar in guitars)
        {
            // Ignore serial number since that's unique
            // Ignore price since that's unique
            if (searchGuitar.GetBuilder() != guitar.GetBuilder())
                continue;

            string model = searchGuitar.GetModel();
            if (!string.IsNullOrEmpty(model) &&
                !model.Equals(guitar.GetModel()))
                continue;

            matchingGuitars.Add(guitar);
        }

        return matchingGuitars;
    }
}

Program.cs

// ...
List<Guitar> matchingGuitars = inventory.Search(whatErinLikes);

if (matchingGuitars.Any())
{
    Console.WriteLine("Erin, you might like these guitars:");
    foreach (var guitar in matchingGuitars)
    {
        Console.WriteLine("  We have a " +
        guitar.GetBuilder() + " " + guitar.GetModel() + " guitar:" +
                          + "\n You can have it for only $" +
        guitar.GetPrice() + "!\n  ----");
    }
}
else
{
    Console.WriteLine("Sorry, Erin, we have nothing for you.");
}

結果

Erin, you might like these guitars:
  We have a Fender Stratocastor guitar:
  You can have it for only $1499.95!
  ----
  We have a Fender Stratocastor guitar:
  You can have it for only $1549.95!
  ----

版本三

我們解決了”客戶要它做的事”,哪接下來就是”增加軟體的彈性”,我們可以發現顧客在搜尋時候其實不需要傳價格跟序號,哪我們可不可把吉他的規格特別拉出,因為吉他類別不需要這些特性,就如下面一樣把吉他的規格變成GuitarSpec類別抽離出來,調整完後離偉大軟體我們又更進一步了! (版本四)

// 不需要傳哪麼多參數
Guitar whatErinLikes = new Guitar("", 0, "fender", "Stratocastor");

GuitarSpec.cs

public class GuitarSpec
{
    private readonly Builder builder;

    private readonly string model;

    public GuitarSpec(Builder builder, string model)
    {
        this.builder = builder;
        this.model = model;
    }

    public Builder GetBuilder()
    {
        return builder;
    }

    public string GetModel()
    {
        return model;
    }
}

版本四

這時候客戶突然說可以幫我加個吉他弦數,這時候心裡想不就多一個參數就結束,分分鐘就可以調整完,沒有想到一修改下去才發現不得了,除了要調整GuitarSpec.cs 外,連Guitar.csInventory.cs 也要跟著調整,就表示GuitarSpec.cs 跟其他兩個類別相依性太重,如果把Guitar.cs 改用 GuitarSpec 類別當參數,之後把 Inventory.cs 找吉他的部分抽離到 GuitarSpec.cs ,這樣未來如果參數增加就只需要調整 GuitarSpec.cs就可以。

Guitar.cs

public class Guitar
{
    private readonly string serialNumber;

    private decimal price;
    
    private readonly GuitarSpec spec; // 使用 GuitarSpec

    

    public Guitar(string serialNumber, decimal price,
            GuitarSpec spec)
    {
        this.serialNumber = serialNumber;
        this.price = price;
        this.spec = spec;
    }
    
    // ...
}

GuitarSpec.cs

public class GuitarSpec
{
    private readonly Builder builder;

    private readonly string model;
    
    private readonly int numStrings;

    public GuitarSpec(Builder builder, string model, int numString)
    {
        this.builder = builder;
        this.model = model;
        this.numStrings = numStrings;
    }

    public Builder GetBuilder()
    {
        return builder;
    }

    public string GetModel()
    {
        return model;
    }
    
    public int GetNumStrings()
    {
        return numStrings;
    }
    
    public bool matches(GuitarSpec otherSpec)
    {
        if (builder != otherSpec.builder)
            return false;

        if (!string.IsNullOrEmpty(model) &&
            !model.ToLower().Equals(otherSpec.model.ToLower()))
            return false;

        if (numStrings != otherSpec.numStrings)
            return false;

        return true;
    }
}

結論

依據偉大軟體的三步驟,一步步慢慢調整最後寫出來的軟體,既符合客戶需求又沒有重複代碼,未來在維護跟擴展上也相當簡單,這才是我們所追求偉大軟體,所以在寫任何軟體都要時時刻刻提醒自己此三步驟。

參考

Object-oriented analysis and design

Head First Object-Oriented Analysis and Design

開閉原則

類別圖


作者: PuTaoNi
版權聲明: 本站所有文章除特別聲明外,均採用 CC BY 4.0 許可協議。轉載請註明來源 PuTaoNi !
 上一篇
[.NET] 如何透過 DisplayName 或 Description 自訂 Enum 字串 [.NET] 如何透過 DisplayName 或 Description 自訂 Enum 字串
enum 由一組整數類型命名的常數定義,其為實值類型。如何透過 DisplayName 或 Description 自訂 Enum 字串。
2022-03-30
下一篇 
[.NET] 什麼是泛型 (Generics) [.NET] 什麼是泛型 (Generics)
泛型是在 C# 2.0 才被加入的新功能,主要是將類別參數化`T`,讓設計類別(Class)、結構(Struct)、介面(Interface)與方法(Method)時可以使用一個或多個參數,這樣就可以增加重用性(Reusability)、類型安全(Type safety)與效率(Efficiency),下面的例子就是簡單的泛型類別。
2022-03-22
  目錄