http://www.tracefact.net/Software-Design/A-Sample-Design.aspx
本文是《Object-Oriented Analysis and Design》一書第一章和第五章的讀書筆記。我對書中提供的一個范例程序進行了總結和整理,通過逐步優化這個樂器管理的范例程序,分析了進行程序設計時需要注意到的一些問題。
1.簡單直接的實現
這個程序起初的需求很簡單: 我們需要創建一個吉他管理程序,它能夠保存所有的吉他信息,并且可以通過輸入吉他的參數來進行查詢,返回查詢結果。 我們知道一個優良的軟件應該從兩個角度去衡量:
- 從用戶的角度,軟件應該是符合用戶期望的,也就是滿足了用戶的需求,可以完成用戶期望它完成的工作。
- 從開發者的角度,軟件應該是易于維護的、可擴展的,以及可重用的。
這兩個方面應該是遞進的,也就是說軟件首先要能滿足用戶的需求。所以我們先看如何完成用戶的需求,我們定義一個類Guitar,它代表了吉他;以及一個Inventory類,它用于維護現有吉他的信息,可以進行添加、查找等操作:
然后我們來看下實現:
// 吉他類
public
class
Guitar
{
???
private
string
serialNumber;??
// 序列號
???
private
string
builder;???????
// 廠商
???
private
string
model;?????????
// 型號
???
private
string
type;??????????
// 類型
???
private
string
backWood;??????
// 后部材質
???
private
string
topWood;???????
// 前面材質
???
private
double
price;?????????
// 價格
???
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
{
???
// 維護現有的所有吉他
???
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);
??? }
???
// 搜索吉他列表,尋找滿足searchGuitar參數的吉他
???
// 如果searchGuitar的參數為null或者"",則忽略此參數
???
public
Guitar Search(Guitar searchGuitar) {
??????? List<Guitar>.
Enumerator
it =? guitarList.GetEnumerator();
???????
// 價格Price和序列號SerialNumber不參與查詢
???????
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;??
// 找到第一個匹配結果就返回
???????????
return
result;
??????? }
???????
return
result;
??? }
}
接下來我們向Inventory中添加一些Guitar,然后來測試下查找功能:
class
Program
{
???
static
void
???????
Inventory
inventory =
new
Inventory
();
??????? initializeInventory(inventory);
???????
// 想要查找的Guitar
???????
Guitar
wanted =
new
Guitar
(
"", "fender", "Stratocastor", "electric", "Alder", "Alder"
, 0);
???????
// 返回符合條件的結果
???????
Guitar
guitar = inventory.Search(wanted);
???????
if
(guitar !=
null
) {
// 找到符合條件的結果
???????????
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);
??? }
}
結果我們發現并未返回搜索結果,但是我們看下InitializeInventory()方法,確實存在一個Guitar,它的屬性完全符合要查找的Guitar實例wanted。為什么查詢卻找不到呢?仔細查看一下,我們發現添加到Inventory中的Guitar的制造商Builder是"Fender",而輸入的searchGuitar的Builder屬性為"fender"。我們知道,在C#中字符串的大小寫是敏感的,即是說 "a"=="A"返回的是false,所以"Fender"不等于"fender"。所以我們遇到的問題是:在不要求嚴格匹配大小寫的情況下,對于字符串的比較,我們應該先全部轉換為大寫或者小寫,然后再進行比較。但是這樣做就沒有問題了么?我們看一下Guitar類的定義,除了price為double類型以外,其余均為string。而某些屬性,比如說吉他的發聲類型type,只有兩種可能,一種是傳統的、通過震動發聲的(Acoustic),一種是電子發聲的(Electric);對于制造商Builder,可能只有有限的幾個廠家。但使用string類型時,我們無法對于這些屬性的取值進行限制,此時,我們應該考慮: 如果對象的屬性是由有限個項目構成的集合,我們最好定義一個枚舉,并設置對象的屬性為這個枚舉類型。
所以對于上面程序可以進行的第一個改進,就是定義枚舉,并將部分屬性的值,由string改為枚舉類型:
// 發生類型
public
enum
SoundType
{
??? Acoustic, Electric
}
// 制造商
public
enum
Builder
{
??? Fender, Martin, Gibson, Collings, Olson
}
// 木料
public
enum
Wood
{
??? IndianRoseWood, BrazilianRoseWood, Mahogany, Maple, Cocobolo, Cedar, Alder, Sitka
}
同時修改Guitar類和Inventory類,讓它們使用這些枚舉作為字段類型:
public
class
Guitar
{
???
private
string
serialNumber;??
// 序列號
???
private
Builder
builder;??????
// 廠商
???
private
string
model;?????????
// 型號
???
private
SoundType
type;???????
// 類型
???
private
Wood
backWood;????????
// 后部材質
???
private
Wood
topWood;?????????
// 前面材質
???
private
double
price;?????????
// 價格
???
???
// 構造函數、屬性做相應修改,此處略
}
此時,我們發現上面例子Inventory中符合搜索條件的有兩項,而Search()方法只能返回查詢到的第一個結果,所以第二處改進就是對Inventory的Search()方法進行修改,讓它返回一個查詢結果列表:
public
class
Inventory
{??
???
private
List<Guitar> guitarList;
// 維護現有的所有吉他
???
public
Inventory() {
??????? guitarList =
new
List
<Guitar>();
??? }
???
// AddGuitar()方法略...
???
// 搜索吉他列表,尋找滿足searchGuitar參數的吉他
???
public
List<Guitar> Search(Guitar searchGuitar) {
??????? List<Guitar>.
Enumerator
it = guitarList.GetEnumerator();
??????? List<Guitar> list =
new
List
<Guitar>();???
// 保存滿足搜索條件的吉他
???????
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;???
// 返回結果
??? }
}
然后我們進行一下測試,可以看到它返回了兩個結果。
static
void
???
Inventory
inventory =
new
Inventory
();
??? initializeInventory(inventory);
???
// 想要查找的Guitar
???
Guitar
wanted =
new
Guitar
(
"", Builder.Fender, "Stratocastor"
, SoundType.Electric, Wood.Alder, Wood.Alder, 0);
???????????????
???
// 返回符合條件的結果
??? 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."
);
??? }
}
這里仍然需要注意一個問題:上面我們將Guitar的字段類型由string改為了枚舉,雖然我們限制了輸入,字段只能接受有限的數值,但是我們在調用Search()方法時,必須明確的指定一個枚舉值。而有時候,我們并不希望指明數值(我們希望忽略此查詢條件),比如說,我們不希望限制吉他的木料(任何木料的吉他都滿足查詢條件),在使用string類型時,我們只需要傳遞null或者空字符串("")進去就可以了,但使用枚舉后卻必須指定一個數值。此時,可以向枚舉中添加一個字段,NotSet,這個值相當于string為null或空字符串("")時的情況。然后將Search()方法中的判斷語句進行一下修改就可以了:
if
(searchGuitar.TopWood != Wood.NotSet &&
??? guitar.TopWood != searchGuitar.TopWood)
???
continue
;
2.屬性分離和解耦
屬性分離
我們再對上面的程序稍微進行一下分析,發現對于Guitar來說,SerialNumber和Price屬性是一定會有的,而其他的屬性以后可能會添加,比如說我們可能會再添加一個NumStrings屬性,代表吉他有多少根玄;也可能會刪除某個屬性,比如我們可能以后會覺得model屬性多余,然后把它刪除掉。除此以外,我們發現Inventory類的Search()方法只需要Guitar的部分屬性,而我們傳遞了整個Guitar進去。
此時, 我們可以將不變的部分(SerialNumber和Price)仍保留在Guitar類中,將可能會變化的部分(Guitar類的其他屬性),封裝為另一個類型,我們稱為GuitarSpec,并在Guitar中保存一個GuitarSpec類型實例:
public
class
GuitarSpec
{
???
private
Builder
builder;??????
// 廠商
???
private
string
model;?????????
// 型號
???
private
SoundType
type;???????
// 類型
???
private
Wood
backWood;????????
// 后部材質
???
private
Wood
topWood;?????????
// 前面材質
???
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成為了一個獨立的對象,所以,我們的Guitar類型只需要保存一個GuitarSpec對象就可以了:
public
class
Guitar
{
???
private
string
serialNumber;??
// 序列號
???
private
double
price;?????????
// 價格
???
private
GuitarSpec
spec;??????
// 吉他屬性集
???
// 略...
}
此處有一個地方值得注意, Guitar的構造函數通常會有下面兩種寫法:
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;
}
采用第一種寫法時,我們在Guitar類的構造函數中創建GuitarSpec類型實例,第二種在Guitar類外部先行創建好,然后再傳入。那么采用那種方式好呢?我們回想一下,創建GuitarSpec的目的就是為了將易變化的部分從Guitar類中隔離出去,而采用第一種方式時,無異于再次將變化重新引入Guitar類,因為當我們向GuitarSpec類添加或刪除屬性時,必須同時修改Guitar類的構造函數!所以,這里我們采用第二種方式的構造函數。
類似的我們修改Inventory類的AddGuitar()方法和Search()方法:
// 向列表中添加 吉他
public
void
AddGuitar(
string
serialNumber,
double
price, GuitarSpec spec) {
???
Guitar
guitar =
new
Guitar
(serialNumber, price, spec);
??? guitarList.Add(guitar);
}
// 搜索吉他列表,尋找滿足searchSpec參數的吉他
public
List<Guitar> Search(GuitarSpec searchSpec) {
??? List<Guitar>.
Enumerator
it = guitarList.GetEnumerator();
??? List<Guitar> list =
new
List
<Guitar>();
// 保存滿足搜索條件的吉他
???
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;???
// 返回結果
}
現在看上去程序已經完善的差不多了,我們上面做得這些都是為了能夠在Guitar的屬性變化的時候,盡可能的少做修改。檢驗程序是否經得起變化的一個方法就是我們現在假設刪除一個屬性model,看看需要改變哪些地方:我們得出Guitar類是不需要進行修改的,GuitarSpec類需要刪除model屬性, 然而,我們發現Inventory類也需要進行修改,因為它的Search方法依賴于guitarSpec類的Model屬性,因為要對它進行判斷。 此時,我們說Inventory類與GuitarSpec類是耦合在一起的。那么如何才能使得修改GuitarSpec類不需要改動Inventory類呢?我們可以將對GuitarSpec進行判等的操作,委托給GuitarSpec類型本身來完成,我們讓GuitarSpec類實現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
;
??? }
}
現在判斷兩個GuitarSpec是否相等的邏輯轉移到了GuitarSpec類型本身,我們再次修改Inventory的Search()方法,讓它將對GuitarSpec的判等操作委托出去。
// 搜索吉他列表,尋找滿足searchSpec參數的吉他
public
List<Guitar> Search(GuitarSpec searchSpec) {
??? List<Guitar>.
Enumerator
it = guitarList.GetEnumerator();
??? List<Guitar> list =
new
List
<Guitar>();
// 保存滿足搜索條件的吉他
???
while
(it.MoveNext()) {
???????
GuitarSpec
guitarSpec = it.Current.Spec;
???????
if
(guitarSpec.Equals(searchSpec))
// 進行兩個對象的判等
??????????? list.Add(it.Current);?
// 將結果添加到列表中
??? }
???
return
list;???
// 返回結果
}
經過現在的修改之后,不僅Search()方法的實現變得更為簡單,各個類的職責也更加清晰,我們修改GuitarSpec類型也不會影響到Inventory類和Guitar類。
3.抽象和繼承
接下來我們來對上面的程序進行一下擴展,假如我們的程序不僅需要對吉他(Guitar)進行管理和維護,還需要對曼陀林(Mandolin,一種琵琶樂器)進行管理,它的屬性與吉他是類似的,但是多了一個Style屬性,有"A"和"F"兩種取值;同時我們為吉他再加入一個NumStrings屬性,代表玹的數量,那么該如何改進程序呢?
首先我們創建一個Style枚舉,它只包含A、F兩個枚舉值。接下來,我們可以將Guitar類和Mandolin的公共部分抽象出來,建立一個Instrument基類,這個Instrument基類包含Guitar和Mandolin公有的部分,然后讓Guitar和Mandolin繼承自Instrument。 因為我們實際上并不需要創建一個Instrument的實例,所以我們將它聲明為抽象的。 類似的,我們將GuitarSpec也抽象為InstrumentSpec,并且再為Mandolin創建一個MandolinSpec類,讓GuitarSpec和MandolinSpec繼承自InstrumentSpec:
// Instrument樂器基類
public
abstract
class
Instrument
{
???
private
string
serialNumber;??
// 序列號
???
private
double
price;?????????
// 價格
???
private
InstrumentSpec
spec;??
// 樂器屬性集
???
// 構造函數和屬性略
}
// 吉他類
public
class
Guitar
:
Instrument
{
??
public
Guitar(
string
serialNumber,
double
price, GuitarSpec spec):
base
(serialNumber, price, spec) {
??? }
}
// 曼陀林類
public
class
Mandolin
:
Instrument
{
???
public
Mandolin(
string
serialNumber,
double
price, MandolinSpec spec)
??????? :
base
(serialNumber, price, spec) {
??? }
}
以及InstrumentSpec和GuitarSpec、MandolinSpec類:
public
abstract
class
InstrumentSpec
: IEquatable<InstrumentSpec> {
???
private
Builder
builder;??????
// 廠商
???
private
string
model;?????????
// 型號
???
private
SoundType
type;???????
// 類型
???
private
Wood
backWood;????????
// 后部材質
???
private
Wood
topWood;?????????
// 前面材質
???
// 構造函數和屬性略
???
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類:
public
class
Inventory
{
???
private
List<Instrument> instrumentList;
// 維護現有的所有樂器
???
public
Inventory() {
??????? instrumentList =
new
List
<Instrument>();
??? }
???
// Search() 和 AddInstrument()方法見下
}
我們通過抽象和繼承完成了程序的擴展。現在來看一下上面實現的擴展性如何,為了更簡單地對問題進行描述,我們設想如果再加入一種樂器,班卓琴(Banjo),程序需要做哪些改動?
1、我們需要再定義一個繼承自Instrument的類Banjo;
2、以及一個繼承自InstrumentSpec的類BanjoSpec;此時,如果BanjoSpec擁有InstrumentSpec沒有定義的屬性,那么很好辦,我們在BanjoSpec中添加新增的屬性即可;如果BanjoSepc不需要InstrumentSpec中定義的屬性,比如說Model,那么就麻煩了,我們需要從InstrumentSpec中刪掉此屬性,然后再在InstrumentSpec除了BanjoSpec以外的所有子類中添加剛才刪去的Model屬性。
3、我們還需要修改Inventory的AddInstrument()方法:
// 向列表中添加 樂器
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);
}
這里,因為Instrument是抽象類,所以我們無法創建Instrument的實例,只能創建其子類的實例,而Guitar和Mandolin的構造函數,分別需要InstrumentSpec的子類(GuitarSpec和MandolinSpec),所以我們需要先進行向下轉換((GuitarSpec)spec),才能創建對象。
4、類似地,我們也需要修改Search()方法:
// 搜索列表,尋找滿足SearchSpec參數的樂器
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;
}
我們看到,盡管只是添加一種樂器,不僅需要對多處進行修改,而且還要再添加兩個新類Banjo和BanjoSpec。設想如果有10多種樂器,那么改動及類的數量都會是非常多的,維護起來也會像是噩夢一般。那么下來該再如何改進呢?我們接著往下看。
4.動態屬性
首先我們看一下Guitar、Mandolin和Banjo類,它們除了構造函數不同以外其余完全相同。而一般情況下,我們定義一個抽象類和子類這種繼承體系,目的是 為了在基類中實現一種行為,然后在各個子類中對其進行重寫,以實現多態的效果。 所以,此處我們可以考慮另外一種方式,將Instrument聲明為實例的,并且在其中加入一個枚舉類型的屬性InstrumentType,由這個屬性來標識樂器的類別。以后我們需要添加新的類型,只需要在這個枚舉中添加就可以了:
// 樂器類型
public
enum
InstrumentType
{
??? Guitar = 0, Mandolin, Banjo
}
因為InstrumentType和SerialNumber、Price一樣,屬于每種樂器都有的屬性,所以我們將它定義在Instrument類中,而非InstrumentSpec中,此時Instument我們也聲明為一般類,而非抽象類:
public
class
Instrument
{
???
private
InstrumentType
type;??
// 樂器類型
???
// 其余略...
}
對于InstrumentSpec類及其子類而言,由于屬性是多變的,而基類并沒有定義抽象或者虛擬方法供子類覆蓋,所以我們可以使用一個Hashtable將樂器的屬性值按照 key/value 的形式保存起來,其中 key是屬性名稱,value是屬性值。這樣就可以刪去所有的InstrumentSpec的子類(GuitarSpec、MandolinSepc等),同時,我們將InstrumentSpec聲明為一般類:
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
;
??? }
}
通過上面的改變,我們添加新樂器時,只需要改變枚舉就可以了,而不需要再添加大量的諸如Guitar和GuitarSpec這樣的子類。
最后我們再看一下Inventory類的實現:
public
class
Inventory
{
???
private
List<Instrument> instrumentList;
// 維護現有的所有樂器
???
public
Inventory() {
??????? instrumentList =
new
List
<Instrument>();
??? }
???
// 向列表中添加 樂器
???
public
void
AddInstrument(
string
serialNumber,
double
price, InstrumentSpec spec) {
???????
Instrument
instrument =
new
Instrument
(serialNumber, price, spec);
??????? instrumentList.Add(instrument);
??? }
???
// 搜索列表,尋找滿足SearchSpec參數的樂器
???
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類也變得清爽了許多。那么采用這種方式是不是就最好了呢?我們仍然要看到它的問題:
- 盡管將屬性和屬性值保存在Hashtable中極大的增加了靈活性,但是我們每次構建對象,為對象添加屬性值也會變得非常繁瑣。
- Hashtable返回的是一個Object類型的對象,所以我們在獲得到屬性之后,還需要再進行一次向下轉換才行。
- 同樣,因為Hashtable可以接收任何類型的對象,所以我們也就喪失了類型安全,比如說,對于一個只可以接受int類型的屬性,我們可以輸入任意值而在編譯時不會報錯,只有在運行時,我們將值取出進行向下轉化時才會拋出異常。
所以說,設計并沒有最好,只有最合適的,本文討論的也是一樣,我們只能根據實際情況,選擇最合適的解決方案。對于 只有一種樂器、支持多種樂器、樂器屬性變化不大、屬性變化很大等各種不同情況,我們需要做出權衡,選擇合適的解決方案。另外在實現時還要做出一定的預見,考慮以后某方面的變更會不會很大,然后再考慮需不需要留出擴展的余地。對于一個系統,我們很可能 設計不足 ,也有可能 過度設計 。我覺得,我們應該首先具備了 過度設計 的能力,然后再去考慮哪些地方不需要過度“靈活”,因為通常每種設計都有著自身的優點和缺陷,很難找到一種絕對正確的方案
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

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