【深入淺出物件導向分析與設計】第一章、偉大軟體由此開始

紀錄 O'REILLY 深入淺出物件導向分析與設計 (Head First Object-Oriented Analysis & Design) 的讀後心得,並將範例轉為 C#.Net Code。

情境

客戶:Rick 的亂彈吉他店

需求:建立吉他庫存管理應用程式,並提供搜尋功能,為客戶配對心目中的理想吉他。

廠商:「低階編程」軟體公司

顧客:Erin (來到 Rick 的店尋找心目中理想的吉他)

版次:v0.1

【類別圖】

【程式碼】

Guitar.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Guitar
    {
        private string serialNumber, builder, model, type, backWood, topWood;
        private double price;

        public Guitar(string serialNumber, double price, string builder, string model, string type, string backWood, string topWood)
        {
            this.serialNumber = serialNumber;
            this.price = price;
            this.builder = builder;
            this.model = model;
            this.type = type;
            this.backWood = backWood;
            this.topWood = topWood;
        }

        public string getSerialNumber()
        {
            return serialNumber;
        }

        public double getPrice()
        {
            return price;
        }

        public void setPrice(double newPrice)
        {
            price = newPrice;
        }

        public string getBuilder()
        {
            return builder;
        }

        public string getModel()
        {
            return model;
        }

        public string getType()
        {
            return type;
        }

        public string getBackWood()
        {
            return backWood;
        }

        public string getTopWood()
        {
            return topWood;
        }

    }
}

Inventory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Inventory
    {
        private List<Guitar> guitars;// = new List<Guitar>();

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

        public void addGuitar(string serialNumber, double price, string builder, string model, string type, string backWood, string topWood)
        {
            guitars.Add(new Guitar(serialNumber, price, builder, model, type, backWood, topWood));
        }

        public Guitar getGuitar(string serialNumber)
        {
            return guitars.First(guitar => guitar.getSerialNumber().Equals(serialNumber));
        }

        public Guitar search(Guitar searchGuitart)
        {
            //return guitars.First(guitar => guitar.Equals(searchGuitart));
            for (int i = 0; i < guitars.Count; ++i)
            {
                Guitar guitar = guitars[i];

                string builder = searchGuitart.getBuilder();
                if ((builder != null) && (!builder.Equals("")) && (!builder.Equals(guitar.getBuilder())))
                {
                    continue;
                }

                string model = searchGuitart.getModel();
                if ((model != null) && (!model.Equals("")) && (!model.Equals(guitar.getModel())))
                {
                    continue;
                }

                string type = searchGuitart.getType();
                if ((type != null) && (!type.Equals("")) && (!type.Equals(guitar.getType())))
                {
                    continue;
                }

                string backWood = searchGuitart.getBackWood();
                if ((backWood != null) && (!backWood.Equals("")) && (!backWood.Equals(guitar.getBackWood())))
                {
                    continue;
                }

                string topWood = searchGuitart.getTopWood();
                if ((topWood != null) && (!topWood.Equals("")) && (!topWood.Equals(guitar.getTopWood())))
                {
                    continue;
                }

                return guitar;

            }
            return null;

        }

    }
}

Tester.cs (測試程式表單)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ricksGuitars_start
{
    public partial class fmFindGuitarTester : Form
    {
        Inventory inventory = new Inventory();
        Guitar whatErinLike = new Guitar("", 0, "fender", "Stratocastor", "electric", "Alder", "Alder");

        public fmFindGuitarTester()
        {
            InitializeComponent();

            inventory.addGuitar("V95693", 1499.95, "Fender", "Stratocastor", "electric", "Alder", "Alder");
            inventory.addGuitar("V9512", 1549.95, "Fender", "Stratocastor", "electric", "Alder", "Alder");
        }

        private void fmFindGuitarTester_Load(object sender, EventArgs e)
        {
            Guitar guitar = inventory.search(whatErinLike);
            string sRet = "Sorry, we have nothing for you.";
            if (guitar != null)
            {
                sRet = "You might like this " + guitar.getBuilder() + " " + guitar.getModel() + " " +
                    guitar.getType() + " guitar : \n" + guitar.getBackWood() + " back and sides,\n" +
                    guitar.getTopWood() + " top.\nYou can have it for only $" + guitar.getPrice().ToString() + "!";
            }
            tbMsg.Text = sRet;
        }
    }
}

