永遠希望你的軟體更容易擴充?當你的程式遇到一些問題需要修改的時候,可能意味著你的軟體需要更容易擴充。為了很好的擴充你的軟體,你需要一些分析,整體的設計,並且研究物件導向原則如何能解耦你的軟體。最後你會發現高內聚協助你解耦合。
回到查詢工具的類圖,我們看看有哪些地方不合理:
1、 對於addInstrument方法每次添加一種樂器類型就要添加對這個方法進行修改
2、 對於search方法每次添加一種樂器類型就要添加一個相對應的search方法
首先看看search方法的修改:
解決每次添加一種樂器類型就要添加一個相對應的search方法,這個問題的關鍵在於InstrumentSpec類型是一個抽象類別型,我們不能執行個體化它。但是如果把它變為實際類型就能夠解決問題了嗎?
把InstruemntSpec變為實際類型後給我們帶來了什嗎?
首先是search方法的變化:
public List<Instrument> Search(InstrumentSpec Spec)
{
List<Instrument> list = new List<Instrument>();
foreach (Instrument instrument in inventory)
{
if(instrument.GetSpec().matches(Spec))
{
list.Add(instrument);
}
}
return list;
}
我們來分析一下:
在這個程式中為什麼需要一個Instrument類型?
大多數樂器至少有一小部公用屬性,如:serial number和price。Instrument類型儲存這些公用屬性,並且特殊的樂器類型可以從Instrument類型延伸。
所有樂器的公用部分是什嗎?
Serial Number,price和一些規格雖然說在不同的樂器類型中那些規格可能不同
樂器之間的不同是什嗎?
規格:每一種樂器都包含一些不同的屬性,有不同的樂器類型和不同的建構函式。
即使現在search方法看起來好一些了,但是Inventory中的addInstrment方法還有些問題。原先的程式因為每一個Instrument type有一個相對應的擴充類型,所以把把Instrument抽象化。
通常建立擴充類型的原因是擴充類型與超級類型的行為是不同的。但是在查詢工具中,Guitar的行為和其他的樂器類型不同嗎?他們僅僅是有一些不同的屬性,而沒有不同的行為。由此產生一個問題:我們真的需要那些擴充類型嗎?
在這個查詢工具中所有的樂器都有相同的行為。因此instrument type擴充類型存在只有兩個原因:
1、因為Instrument反映了一個概念,不是實際的對象。我們必須為每一個Instrument type建立一個擴充類型。
2、 每一個不同的樂器類型有不同的屬性,並且使用不同的InstrmentSpec擴充類型,所以我們需要為每一個instrument建立特殊的建構函式。
看起來是兩條很好的理由,但是結果是使軟體很難變化,還記得寫好軟體的第二步嗎?應有物件導向原則使其容易擴充(Apply basic OO principles to add flexibility)
在原來的程式中使用了繼承、多態、抽象。但是現在應該考慮一下封裝變化。如何把變化的部分從相對穩定的部分中分離出來。
現在來改變一下原先的設計:
根據上面的分析,實際上Instrument的擴充類型只是屬性不同,類型中的行為沒有什麼變化。所以可以考慮去掉所有的擴充類型。
我們現在需要一個新的屬性InstrumentType來表示Instrument的類型。對於instrument類型中的不同屬性,我們可以試著有動態方式得到他們。這個問題的關鍵在於我們用什麼方式去儲存這些屬性。實際上屬性無非兩部分組成:1、屬性名稱2、屬性值,只要把這兩部分對應起來就可以。試想一下,可以用Hashtable對象去儲存這些屬性。
當你的對象中有些變化的屬性,或者說不確定的屬性,使用集合動態儲存這些屬性(When you have a set of properties that vary across you objects, use a collection, like a Map, to store those properties dynamically.)原書使用Java編寫,在DotNet中的可以使用Hashtable類型。
這樣你可以從原先的類型中移除很多方法,並且當需要添加屬性的時候避免修改代碼在你的程式中(You’ll remove lots of methods from your classes, and avoid having to change your code when new properties are added to your app.)
那麼現在看看在程式中應該如何設計Instrument和InstrumentSpec類型
下面去具體到代碼上,看看程式怎麼寫:
public class InstrumentSpec
{
private Hashtable _properties;
public InstrumentSpec(Hashtable properties)
{
if (properties != null)
{
_properties = properties;
}
else
{
_properties = new Hashtable();
}
}
public Object GetProperty(String PropertyName)
{
return _properties[PropertyName];
}
public Hashtable GetProperties()
{
return _properties;
}
public Boolean matches(InstrumentSpec OtherSpec)
{
Boolean Flag = true;
Hashtable OtherProperties = OtherSpec.GetProperties();
foreach (DictionaryEntry DE in OtherProperties)
{
String PropertyName = DE.Key.ToString();
if (_properties[PropertyName] != DE.Value)
{
Flag = false;
break;
}
}
return Flag;
}
}
public class Inventory
{
public Inventory()
{
_instruments = new List<Instrument>();
}
private List<Instrument> _instruments;
public List<Instrument> instruments
{
get { return _instruments; }
set { _instruments = value; }
}
public void addInstrument(String serialNumber, Double price, InstrumentSpec spec)
{
instruments.Add(new Instrument(serialNumber, price, spec));
}
public List<Instrument> Search(InstrumentSpec Spec)
{
List<Instrument> list = new List<Instrument>();
foreach (Instrument instrument in _instruments)
{
if (instrument.Spec.matches(Spec))
{
list.Add(instrument);
}
}
return list;
}
}
public enum InstrumentType
{
GUITAR, BANJO, DOBRO, FIDDLE, BASS, MANDOLIN
}
Instrument類型和以前差不多:
public class Instrument
{
public Instrument(String serialNumber, Double price, InstrumentSpec spec)
{
_SerialNumber = serialNumber;
_Price = price;
_Spec = spec;
}
private String _SerialNumber;
private Double _Price;
private InstrumentSpec _Spec;
public String SerialNumber
{
get { return _SerialNumber; }
set { _SerialNumber = value; }
}
public Double Price
{
get { return _Price; }
set { _Price = value; }
}
public InstrumentSpec Spec
{
get { return _Spec; }
set { _Spec = value; }
}
}
用用戶端程式測試一下:
class Program
{
private static Inventory inventory;
static void Main(string[] args)
{
inventory = new Inventory();
InventoryInit();
FindInstrument();
Console.Read();
}
static void FindInstrument()
{
Hashtable FindProps = new Hashtable();
FindProps.Add("builder", "Collings");
FindProps.Add("model", "CJ1");
InstrumentSpec FindSpec = new InstrumentSpec(FindProps);
List<Instrument> list = inventory.Search(FindSpec);
if (list.Count > 0)
{
foreach (Instrument instrument in list)
{
Console.WriteLine("Find the SerialNumber is " + instrument.SerialNumber.ToString());
}
}
else
{
Console.WriteLine("Can NOT find");
}
}
static void InventoryInit()
{
Hashtable properties = new Hashtable();
properties.Add("instrumentType", InstrumentType.GUITAR);
properties.Add("builder", "Collings");
properties.Add("model", "CJ");
properties.Add("type", "Acoustic");
properties.Add("numStrings", 6);
properties.Add("topWood", "Indian");
properties.Add("backWood", "Sitka");
inventory.addInstrument("11277", 4000, new InstrumentSpec(properties));
Hashtable properties2 = new Hashtable();
properties2.Add("instrumentType", InstrumentType.MANDOLIN);
properties2.Add("builder", "Collings");
properties2.Add("model", "CJ1");
properties2.Add("type", "Acoustic");
properties2.Add("numStrings", 6);
properties2.Add("topWood", "Indian");
properties2.Add("backWood", "Sitka");
inventory.addInstrument("11278", 4000, new InstrumentSpec(properties2));
}
}
輸出結果:
對這個查詢工具已經作了很多的工作,但是不要忘記我們要做什麼!現在再回來看看這個程式的類圖:
再回來看看那幾個問題:
1、 如果要加入一種新的樂器類型,有多少現有類型需要添加?
現有的程式不需要添加新的類型,現在我們已經把所有的樂器規格從Instrument和InstrumentSpec類型中脫離。
2、 如果變換一種樂器類型,有多少現有類型需要變化?
一個,修改InstrumentType,加入新的樂器類型
3、 如果說客戶要求記錄每個樂器的生產時間,有多少類型需要變化?
不需要,可以把它加入InstrumentSpec的properties中
4、 如果說客戶給某種樂器修改屬性,有多少類型需要變化?
如果說屬性值與其他類型無關,則不需要變化,但如果屬性值是一個枚舉類型,那麼就有一個類型需要變化
現在看起來程式比以前更容易擴充了,但是什麼是內聚(cohesive)?內聚類型只做一件事情並且不做其他的事情(A cohesive class does really well and does not try to do or be something else)
內聚表現元素之間串連的程度。包括模組、類型或者對象。軟體高內聚描述應用程式的單個類型的職責。還記得在上一篇文章中(OO Catastrophe)中有這樣一句話:“Every class should attempt to make sure that it has only one reason to this, the death of many a badly designed piece of software”。
現在我們再回來說說內聚,還記得上一章提到過的“變化”嗎?Every class should attempt to make sure that it has only one reason to this, the death of many a badly designed piece of software.實際上它的意思就是每一個類型儘可能僅有一個變化點或者說僅有一個變化原因。內聚表現在程式中類型之間的緊密關係程度。還記得那個汽車的例子嗎?
每一個類型方法被相對較好的定義。每一個類型都是高內聚類型,並且很容易變化,但不影響其他的類型。回來看看我們所作的改變是否使得樂器的查詢工具高內聚?程式中的對象是否解耦合?是否可以很容易的應對變化?
從這課當中我們又能總結出一些有關物件導向分析設計的東西。看看我們已經知道了多少。