http://www.tracefact.net/Software-Design/A-Sample-Design.aspx
本文是《Object-Oriented Analysis and Design》一書(shū)第一章和第五章的讀書(shū)筆記。我對(duì)書(shū)中提供的一個(gè)范例程序進(jìn)行了總結(jié)和整理,通過(guò)逐步優(yōu)化這個(gè)樂(lè)器管理的范例程序,分析了進(jìn)行程序設(shè)計(jì)時(shí)需要注意到的一些問(wèn)題。
1.簡(jiǎn)單直接的實(shí)現(xiàn)
這個(gè)程序起初的需求很簡(jiǎn)單: 我們需要?jiǎng)?chuàng)建一個(gè)吉他管理程序,它能夠保存所有的吉他信息,并且可以通過(guò)輸入吉他的參數(shù)來(lái)進(jìn)行查詢(xún),返回查詢(xún)結(jié)果。 我們知道一個(gè)優(yōu)良的軟件應(yīng)該從兩個(gè)角度去衡量:
- 從用戶(hù)的角度,軟件應(yīng)該是符合用戶(hù)期望的,也就是滿(mǎn)足了用戶(hù)的需求,可以完成用戶(hù)期望它完成的工作。
- 從開(kāi)發(fā)者的角度,軟件應(yīng)該是易于維護(hù)的、可擴(kuò)展的,以及可重用的。
這兩個(gè)方面應(yīng)該是遞進(jìn)的,也就是說(shuō)軟件首先要能滿(mǎn)足用戶(hù)的需求。所以我們先看如何完成用戶(hù)的需求,我們定義一個(gè)類(lèi)Guitar,它代表了吉他;以及一個(gè)Inventory類(lèi),它用于維護(hù)現(xiàn)有吉他的信息,可以進(jìn)行添加、查找等操作:
然后我們來(lái)看下實(shí)現(xiàn):
// 吉他類(lèi)
public
class
Guitar
{
???
private
string
serialNumber;??
// 序列號(hào)
???
private
string
builder;???????
// 廠(chǎng)商
???
private
string
model;?????????
// 型號(hào)
???
private
string
type;??????????
// 類(lèi)型
???
private
string
backWood;??????
// 后部材質(zhì)
???
private
string
topWood;???????
// 前面材質(zhì)
???
private
double
price;?????????
// 價(jià)格
???
public
Guitar(
string
serialNumber,
string
builder,
string
model,
string
type,
string
backWood,
string
topWood,
double
price) {
???????
this
.backWood = backWood;
???????
this
.builder = builder;
???????
this
.model = model;
???????
this
.price = price;
???????
this
.serialNumber = serialNumber;
???????
this
.topWood = topWood;
???????
this
.type = type;
??? }
???
???
// 公有屬性省略
}
public
class
Inventory
{
???
// 維護(hù)現(xiàn)有的所有吉他
???
private
List<Guitar> guitarList;
???
public
Inventory() {
??????? guitarList =
new
List
<Guitar>();
??? }
???
// 向列表中添加 吉他
???
public
void
AddGuitar(
string
serialNumber,
string
builder,
string
model,
string
type,
string
backWood,
string
topWood,
double
price) {
???????
Guitar
guitar =
new
Guitar
(serialNumber, builder, model, type,backWood, topWood, price);
??????? guitarList.Add(guitar);
??? }
???
// 搜索吉他列表,尋找滿(mǎn)足searchGuitar參數(shù)的吉他
???
// 如果searchGuitar的參數(shù)為null或者"",則忽略此參數(shù)
???
public
Guitar Search(Guitar searchGuitar) {
??????? List<Guitar>.
Enumerator
it =? guitarList.GetEnumerator();
???????
// 價(jià)格Price和序列號(hào)SerialNumber不參與查詢(xún)
???????
Guitar
result =
null
;
???????
while
(it.MoveNext()) {
???????????
Guitar
guitar = it.Current;
???????????
string
builder = searchGuitar.Builder;
???????????
if
(!
String
.IsNullOrEmpty(builder) &&
??????????????? !builder.Equals(guitar.Builder))
???????????????
continue
;
???????????
string
model = searchGuitar.Model;
???????????
if
(!
String
.IsNullOrEmpty(model) &&
??????????????? !model.Equals(guitar.Model))
???????????????
continue
;
???????????
string
type = searchGuitar.Type;
???????????
if
(!
String
.IsNullOrEmpty(type) &&
??????????????? !type.Equals(guitar.Type))
???????????????
continue
;
???????????
string
backWood = searchGuitar.BackWood;
???????????
if
(!
String
.IsNullOrEmpty(backWood) &&
??????????????? !backWood.Equals(guitar.BackWood))
???????????????
continue
;
???????????
string
topWood = searchGuitar.TopWood;
???????????
if
(!
String
.IsNullOrEmpty(topWood) &&
??????????????? !topWood.Equals(guitar.TopWood))
???????????????
continue
;
??????????? result = guitar;??
// 找到第一個(gè)匹配結(jié)果就返回
???????????
return
result;
??????? }
???????
return
result;
??? }
}
接下來(lái)我們向Inventory中添加一些Guitar,然后來(lái)測(cè)試下查找功能:
class
Program
{
???
static
void
???????
Inventory
inventory =
new
Inventory
();
??????? initializeInventory(inventory);
???????
// 想要查找的Guitar
???????
Guitar
wanted =
new
Guitar
(
"", "fender", "Stratocastor", "electric", "Alder", "Alder"
, 0);
???????
// 返回符合條件的結(jié)果
???????
Guitar
guitar = inventory.Search(wanted);
???????
if
(guitar !=
null
) {
// 找到符合條件的結(jié)果
???????????
Console
.WriteLine(
"You might like
this
{0} {1} {2} guitar:\n {3} back and sides,\n {4} top.\n You can have it
for
only ${5} !"
, guitar.Builder, guitar.Model, guitar.Type, guitar.BackWood, guitar.TopWood, guitar.Price);
??????? }
else
{
???????????
Console
.WriteLine(
"Sorry, nothing found."
);
??????? }
??? }
???
private
static
void
initializeInventory(Inventory item) {
??????? item.AddGuitar(
"V95693", "Fender", "Stratocastor", "electric", "Alder", "Alder"
, 1499.95D);
?????? item.AddGuitar(
"B95315", "Gibson", "SpecialKind", "electric", "Maple", "Cedar"
, 2134.30D);
??????? item.AddGuitar(
"V95694", "Fender", "Stratocastor", "electric", "Alder", "Alder"
, 1599.95D);
??? }
}
結(jié)果我們發(fā)現(xiàn)并未返回搜索結(jié)果,但是我們看下InitializeInventory()方法,確實(shí)存在一個(gè)Guitar,它的屬性完全符合要查找的Guitar實(shí)例wanted。為什么查詢(xún)卻找不到呢?仔細(xì)查看一下,我們發(fā)現(xiàn)添加到Inventory中的Guitar的制造商Builder是"Fender",而輸入的searchGuitar的Builder屬性為"fender"。我們知道,在C#中字符串的大小寫(xiě)是敏感的,即是說(shuō) "a"=="A"返回的是false,所以"Fender"不等于"fender"。所以我們遇到的問(wèn)題是:在不要求嚴(yán)格匹配大小寫(xiě)的情況下,對(duì)于字符串的比較,我們應(yīng)該先全部轉(zhuǎn)換為大寫(xiě)或者小寫(xiě),然后再進(jìn)行比較。但是這樣做就沒(méi)有問(wèn)題了么?我們看一下Guitar類(lèi)的定義,除了price為double類(lèi)型以外,其余均為string。而某些屬性,比如說(shuō)吉他的發(fā)聲類(lèi)型type,只有兩種可能,一種是傳統(tǒng)的、通過(guò)震動(dòng)發(fā)聲的(Acoustic),一種是電子發(fā)聲的(Electric);對(duì)于制造商Builder,可能只有有限的幾個(gè)廠(chǎng)家。但使用string類(lèi)型時(shí),我們無(wú)法對(duì)于這些屬性的取值進(jìn)行限制,此時(shí),我們應(yīng)該考慮: 如果對(duì)象的屬性是由有限個(gè)項(xiàng)目構(gòu)成的集合,我們最好定義一個(gè)枚舉,并設(shè)置對(duì)象的屬性為這個(gè)枚舉類(lèi)型。
所以對(duì)于上面程序可以進(jìn)行的第一個(gè)改進(jìn),就是定義枚舉,并將部分屬性的值,由string改為枚舉類(lèi)型:
// 發(fā)生類(lèi)型
public
enum
SoundType
{
??? Acoustic, Electric
}
// 制造商
public
enum
Builder
{
??? Fender, Martin, Gibson, Collings, Olson
}
// 木料
public
enum
Wood
{
??? IndianRoseWood, BrazilianRoseWood, Mahogany, Maple, Cocobolo, Cedar, Alder, Sitka
}
同時(shí)修改Guitar類(lèi)和Inventory類(lèi),讓它們使用這些枚舉作為字段類(lèi)型:
public
class
Guitar
{
???
private
string
serialNumber;??
// 序列號(hào)
???
private
Builder
builder;??????
// 廠(chǎng)商
???
private
string
model;?????????
// 型號(hào)
???
private
SoundType
type;???????
// 類(lèi)型
???
private
Wood
backWood;????????
// 后部材質(zhì)
???
private
Wood
topWood;?????????
// 前面材質(zhì)
???
private
double
price;?????????
// 價(jià)格
???
???
// 構(gòu)造函數(shù)、屬性做相應(yīng)修改,此處略
}
此時(shí),我們發(fā)現(xiàn)上面例子Inventory中符合搜索條件的有兩項(xiàng),而Search()方法只能返回查詢(xún)到的第一個(gè)結(jié)果,所以第二處改進(jìn)就是對(duì)Inventory的Search()方法進(jìn)行修改,讓它返回一個(gè)查詢(xún)結(jié)果列表:
public
class
Inventory
{??
???
private
List<Guitar> guitarList;
// 維護(hù)現(xiàn)有的所有吉他
???
public
Inventory() {
??????? guitarList =
new
List
<Guitar>();
??? }
???
// AddGuitar()方法略...
???
// 搜索吉他列表,尋找滿(mǎn)足searchGuitar參數(shù)的吉他
???
public
List<Guitar> Search(Guitar searchGuitar) {
??????? List<Guitar>.
Enumerator
it = guitarList.GetEnumerator();
??????? List<Guitar> list =
new
List
<Guitar>();???
// 保存滿(mǎn)足搜索條件的吉他
???????
while
(it.MoveNext()) {
???????????
Guitar
guitar = it.Current;
???????????
if
(guitar.Builder!=searchGuitar.Builder)
???????????????
continue
;
???????????
string
model = searchGuitar.Model.ToLower();
???????????
if
(!
String
.IsNullOrEmpty(model) &&
??????????????? !model.Equals(guitar.Model.ToLower()))
???????????????
continue
;
???????????
if
(guitar.Type != searchGuitar.Type)
???????????????
continue
;?????????????????????????
???????????
if
(guitar.BackWood != searchGuitar.BackWood)
???????????????
continue
;
???????????
if
(guitar.TopWood != searchGuitar.TopWood)
???????????????
continue
;
??? ??????? list.Add(guitar);?
// 添加到列表中
??????? }
???????
return
list;???
// 返回結(jié)果
??? }
}
然后我們進(jìn)行一下測(cè)試,可以看到它返回了兩個(gè)結(jié)果。
static
void
???
Inventory
inventory =
new
Inventory
();
??? initializeInventory(inventory);
???
// 想要查找的Guitar
???
Guitar
wanted =
new
Guitar
(
"", Builder.Fender, "Stratocastor"
, SoundType.Electric, Wood.Alder, Wood.Alder, 0);
???????????????
???
// 返回符合條件的結(jié)果
??? List<Guitar> list = inventory.Search(wanted);
???
if
(list.Count > 0) {
???????
foreach
(Guitar guitar
in
list) {
???????????
Console
.WriteLine(
"You might like
this
{0} {1} {2} guitar:\n {3} back and sides,\n {4} top.\n You can have it
for
only ${5} !"
, guitar.Builder, guitar.Model, guitar.Type, guitar.BackWood, guitar.TopWood, guitar.Price);
??????? }
??? }
else
{
???????
Console
.WriteLine(
"Sorry, not found."
);
??? }
}
這里仍然需要注意一個(gè)問(wèn)題:上面我們將Guitar的字段類(lèi)型由string改為了枚舉,雖然我們限制了輸入,字段只能接受有限的數(shù)值,但是我們?cè)谡{(diào)用Search()方法時(shí),必須明確的指定一個(gè)枚舉值。而有時(shí)候,我們并不希望指明數(shù)值(我們希望忽略此查詢(xún)條件),比如說(shuō),我們不希望限制吉他的木料(任何木料的吉他都滿(mǎn)足查詢(xún)條件),在使用string類(lèi)型時(shí),我們只需要傳遞null或者空字符串("")進(jìn)去就可以了,但使用枚舉后卻必須指定一個(gè)數(shù)值。此時(shí),可以向枚舉中添加一個(gè)字段,NotSet,這個(gè)值相當(dāng)于string為null或空字符串("")時(shí)的情況。然后將Search()方法中的判斷語(yǔ)句進(jìn)行一下修改就可以了:
if
(searchGuitar.TopWood != Wood.NotSet &&
??? guitar.TopWood != searchGuitar.TopWood)
???
continue
;
2.屬性分離和解耦
屬性分離
我們?cè)賹?duì)上面的程序稍微進(jìn)行一下分析,發(fā)現(xiàn)對(duì)于Guitar來(lái)說(shuō),SerialNumber和Price屬性是一定會(huì)有的,而其他的屬性以后可能會(huì)添加,比如說(shuō)我們可能會(huì)再添加一個(gè)NumStrings屬性,代表吉他有多少根玄;也可能會(huì)刪除某個(gè)屬性,比如我們可能以后會(huì)覺(jué)得model屬性多余,然后把它刪除掉。除此以外,我們發(fā)現(xiàn)Inventory類(lèi)的Search()方法只需要Guitar的部分屬性,而我們傳遞了整個(gè)Guitar進(jìn)去。
此時(shí), 我們可以將不變的部分(SerialNumber和Price)仍保留在Guitar類(lèi)中,將可能會(huì)變化的部分(Guitar類(lèi)的其他屬性),封裝為另一個(gè)類(lèi)型,我們稱(chēng)為GuitarSpec,并在Guitar中保存一個(gè)GuitarSpec類(lèi)型實(shí)例:
public
class
GuitarSpec
{
???
private
Builder
builder;??????
// 廠(chǎng)商
???
private
string
model;?????????
// 型號(hào)
???
private
SoundType
type;???????
// 類(lèi)型
???
private
Wood
backWood;????????
// 后部材質(zhì)
???
private
Wood
topWood;?????????
// 前面材質(zhì)
???
public
GuitarSpec(Builder builder,
string
model, SoundType type, Wood backWood, Wood topWood) {
???????
this
.backWood = backWood;
???????
this
.builder = builder;
???????
this
.model = model;
???????
this
.topWood = topWood;
???????
this
.type = type;
??? }
???
// 屬性略
}
解耦
由于GuitarSpec成為了一個(gè)獨(dú)立的對(duì)象,所以,我們的Guitar類(lèi)型只需要保存一個(gè)GuitarSpec對(duì)象就可以了:
public
class
Guitar
{
???
private
string
serialNumber;??
// 序列號(hào)
???
private
double
price;?????????
// 價(jià)格
???
private
GuitarSpec
spec;??????
// 吉他屬性集
???
// 略...
}
此處有一個(gè)地方值得注意, Guitar的構(gòu)造函數(shù)通常會(huì)有下面兩種寫(xiě)法:
public
Guitar(
string
serialNumber, Builder builder,
string
model, SoundType type, Wood backWood, Wood topWood,
double
price) {
???
this
.price = price;
???
this
.serialNumber = serialNumber;
???
this
.spec =
new
GuitarSpec
(builder, model, type, backWood, topWood);
}
public
Guitar(
string
serialNumber,
double
price, GuitarSpec spec) {
???
this
.price = price;
???
this
.serialNumber = serialNumber;
???
this
.spec = spec;
}
采用第一種寫(xiě)法時(shí),我們?cè)贕uitar類(lèi)的構(gòu)造函數(shù)中創(chuàng)建GuitarSpec類(lèi)型實(shí)例,第二種在Guitar類(lèi)外部先行創(chuàng)建好,然后再傳入。那么采用那種方式好呢?我們回想一下,創(chuàng)建GuitarSpec的目的就是為了將易變化的部分從Guitar類(lèi)中隔離出去,而采用第一種方式時(shí),無(wú)異于再次將變化重新引入Guitar類(lèi),因?yàn)楫?dāng)我們向GuitarSpec類(lèi)添加或刪除屬性時(shí),必須同時(shí)修改Guitar類(lèi)的構(gòu)造函數(shù)!所以,這里我們采用第二種方式的構(gòu)造函數(shù)。
類(lèi)似的我們修改Inventory類(lèi)的AddGuitar()方法和Search()方法:
// 向列表中添加 吉他
public
void
AddGuitar(
string
serialNumber,
double
price, GuitarSpec spec) {
???
Guitar
guitar =
new
Guitar
(serialNumber, price, spec);
??? guitarList.Add(guitar);
}
// 搜索吉他列表,尋找滿(mǎn)足searchSpec參數(shù)的吉他
public
List<Guitar> Search(GuitarSpec searchSpec) {
??? List<Guitar>.
Enumerator
it = guitarList.GetEnumerator();
??? List<Guitar> list =
new
List
<Guitar>();
// 保存滿(mǎn)足搜索條件的吉他
???
while
(it.MoveNext()) {
???????
GuitarSpec
guitarSpec = it.Current.Spec;
???????
if
(guitarSpec.Builder != searchSpec.Builder)
???????????
continue
;
???????
string
model = searchSpec.Model.ToLower();
???????
if
(!
String
.IsNullOrEmpty(model) &&
??????????? !model.Equals(guitarSpec.Model.ToLower()))
???????????
continue
;
???????
if
(guitarSpec.Type != searchSpec.Type)
???????????
continue
;
???????
if
(guitarSpec.BackWood != searchSpec.BackWood)
???????????
continue
;
???????
if
(guitarSpec.TopWood != searchSpec.TopWood)
???????????
continue
;
??????? list.Add(it.Current);?
// 添加到列表中
??? }
???
return
list;???
// 返回結(jié)果
}
現(xiàn)在看上去程序已經(jīng)完善的差不多了,我們上面做得這些都是為了能夠在Guitar的屬性變化的時(shí)候,盡可能的少做修改。檢驗(yàn)程序是否經(jīng)得起變化的一個(gè)方法就是我們現(xiàn)在假設(shè)刪除一個(gè)屬性model,看看需要改變哪些地方:我們得出Guitar類(lèi)是不需要進(jìn)行修改的,GuitarSpec類(lèi)需要?jiǎng)h除model屬性, 然而,我們發(fā)現(xiàn)Inventory類(lèi)也需要進(jìn)行修改,因?yàn)樗腟earch方法依賴(lài)于guitarSpec類(lèi)的Model屬性,因?yàn)橐獙?duì)它進(jìn)行判斷。 此時(shí),我們說(shuō)Inventory類(lèi)與GuitarSpec類(lèi)是耦合在一起的。那么如何才能使得修改GuitarSpec類(lèi)不需要改動(dòng)Inventory類(lèi)呢?我們可以將對(duì)GuitarSpec進(jìn)行判等的操作,委托給GuitarSpec類(lèi)型本身來(lái)完成,我們讓GuitarSpec類(lèi)實(shí)現(xiàn)IEquatable<T>接口:
public
class
GuitarSpec
:IEquatable<GuitarSpec> {
???
// 其余略...????????
???
public
bool
Equals(GuitarSpec other) {
???????
if
(builder != other.Builder)
???????????
return
false
;
???????
string
model = other.Model.ToLower();
???????
if
(!
String
.IsNullOrEmpty(model) &&
??????????? ! model.Equals(
this
.model.ToLower()))
???????????
return
false
;
???????
if
(type != other.Type)
???????????
return
false
;
???????
if
(backWood != other.BackWood)
???????????
return
false
;
???????
if
(topWood != other.TopWood)
???????????
return
false
;
???????
return
true
;
??? }
}
現(xiàn)在判斷兩個(gè)GuitarSpec是否相等的邏輯轉(zhuǎn)移到了GuitarSpec類(lèi)型本身,我們?cè)俅涡薷腎nventory的Search()方法,讓它將對(duì)GuitarSpec的判等操作委托出去。
// 搜索吉他列表,尋找滿(mǎn)足searchSpec參數(shù)的吉他
public
List<Guitar> Search(GuitarSpec searchSpec) {
??? List<Guitar>.
Enumerator
it = guitarList.GetEnumerator();
??? List<Guitar> list =
new
List
<Guitar>();
// 保存滿(mǎn)足搜索條件的吉他
???
while
(it.MoveNext()) {
???????
GuitarSpec
guitarSpec = it.Current.Spec;
???????
if
(guitarSpec.Equals(searchSpec))
// 進(jìn)行兩個(gè)對(duì)象的判等
??????????? list.Add(it.Current);?
// 將結(jié)果添加到列表中
??? }
???
return
list;???
// 返回結(jié)果
}
經(jīng)過(guò)現(xiàn)在的修改之后,不僅Search()方法的實(shí)現(xiàn)變得更為簡(jiǎn)單,各個(gè)類(lèi)的職責(zé)也更加清晰,我們修改GuitarSpec類(lèi)型也不會(huì)影響到Inventory類(lèi)和Guitar類(lèi)。
3.抽象和繼承
接下來(lái)我們來(lái)對(duì)上面的程序進(jìn)行一下擴(kuò)展,假如我們的程序不僅需要對(duì)吉他(Guitar)進(jìn)行管理和維護(hù),還需要對(duì)曼陀林(Mandolin,一種琵琶樂(lè)器)進(jìn)行管理,它的屬性與吉他是類(lèi)似的,但是多了一個(gè)Style屬性,有"A"和"F"兩種取值;同時(shí)我們?yōu)榧偌尤胍粋€(gè)NumStrings屬性,代表玹的數(shù)量,那么該如何改進(jìn)程序呢?
首先我們創(chuàng)建一個(gè)Style枚舉,它只包含A、F兩個(gè)枚舉值。接下來(lái),我們可以將Guitar類(lèi)和Mandolin的公共部分抽象出來(lái),建立一個(gè)Instrument基類(lèi),這個(gè)Instrument基類(lèi)包含Guitar和Mandolin公有的部分,然后讓Guitar和Mandolin繼承自Instrument。 因?yàn)槲覀儗?shí)際上并不需要?jiǎng)?chuàng)建一個(gè)Instrument的實(shí)例,所以我們將它聲明為抽象的。 類(lèi)似的,我們將GuitarSpec也抽象為InstrumentSpec,并且再為Mandolin創(chuàng)建一個(gè)MandolinSpec類(lèi),讓GuitarSpec和MandolinSpec繼承自InstrumentSpec:
// Instrument樂(lè)器基類(lèi)
public
abstract
class
Instrument
{
???
private
string
serialNumber;??
// 序列號(hào)
???
private
double
price;?????????
// 價(jià)格
???
private
InstrumentSpec
spec;??
// 樂(lè)器屬性集
???
// 構(gòu)造函數(shù)和屬性略
}
// 吉他類(lèi)
public
class
Guitar
:
Instrument
{
??
public
Guitar(
string
serialNumber,
double
price, GuitarSpec spec):
base
(serialNumber, price, spec) {
??? }
}
// 曼陀林類(lèi)
public
class
Mandolin
:
Instrument
{
???
public
Mandolin(
string
serialNumber,
double
price, MandolinSpec spec)
??????? :
base
(serialNumber, price, spec) {
??? }
}
以及InstrumentSpec和GuitarSpec、MandolinSpec類(lèi):
public
abstract
class
InstrumentSpec
: IEquatable<InstrumentSpec> {
???
private
Builder
builder;??????
// 廠(chǎng)商
???
private
string
model;?????????
// 型號(hào)
???
private
SoundType
type;???????
// 類(lèi)型
???
private
Wood
backWood;????????
// 后部材質(zhì)
???
private
Wood
topWood;?????????
// 前面材質(zhì)
???
// 構(gòu)造函數(shù)和屬性略
???
public
bool
Equals(InstrumentSpec other) {
???????
string
model = other.Model.ToLower();
???????
if
(!
String
.IsNullOrEmpty(model) &&
??????????? ! model.Equals(
this
.model.ToLower()))
???????????
return
false
;
???????
if
(builder != other.Builder)
???????????
return
false
;
???????
if
(type != other.Type)
???????????
return
false
;
???????
if
(backWood != other.BackWood)
???????????
return
false
;
???????
if
(topWood != other.TopWood)
???????????
return
false
;
???????
return
true
;
??? }
}
public
class
GuitarSpec
:InstrumentSpec, IEquatable<GuitarSpec> {
???
private
int
numStrings;
???
public
GuitarSpec(Builder builder,
string
model, SoundType type, Wood backWood, Wood topWood,
int
numStrings)
??????? :
base
(builder, model,type,backWood, topWood) {
???????
this
.numStrings = numStrings;
??? }
???
public
int
NumStrings{
??????? get {
return
numStrings; }
??? }
???
public
bool
Equals(GuitarSpec other) {
???????
if
(!
base
.Equals(other))
???????????
return
false
;
???????
if
(numStrings != other.NumStrings)
???????????
return
false
;
???????
return
true
;
??? }
}
public
class
MandolinSpec
: InstrumentSpec, IEquatable<MandolinSpec> {
???
private
Style
style;
???
public
MandolinSpec(Builder builder,
string
model, SoundType type, Wood backWood, Wood topWood, Style style)
??????? :
base
(builder, model, type, backWood, topWood) {
???????
this
.style = style;
??? }
???
public
bool
Equals(MandolinSpec other) {
???????
if
(!
base
.Equals(other))
???????????
return
false
;
???????
if
(style != other.style)
???????????
return
false
;
???????
return
true
;
??? }
}
最后,我們需要修改Inventory類(lèi):
public
class
Inventory
{
???
private
List<Instrument> instrumentList;
// 維護(hù)現(xiàn)有的所有樂(lè)器
???
public
Inventory() {
??????? instrumentList =
new
List
<Instrument>();
??? }
???
// Search() 和 AddInstrument()方法見(jiàn)下
}
我們通過(guò)抽象和繼承完成了程序的擴(kuò)展?,F(xiàn)在來(lái)看一下上面實(shí)現(xiàn)的擴(kuò)展性如何,為了更簡(jiǎn)單地對(duì)問(wèn)題進(jìn)行描述,我們?cè)O(shè)想如果再加入一種樂(lè)器,班卓琴(Banjo),程序需要做哪些改動(dòng)?
1、我們需要再定義一個(gè)繼承自Instrument的類(lèi)Banjo;
2、以及一個(gè)繼承自InstrumentSpec的類(lèi)BanjoSpec;此時(shí),如果BanjoSpec擁有InstrumentSpec沒(méi)有定義的屬性,那么很好辦,我們?cè)贐anjoSpec中添加新增的屬性即可;如果BanjoSepc不需要InstrumentSpec中定義的屬性,比如說(shuō)Model,那么就麻煩了,我們需要從InstrumentSpec中刪掉此屬性,然后再在InstrumentSpec除了BanjoSpec以外的所有子類(lèi)中添加剛才刪去的Model屬性。
3、我們還需要修改Inventory的AddInstrument()方法:
// 向列表中添加 樂(lè)器
public
void
AddInstrument(
string
serialNumber,
double
price, InstrumentSpec spec) {
???
Instrument
instrument =
null
;
???
if
(spec
is
GuitarSpec){
??????? instrument =
new
Guitar
(serialNumber, price, (GuitarSpec)spec);
??? }
else
if
(spec
is
MandolinSpec) {
??????? instrument =
new
Mandolin
(serialNumber, price, (MandolinSpec)spec);
??? }
??? instrumentList.Add(instrument);
}
這里,因?yàn)镮nstrument是抽象類(lèi),所以我們無(wú)法創(chuàng)建Instrument的實(shí)例,只能創(chuàng)建其子類(lèi)的實(shí)例,而Guitar和Mandolin的構(gòu)造函數(shù),分別需要InstrumentSpec的子類(lèi)(GuitarSpec和MandolinSpec),所以我們需要先進(jìn)行向下轉(zhuǎn)換((GuitarSpec)spec),才能創(chuàng)建對(duì)象。
4、類(lèi)似地,我們也需要修改Search()方法:
// 搜索列表,尋找滿(mǎn)足SearchSpec參數(shù)的樂(lè)器
public
List<Instrument> Search(InstrumentSpec searchSpec) {
??? List<Instrument>.
Enumerator
it = instrumentList.GetEnumerator();
??? List<Instrument> list =
new
List
<Instrument>();
???
MandolinSpec
mandolinSpec;
???
GuitarSpec
guitarSpec;
???
while
(it.MoveNext()) {
???????
if
(it.Current
is
Guitar && searchSpec
is
GuitarSpec) {
??????????? guitarSpec = (GuitarSpec)it.Current.Spec;
???????????
if
(guitarSpec.Equals((GuitarSpec)searchSpec))
??????????????? list.Add(it.Current);
??????? }
else
if
(it.Current
is
Mandolin && searchSpec
is
MandolinSpec) {
??????????? mandolinSpec = (MandolinSpec)it.Current.Spec;
???????????
if
(mandolinSpec.Equals((MandolinSpec)searchSpec))
??????????????? list.Add(it.Current);
??????? }
??? }
???
return
list;
}
我們看到,盡管只是添加一種樂(lè)器,不僅需要對(duì)多處進(jìn)行修改,而且還要再添加兩個(gè)新類(lèi)Banjo和BanjoSpec。設(shè)想如果有10多種樂(lè)器,那么改動(dòng)及類(lèi)的數(shù)量都會(huì)是非常多的,維護(hù)起來(lái)也會(huì)像是噩夢(mèng)一般。那么下來(lái)該再如何改進(jìn)呢?我們接著往下看。
4.動(dòng)態(tài)屬性
首先我們看一下Guitar、Mandolin和Banjo類(lèi),它們除了構(gòu)造函數(shù)不同以外其余完全相同。而一般情況下,我們定義一個(gè)抽象類(lèi)和子類(lèi)這種繼承體系,目的是 為了在基類(lèi)中實(shí)現(xiàn)一種行為,然后在各個(gè)子類(lèi)中對(duì)其進(jìn)行重寫(xiě),以實(shí)現(xiàn)多態(tài)的效果。 所以,此處我們可以考慮另外一種方式,將Instrument聲明為實(shí)例的,并且在其中加入一個(gè)枚舉類(lèi)型的屬性InstrumentType,由這個(gè)屬性來(lái)標(biāo)識(shí)樂(lè)器的類(lèi)別。以后我們需要添加新的類(lèi)型,只需要在這個(gè)枚舉中添加就可以了:
// 樂(lè)器類(lèi)型
public
enum
InstrumentType
{
??? Guitar = 0, Mandolin, Banjo
}
因?yàn)镮nstrumentType和SerialNumber、Price一樣,屬于每種樂(lè)器都有的屬性,所以我們將它定義在Instrument類(lèi)中,而非InstrumentSpec中,此時(shí)Instument我們也聲明為一般類(lèi),而非抽象類(lèi):
public
class
Instrument
{
???
private
InstrumentType
type;??
// 樂(lè)器類(lèi)型
???
// 其余略...
}
對(duì)于InstrumentSpec類(lèi)及其子類(lèi)而言,由于屬性是多變的,而基類(lèi)并沒(méi)有定義抽象或者虛擬方法供子類(lèi)覆蓋,所以我們可以使用一個(gè)Hashtable將樂(lè)器的屬性值按照 key/value 的形式保存起來(lái),其中 key是屬性名稱(chēng),value是屬性值。這樣就可以刪去所有的InstrumentSpec的子類(lèi)(GuitarSpec、MandolinSepc等),同時(shí),我們將InstrumentSpec聲明為一般類(lèi):
public
class
InstrumentSpec
: IEquatable<InstrumentSpec> {
???
private
Hashtable
properties;
???
public
InstrumentSpec(Hashtable properties) {
???????
if
(properties ==
null
)
??????????? properties =
new
Hashtable
();
???????
else
???????????
this
.properties = properties;
??? }
???
public
Hashtable Properties {
??????? get {
return
properties; }
??? }
???
public
Object GetProperty(
object
propertyName) {
???????
return
properties[propertyName];
??? }
???
public
bool
Equals(InstrumentSpec other) {
???????
IEnumerator
it = other.properties.Keys.GetEnumerator();
???????
while
(it.MoveNext()) {
???????????
if
(properties[it.Current] != other.properties[it.Current])
???????????????
return
false
;
??????? }
???????
return
true
;
??? }
}
通過(guò)上面的改變,我們添加新樂(lè)器時(shí),只需要改變枚舉就可以了,而不需要再添加大量的諸如Guitar和GuitarSpec這樣的子類(lèi)。
最后我們?cè)倏匆幌翴nventory類(lèi)的實(shí)現(xiàn):
public
class
Inventory
{
???
private
List<Instrument> instrumentList;
// 維護(hù)現(xiàn)有的所有樂(lè)器
???
public
Inventory() {
??????? instrumentList =
new
List
<Instrument>();
??? }
???
// 向列表中添加 樂(lè)器
???
public
void
AddInstrument(
string
serialNumber,
double
price, InstrumentSpec spec) {
???????
Instrument
instrument =
new
Instrument
(serialNumber, price, spec);
??????? instrumentList.Add(instrument);
??? }
???
// 搜索列表,尋找滿(mǎn)足SearchSpec參數(shù)的樂(lè)器
???
public
List<Instrument> Search(InstrumentSpec searchSpec) {
??????? List<Instrument>.
Enumerator
it = instrumentList.GetEnumerator();
??????? List<Instrument> list =
new
List
<Instrument>();
???????
while
(it.MoveNext()) {
???????????
if
(it.Current.Spec.Equals(searchSpec))
??????????????? list.Add(it.Current);
??????? }
???????
return
list;
??? }
}
可以看到Inventory類(lèi)也變得清爽了許多。那么采用這種方式是不是就最好了呢?我們?nèi)匀灰吹剿膯?wèn)題:
- 盡管將屬性和屬性值保存在Hashtable中極大的增加了靈活性,但是我們每次構(gòu)建對(duì)象,為對(duì)象添加屬性值也會(huì)變得非常繁瑣。
- Hashtable返回的是一個(gè)Object類(lèi)型的對(duì)象,所以我們?cè)讷@得到屬性之后,還需要再進(jìn)行一次向下轉(zhuǎn)換才行。
- 同樣,因?yàn)镠ashtable可以接收任何類(lèi)型的對(duì)象,所以我們也就喪失了類(lèi)型安全,比如說(shuō),對(duì)于一個(gè)只可以接受int類(lèi)型的屬性,我們可以輸入任意值而在編譯時(shí)不會(huì)報(bào)錯(cuò),只有在運(yùn)行時(shí),我們將值取出進(jìn)行向下轉(zhuǎn)化時(shí)才會(huì)拋出異常。
所以說(shuō),設(shè)計(jì)并沒(méi)有最好,只有最合適的,本文討論的也是一樣,我們只能根據(jù)實(shí)際情況,選擇最合適的解決方案。對(duì)于 只有一種樂(lè)器、支持多種樂(lè)器、樂(lè)器屬性變化不大、屬性變化很大等各種不同情況,我們需要做出權(quán)衡,選擇合適的解決方案。另外在實(shí)現(xiàn)時(shí)還要做出一定的預(yù)見(jiàn),考慮以后某方面的變更會(huì)不會(huì)很大,然后再考慮需不需要留出擴(kuò)展的余地。對(duì)于一個(gè)系統(tǒng),我們很可能 設(shè)計(jì)不足 ,也有可能 過(guò)度設(shè)計(jì) 。我覺(jué)得,我們應(yīng)該首先具備了 過(guò)度設(shè)計(jì) 的能力,然后再去考慮哪些地方不需要過(guò)度“靈活”,因?yàn)橥ǔC糠N設(shè)計(jì)都有著自身的優(yōu)點(diǎn)和缺陷,很難找到一種絕對(duì)正確的方案
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