【測試結果】

Why ??   明明有將符合搜尋條件的 Guitar 加入 Inventory,為何會搜尋不到呢?

(明明在 FmFindGuitarTester.cs 的 fmFindGuitarTester() 中,有加入兩筆 Guitar 資料加入 Inventory 中)

(為何在 Inventory 中找不到 whatErinLike 這把 Guitar 呢?)

顯然 Inventory 的 Search() 有問題,但原因似乎不是很明顯。

看出來了嗎?庫存紀錄裡的兩筆資料,builder 皆為 "Fender",但是 Erin 輸入做搜尋的條件是 "fender",原因似乎有點愚蠢,解決方法很簡單,只要將 Search() 內的字串比對,採用不分大小寫的方式即可。

但是,真的就這樣解決了就好嗎?        問題背後的問題似乎沒這簡單。      代誌嘸像憨人想的那麼簡單!!

 

由於此問題的產生,引發了幾個觀點:

  1. 「看看那些 string !實在真恐怖...不能用常數或物件代替嗎?」
  2. 「哇...從老闆的筆記看來,他想讓客人有多重選擇。Search() 方法難道不該傳回一個包含所有符合條件的項目清單嗎?」
  3. 「這個設計真恐怖!Inventory 與 Guitar 類別互相依賴太深,我無法相信這是有經驗的軟體人員所以建立的架構。」

 

相信大家想寫出「偉大軟體」,但什麼是「偉大軟體」?  又該從何處下手呢?

  1. 對客戶友善的程式設計師說:「偉大軟體總是做客戶要它做的事。即使客戶突發奇想,以新方式使用軟體,它還是能交付客戶預期的結果。」
  2. 物件導向的程式設計師說:「偉大軟體是物件導向的程式碼。因此沒有一堆重複的程式碼,每個物件將自己的行為控制得當,擴展也相當容易,因為你的設計既穩固又有彈性。」
  3. 設計大師說:「偉大軟體使用經過千錘百練的設計模式與原則。物件保持鬆散耦合,程式碼「"禁止修改而關閉,允許擴展而開放」。讓程式碼更能重複利用,不必重作每一件事,可以一次又一次的運用程式零件來完成。」

 

「偉大軟體」的意義為何?

  1. 偉大軟體必須讓客戶滿意,做客戶要它做的事。贏得客戶芳心,讓客戶認為他是偉大的。
  2. 偉大軟體是設計良好、編程良好、易於維護、可重用與擴展。讓你的程式與你一樣聰明。

 

偉大軟體的三步驟

Step 1:確認你的軟體做客戶要它做的事。

把重點放在客戶上,確保應用程式做它應該做的事。這裡是收集需求分析工作所著力之處。

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

一旦軟體可正常運作後,找出重複的程式碼,運用良好的 OO 編成技術改善它。

Step 3:努力達成可維護、可重用的設計。

運用設計模式 OO 原則,讓它經得起時間的考驗。

 

版次:v0.2

開始朝向「偉大軟體」之路邁進吧~

第一站:讓軟體能正常運作

要達到這個目的,程式碼必須做下列修改:

  1. 丟棄 string 比較。增加 Builder、Type 與 Wood 列舉。
  2. 搜尋結果必須能傳回符合客戶需求的所有 Guitar 清單。

【類別圖】

【程式碼】

