[深入淺出物件導向分析與設計] 心得1 : 偉大軟體由此開始
閱讀 深入淺出物件導向分析與設計(Head First Object-Oriented Analysis and Design) 後,發現要撰寫出偉大軟體(great software)相當不容易,反思自己明明都知道書中提到的 OOA&D (Object-oriented analysis and design) ,但卻無法每次將此項能力好好使用,可能的原因是時程太短或對此項技能熟悉或是其他原因,造成常常寫出只要能動程式碼,不管後續程式維護性還是擴充性,如果能將OOA&D能力變成被動能力,只要寫程式自動觸發該有多好,此書將OOA&D以簡單與詼諧的方式進行介紹,只要能將書中提供三個步驟反覆練習,相信很快就能將此項能力從主動變成被動能力。
什麼事偉大軟體?
以客戶導向設計師:
將客戶的想法轉換成軟體,它能夠如客戶預期產生結果。
客戶是神,客戶說的都是對的。以物件導向設計師:
沒有重複的程式碼,每個物件都控制得當,擴展也相當容易,因為設計出既穩固又有彈性(參考)。
設計大師:
讓物件保持鬆散耦合(losely coupled),讓程式碼「禁止修改而關閉,允許擴展而開放」
大師講的話,一定黑人問號冒出(參考)。有助於程式碼重用性,因此不必重作每件事情,可以一直被使用。
上述其實都是只對一部分,要注意偉大軟體都不只是一件事情,需要將下述都達成
- 偉大軟體必須讓客戶滿意,做客戶要它做的事。
- 偉大軟體是設計良好(well-designed)、編成良好(well-coded)、並且易維護、可重用及易擴展。
如何達成呢? 書中有提到可以反覆確認偉大軟體的三個步驟
偉大軟體的三步驟
確認軟體做客戶要它做的事:
重點放在客戶上,確保軟體做它應該先做的事。這裡就需要從客戶哪裡 收集需求(requirement) 與 分析(analysis) 。
應用基本OO原則,增加軟體的彈性:
軟體開始運行後,能找出任何疏忽造成重複的程式碼,並確認使用良好的OO編成技術。
努力達成可維護、可重利用的設計:
有了良好的物件導向的程式後,就要運用設計模式與OO原則,確保程式在日後都運行。
情境
客戶:Rick 的亂彈彈吉他店。
需求:希望可以原本丟棄紙本作業,建立吉他庫存管理應用程式,可以透過此程式進行搜尋,並幫忙客戶配對到心目中的夢幻吉他。
下列範例會適當簡化,並以書中提到重點進行介紹。完整範例(參考)
版本一
依據客戶的需求設計出 Guitar
跟 Inventory
類別來管理吉他,這時候已經躍躍欲試想迎接第一個顧客(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.
版本二
偉大軟體的第一個步驟:確認軟體做客戶要它做的事,很明顯沒有符合哪我該如何調整軟體,需解決你些問題呢?
解決找不到合適吉他
=> string 忽略大小寫,但還有沒有更好的方式呢?
=> 如果知道列舉類型,就可以避免客戶或是自己輸入上錯誤,是一個還不錯解決方法。客戶其實希望適合的吉他,都可以展示給顧客知道
=> 就避免增加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.cs
跟 Inventory.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