Guitar.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public enum Builder { FENDER, MARTIN, GIBSON, COLLINGS, OLSON, RYAN, PRS, UNSPECIFIED };
    public enum Type { ACOUSTIC, ELECTRIC, UNSPECIFIED };
    public enum Wood { INDIAN_ROSEWOOD, BRAZILIAN_ROSEWOOD, MAHOGANY, MAPLE, COCOBOLO, CEDAR, ADIRONDACK, ALDER, SITKA, UNSPECIFIED };

    public class Guitar
    {
        private string serialNumber, model;
        private Builder builder;
        private Type type;
        private Wood backWood, topWood;
        private double price;

        public Guitar(string serialNumber, double price, Builder builder, string model, Type type, Wood backWood, Wood topWood)
        {
            this.serialNumber = serialNumber;
            this.price = price;
            this.builder = builder;
            this.model = model;
            this.type = type;
            this.backWood = backWood;
            this.topWood = topWood;
        }

        public string getSerialNumber()
        {
            return serialNumber;
        }

        public double getPrice()
        {
            return price;
        }

        public void setPrice(double newPrice)
        {
            price = newPrice;
        }

        public Builder getBuilder()
        {
            return builder;
        }

        public string getModel()
        {
            return model;
        }

        public Type getType()
        {
            return type;
        }

        public Wood getBackWood()
        {
            return backWood;
        }

        public Wood getTopWood()
        {
            return topWood;
        }

    }
}

Inventory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Inventory
    {
        private List<Guitar> guitars;

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

        public void addGuitar(string serialNumber, double price, Builder builder, string model, Type type, Wood backWood, Wood topWood)
        {
            guitars.Add(new Guitar(serialNumber, price, builder, model, type, backWood, topWood));
        }

        public Guitar getGuitar(string serialNumber)
        {
            return guitars.First(guitar => guitar.getSerialNumber().Equals(serialNumber));
        }

        public List<Guitar> search(Guitar searchGuitart)
        {
            List<Guitar> matchingGuitars = new List<Guitar>();
            matchingGuitars.Clear();

            for (int i = 0; i < guitars.Count; ++i)
            {
                Guitar guitar = guitars[i];

                if (searchGuitart.getBuilder() != guitar.getBuilder())
                {
                    continue;
                }

                string model = searchGuitart.getModel();
                if ((model != null) && (!model.Equals("")) && (!model.ToLower().Equals(guitar.getModel().ToLower())))
                {
                    continue;
                }

                if (searchGuitart.getType() != guitar.getType())
                {
                    continue;
                }

                if (searchGuitart.getBackWood() != guitar.getBackWood())
                {
                    continue;
                }

                if (searchGuitart.getTopWood() != guitar.getTopWood())
                {
                    continue;
                }

                matchingGuitars.Add(guitar);

            }

            return matchingGuitars;

        }

    }
}

Tester.cs (測試程式表單)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ricksGuitars_start
{
    public partial class fmFindGuitarTester : Form
    {
        Inventory inventory = new Inventory();
        Guitar whatErinLike = new Guitar("", 0, Builder.FENDER, "Stratocastor", Type.ELECTRIC, Wood.ALDER, Wood.ALDER);

        public fmFindGuitarTester()
        {
            InitializeComponent();

            inventory.addGuitar("V95693", 1499.95, Builder.FENDER, "Stratocastor", Type.ELECTRIC, Wood.ALDER, Wood.ALDER);
            inventory.addGuitar("V9512", 1549.95, Builder.FENDER, "Stratocastor", Type.ELECTRIC, Wood.ALDER, Wood.ALDER);
        }

        private void fmFindGuitarTester_Load(object sender, EventArgs e)
        {
            List<Guitar> matchingGuitars = inventory.search(whatErinLike);
            string sRet = "Sorry, we have nothing for you.";
            if (matchingGuitars.Count > 0)
            {
                sRet = "";
                foreach (Guitar guitar in matchingGuitars)
                {
                    string ss = "You might like this " + guitar.getBuilder().ToString() + " " + guitar.getModel() + " " +
                        guitar.getType().ToString() + " guitar : \r\n" + guitar.getBackWood().ToString() + " back and sides, " +
                        guitar.getTopWood().ToString() + " top.\r\nYou can have it for only $" + guitar.getPrice().ToString() + "!\r\n\r\n";
                    sRet += ss;
                }
            }
            tbMsg.Text = sRet;
        }
    }
}

【測試結果】

版次:v0.3

第二站:增加軟體彈性

這裡我們來探討一下,Invertory 類別裡的 search() 方法,當使用者要搜尋心目中離想吉他時,需要傳入整個吉他物件,但是序號(serialNumber)與價格(price)並不需要提供,序號對每一把吉他是唯一的,且 Rick 並不想將價格列入搜尋條件之一,因此使用者只需要提供要做比對的規格即可,並不需要提供整的吉他物件。因此,我們需要增加一個 GuitarSpec 的類別,來作為使用者搜尋吉他時,傳入 search() 方法的物件。

但是,這樣 GuitarSpec 的部份內容,不就會與 Guitar 重複了嗎?

這時就要利用基礎封裝技巧,將程式分成一組一組合乎邏輯的零件。原則如下:

  1. 物件應該做其名稱所指之事。
  2. 每個物件應該代表單一概念。
  3. 未使用的特性(屬性 property)是無用的贈品。

封裝讓你將應用程式零件的內部工作隱藏起來,但是讓零件在做什麼更顯得清楚。

因此,重新修改如下:

【類別圖】

【程式碼】

GuitarSpec.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public enum Builder { FENDER, MARTIN, GIBSON, COLLINGS, OLSON, RYAN, PRS, UNSPECIFIED };
    public enum Type { ACOUSTIC, ELECTRIC, UNSPECIFIED };
    public enum Wood { INDIAN_ROSEWOOD, BRAZILIAN_ROSEWOOD, MAHOGANY, MAPLE, COCOBOLO, CEDAR, ADIRONDACK, ALDER, SITKA, UNSPECIFIED };

    public class GuitarSpec
    {
        private string model;
        private Builder builder;
        private Type type;
        private Wood backWood, topWood;

        public GuitarSpec(Builder builder, string model, Type type, Wood backWood, Wood topWood)
        {
            this.model = model;
            this.builder = builder;
            this.type = type;
            this.backWood = backWood;
            this.topWood = topWood;
        }

        public Builder getBuilder()
        {
            return builder;
        }

        public string getModel()
        {
            return model;
        }

        public Type getType()
        {
            return type;
        }

        public Wood getBackWood()
        {
            return backWood;
        }

        public Wood getTopWood()
        {
            return topWood;
        }
    }
}

Guitar.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Guitar
    {
        private string serialNumber;
        private double price;
        private GuitarSpec spec;

        public Guitar(string serialNumber, double price, Builder builder, string model, Type type, Wood backWood, Wood topWood)
        {
            this.serialNumber = serialNumber;
            this.price = price;
            this.spec = new GuitarSpec(builder, model, type, backWood, topWood);
        }

        public string getSerialNumber()
        {
            return serialNumber;
        }

        public double getPrice()
        {
            return price;
        }

        public void setPrice(double newPrice)
        {
            price = newPrice;
        }

        public GuitarSpec getSpec()
        {
            return spec;
        }

    }
}

Inventory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Inventory
    {
        private List<Guitar> guitars;

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

        public void addGuitar(string serialNumber, double price, Builder builder, string model, Type type, Wood backWood, Wood topWood)
        {
            guitars.Add(new Guitar(serialNumber, price, builder, model, type, backWood, topWood));
        }

        public Guitar getGuitar(string serialNumber)
        {
            return guitars.First(guitar => guitar.getSerialNumber().Equals(serialNumber));
        }

        public List<Guitar> search(GuitarSpec searchGuitart)
        {
            List<Guitar> matchingGuitars = new List<Guitar>();
            matchingGuitars.Clear();

            for (int i = 0; i < guitars.Count; ++i)
            {
                Guitar guitar = guitars[i];
                GuitarSpec guitarSpec = guitar.getSpec();

                if (searchGuitart.getBuilder() != guitarSpec.getBuilder())
                {
                    continue;
                }

                string model = searchGuitart.getModel();
                if ((model != null) && (!model.Equals("")) && (!model.ToLower().Equals(guitarSpec.getModel().ToLower())))
                {
                    continue;
                }

                if (searchGuitart.getType() != guitarSpec.getType())
                {
                    continue;
                }

                if (searchGuitart.getBackWood() != guitarSpec.getBackWood())
                {
                    continue;
                }

                if (searchGuitart.getTopWood() != guitarSpec.getTopWood())
                {
                    continue;
                }

                matchingGuitars.Add(guitar);

            }

            return matchingGuitars;

        }

    }
}

Tester.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ricksGuitars_start
{
    public partial class fmFindGuitarTester : Form
    {
        Inventory inventory = new Inventory();
        GuitarSpec whatErinLike = new GuitarSpec(Builder.FENDER, "Stratocastor", Type.ELECTRIC, Wood.ALDER, Wood.ALDER);

        public fmFindGuitarTester()
        {
            InitializeComponent();

            inventory.addGuitar("V95693", 1499.95, Builder.FENDER, "Stratocastor", Type.ELECTRIC, Wood.ALDER, Wood.ALDER);
            inventory.addGuitar("V9512", 1549.95, Builder.FENDER, "Stratocastor", Type.ELECTRIC, Wood.ALDER, Wood.ALDER);
        }

        private void fmFindGuitarTester_Load(object sender, EventArgs e)
        {
            List<Guitar> matchingGuitars = inventory.search(whatErinLike);
            string sRet = "Sorry, we have nothing for you.";
            if (matchingGuitars.Count > 0)
            {
                sRet = "";
                foreach (Guitar guitar in matchingGuitars)
                {
                    GuitarSpec guitarSpec = guitar.getSpec();
                    string ss = "You might like this " + guitarSpec.getBuilder().ToString() + " " + guitarSpec.getModel() + " " +
                        guitarSpec.getType().ToString() + " guitar : \r\n" + guitarSpec.getBackWood().ToString() + " back and sides, " +
                        guitarSpec.getTopWood().ToString() + " top.\r\nYou can have it for only $" + guitar.getPrice().ToString() + "!\r\n\r\n";
                    sRet += ss;
                }
            }
            tbMsg.Text = sRet;
        }
    }
}

【測試結果】

版次:v1.0

第三站:讓程式易於重用與擴展

現在,我們再來看一下 Inventory 類別的 search() 方法,此時會發現如果 Rick 需要變更搜尋條件或增加項目時,要更動的地方還真不少。

例如 Rick 想要再吉他規格 (GuitarSpec) 中增加一個弦數 (numStrings) 作為搜尋條件時,會需要更動哪些地方呢?

  1. 在 GuitarSpec 類別中增加一個 int numStrings 的屬性。
  2. GuitarSpec 類別的建構子也要增加 int numStrings 參數作為輸入。
  3. 在 GuitarSpec 類別中增加 getNumStrings() 方法以取得吉他弦數。
  4. Guitar 類別的建構子也要增加 int numStrings 參數作為輸入。
  5. 在 Inventory 類別中的 addGuitar() 方法中增加 int numStrings 參數作為輸入。
  6. 在 Inventory 類別中的 search() 方法內容也要修改,增加吉他弦數的比對。

從以上幾點發現了兩個重大問題:

  1. 原本只是想在 GuitarSpec 類別中增加一個屬性,卻至少要修改五個地方(上述的 1~5 點),而且除了動到本身 GuitarSpec 類別外,還要動到 Guitar 與 Inventory 類別。
  2. Inventory 類別中的 seach() 方法無法重複利用,實做的方法太過於依賴其他類別。

天啊~僅僅是要為吉他規格 (GuitarSpec) 增加一個項目,卻要如此勞師動眾,真是牽一髮而動全身啊~這樣的程式叫人如何維護,想到就頭皮發麻~    (好熟悉的內心吶喊~T.T)

如何挽救這程式呢?其實方法很簡單,只要為程式做下列調整,以後就不必擔心一樣的問題囉~

  1. 首先,因為我們是要新增一個 int numStrings 屬性到 GuitarSpec 類別中,所以前面提到的第 1~3 點是免不了的,也是應當要做的。
  2. 再來,第 4 點要做的應該是 Guitar 類別的建構子參數修改成以 GuitarSpec 物件傳入做初始化。(如此便不會受 GuitarSpec 類別的屬性變更而影響了)
  3. 第 5 點要做的與第 4 點類似,addGuitar() 方法參數改為以 GuitarSpec 物件傳入。(如此便不會受 GuitarSpec 類別的屬性變更而影響了)
  4. 第 6 點中的 search() 方法,應該修改成將兩個 GuitarSpec 物件的比較交給 GuitarSpec 類別來處裡,而不是在 Inventory 類別中直接做比較。(如此便不會受 GuitarSpec 類別的屬性變更而影響了)

修改後的類別圖與程式碼如下:

【類別圖】

【程式碼】

GuitarSpec.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public enum Builder { FENDER, MARTIN, GIBSON, COLLINGS, OLSON, RYAN, PRS, UNSPECIFIED };
    public enum Type { ACOUSTIC, ELECTRIC, UNSPECIFIED };
    public enum Wood { INDIAN_ROSEWOOD, BRAZILIAN_ROSEWOOD, MAHOGANY, MAPLE, COCOBOLO, CEDAR, ADIRONDACK, ALDER, SITKA, UNSPECIFIED };

    public class GuitarSpec
    {
        private Builder builder;
        private string model;
        private Type type;
        private int numStrings;     //新增吉他弦數屬性
        private Wood backWood, topWood;

        public GuitarSpec(Builder builder, string model, Type type, int unmStrings, Wood backWood, Wood topWood)
        {
            this.builder = builder;
            this.model = model;
            this.type = type;
            this.numStrings = numStrings;
            this.backWood = backWood;
            this.topWood = topWood;
        }

        public Builder getBuilder()
        {
            return builder;
        }

        public string getModel()
        {
            return model;
        }

        public Type getType()
        {
            return type;
        }

        public int getNumStrings()
        {
            return numStrings;
        }

        public Wood getBackWood()
        {
            return backWood;
        }

        public Wood getTopWood()
        {
            return topWood;
        }

        public bool matches(GuitarSpec otherSpec)
        {
            if (builder != otherSpec.builder)
            {
                return false;
            }

            if ((model != null) && (!model.Equals("")) && (!model.ToLower().Equals(otherSpec.model.ToLower())))
            {
                return false;
            }

            if (type != otherSpec.type)
            {
                return false;
            }

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

            if (backWood != otherSpec.backWood)
            {
                return false;
            }

            if (topWood != otherSpec.topWood)
            {
                return false;
            }

            return true;
        }

    }
}

Guitar.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Guitar
    {
        private string serialNumber;
        private double price;
        private GuitarSpec spec;

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

        public string getSerialNumber()
        {
            return serialNumber;
        }

        public double getPrice()
        {
            return price;
        }

        public void setPrice(double newPrice)
        {
            price = newPrice;
        }

        public GuitarSpec getSpec()
        {
            return spec;
        }

    }
}

Inventory.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ricksGuitars_start
{
    public class Inventory
    {
        private List<Guitar> guitars;

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

        public void addGuitar(string serialNumber, double price, GuitarSpec spec)
        {
            guitars.Add(new Guitar(serialNumber, price, spec));
        }

        public Guitar getGuitar(string serialNumber)
        {
            return guitars.First(guitar => guitar.getSerialNumber().Equals(serialNumber));
        }

        public List<Guitar> search(GuitarSpec searchGuitart)
        {
            List<Guitar> matchingGuitars = new List<Guitar>();
            matchingGuitars.Clear();

            for (int i = 0; i < guitars.Count; ++i)
            {
                Guitar guitar = guitars[i];
                if (guitar.getSpec().matches(searchGuitart))
                {
                    matchingGuitars.Add(guitar);
                }
            }

            return matchingGuitars;

        }

    }
}

Tester.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace ricksGuitars_start
{
    public partial class fmFindGuitarTester : Form
    {
        Inventory inventory = new Inventory();
        GuitarSpec whatErinLike = new GuitarSpec(Builder.FENDER, "Stratocastor", Type.ELECTRIC, 6, Wood.ALDER, Wood.ALDER);

        public fmFindGuitarTester()
        {
            InitializeComponent();

            inventory.addGuitar("V95693", 1499.95, new GuitarSpec(Builder.FENDER, "Stratocastor", Type.ELECTRIC, 6, Wood.ALDER, Wood.ALDER));
            inventory.addGuitar("V9512", 1549.95, new GuitarSpec(Builder.FENDER, "Stratocastor", Type.ELECTRIC, 6, Wood.ALDER, Wood.ALDER));
            inventory.addGuitar("1092", 12995.95, new GuitarSpec(Builder.OLSON, "SJ", Type.ACOUSTIC, 12, Wood.INDIAN_ROSEWOOD, Wood.CEDAR));
            inventory.addGuitar("566-62", 8999.95, new GuitarSpec(Builder.RYAN, "Cathedral", Type.ACOUSTIC, 12, Wood.COCOBOLO, Wood.CEDAR));
        }

        private void fmFindGuitarTester_Load(object sender, EventArgs e)
        {
            List<Guitar> matchingGuitars = inventory.search(whatErinLike);
            string sRet = "Sorry, we have nothing for you.";
            if (matchingGuitars.Count > 0)
            {
                sRet = "";
                foreach (Guitar guitar in matchingGuitars)
                {
                    GuitarSpec guitarSpec = guitar.getSpec();
                    string ss = "You might like this " + guitarSpec.getBuilder().ToString() + " " + guitarSpec.getModel() + " " +
                        guitarSpec.getType().ToString() + " guitar : \r\n" + guitarSpec.getBackWood().ToString() + " back and sides, " +
                        guitarSpec.getTopWood().ToString() + " top.\r\nYou can have it for only $" + guitar.getPrice().ToString() + "!\r\n\r\n";
                    sRet += ss;
                }
            }
            tbMsg.Text = sRet;
        }
    }
}

【測試結果】

結論

回顧來時路:

Step 1:確認你的軟體做客戶要它做的事。

  1. 修正了 Inventory 類別中 search() 方法的功能行問題。
  2. 增加了 Inventory 類別中 search() 方法的功能性,使其能傳回比較結果清單。

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

  1. 將 GuitarSpec 從 Guitar 類別中獨立出來,讓每一類別只做一件事並且把它做好。(單一職責)

Step 3:努力達成可維護、可重用的設計。

  1. 將比較功能從 Inventory 類別的 search() 方法中,移至 GuitarSpec 類別的 matches() 方法中,讓物件彼此獨立。(避免牽一髮而動全身)

完成偉大軟體,沒有撇步,貫徹上述三步驟,如此而已。

但是,說來容易做來難,知道不一定做得到,上述三步驟是方法,「貫徹」才是重點,做事如此,作人也是如此。

 

本文程式參考:http://www.headfirstlabs.com/books/hfooad/