面向对象设计(OOD)思想——还是以播放器为例(ZT) iSlVe~ef
m=V2xoMw6
有了思想才能飞翔,缺乏灵活就象少了轮子的汽车,难以飞奔。为了更好的理解设计思想,结合一个尽可能简洁的实例来说明OOD、设计模式及重构。通过下面的代码,详细地阐述面向对象设计思想。 # 95/,k
一、传统过程化设计思想 a mgex$
假定我们要设计一个媒体播放器(只从软件设计的角度,不涉及硬件)。该媒体播放器目前只支持音频文件mp3和wav。按照结构化设计思想,设计出来的播放器的代码如下: S:s^si2/
public class MediaPlayer g*M3;G
{ n0q(EQy1U
private void PlayMp3() zu%pr95U
{ C@i g3fhV
MessageBox.Show("Play the mp3 file."); ppjrm
} eSU8/9B
`( Gk_VAa
private void PlayWav() {r)M@@[
{ sx\7Z#|
MessageBox.Show("Play the wav file."); i^l;PvIF
} <n{9pZ5.
=fPO0Ot;
public void Play(string audioType) i1C'
{ w5Xdq_e3
switch (audioType.ToLower()) ):@B1 yR
{ RI=B(0A
case ("mp3"): ZHJzh\?
PlayMp3(); FCPbp!q6
break; kn.z8%^(
case ("wav"):
jG#sVK]
PlayWav(); 4dP_'0]9A:
break; _G|6xlO
} {PKER$C
} q8J/tw?%v
} %O${EN
从传统的过程化设计思想来看,这是一段既实用又简洁的代码。 @[Th{HTc.G
如果,客户又提出新的要求:要播放器不仅仅播放mp3和wav文件,还要播放其他音频文件如wma、mp4等,为此我们要不断地增加相应地播放方法和修改条件语句,直止条件语句足够长。 `g~-5Z~J
如果,客户感到这个媒体播放器功能太少了,只能闻其声,不能见其人,太单一。如果在听着优美音乐的同时又能看到歌唱者潇洒、英俊的舞姿那就更好了。从代码设计的角度看,他们希望媒体播放器支持视频文件了。也许你会想,不会再增加视频这方面的代码,可以,在增加视频媒体的播放方法,在修改条件判断语句,如果还有其他,还可以同样地增加、修改。到此你也许会提出,要是不修改或很少修改原来的代码就能增添其他功能该多好啊! jI%yi-<;
这样看,原来的软件设计结构似乎有点问题。事实上,随着功能的不断增加,你越来越发现这个设计非常的糟糕,因为它根本没有为未来的需求变更提供最起码的扩展。为了应接不暇的变更需求,你不得不不厌其烦地修改原来的代码,使其适应需求变化,甚至在修改代码时,由于过多的代码依赖关系弄得人焦头烂额,直止一塌糊涂。 N.?Wev{
二、面向对象设计思想 uu>g(q?4II
还是以设计一个媒体播放器为例,设计要求相同。不访我们换个设计思路利用面向对象设计思想(OOD)来做做看如何! Sy_M!`B
根据OOD的思想,我们应该把mp3和wav分别看作是两个独立的对象。代码设计如下: s5HbuyR^
public class MP3 q,GL#L
{ 92i#It}-/
public void Play() u(R`}C?P'
{ @h]H_
MessageBox.Show("Play the mp3 file."); ]rS+v^@QH
} z,tax`O
} VWi-)
2-4%h!
public class WAV 0/Csc\Xl
{ "Xqj%\
public void Play() dj=n1f+;[
{ ~VKw%WK
MessageBox.Show("Play the wav file."); Jj!T7f*-GX
} o,-@vp
} -3fvO~
Ud](hp"
Public class MediaPlayer epnDvz\
{ qv:WC
TAn
switch (audioType.ToLower()) ^6`U0|5mRX
{ <
5ow81
case ("mp3"): i=ba=-"Mt
MP3 m = new MP3(); t`?FSV
m.Play(); wv<"W@& 9
break; (.c?)_G,
case ("wav"): G`pI{_-e
WAV w = new WAV(); k`- L5#`
w.Play(); <1y%ch;
break; 7G/"!ePW6`
} NS1[-ng
} W `Soa&9
现在我们重构代码,建立统一的Play()方法,(在后面的设计中,你会发现这样改名是多么的重要!)更改媒体播放类MediaPlayer的代码。如果这样的设计代码,实质上没有多大的变化,只是对原来过程化设计思想的一种替代,并没有击中要害,亦然没有灵活性、可扩展性。 O/Fzw^
2.1单向分派技术的应用(在这里用类的多态来实现的) 6jn<YR
E-
我们不访这样设想:既然mp3和wav都属于音频文件,都具有音频文件的共性,应该建立一个共同的AudioMedia父类。 lBCM;#P
public class AudioMedia 0zd1:*KR,
{ hi37p1t
public void Play() .Ee8s]h5W
{ yY1&hop
MessageBox.Show("Play the AudioMedia file.");
np~oF
} 6ul34\;
} th]9@7UE,
现在引入继承思想,OOD就有点雏形了(不是说有了继承就有了OOD思想,这里只是从继承的角度谈一谈OOD思想,当然从其他角度如合成、聚合等角度也能很好地体现OOD思想)。 Ei#"r\q j_
其实在现实生活中,我们的播放器播放的只能是某种具体类型的音频文件如mp3,因此这个AudioMedia类只能是音频媒体的一个抽象化概念,并没有实际的使用情况。对应在OOD设计中,既这个类永远不会被实例化。为此我们应将其改为抽象类,如下: kxKBI{L
public abstract class AudioMedia p\(%bO
{ Q/< $ (Y
public abstract void Play(); d.{RZq2cp
} P"- ,^?6
Q>.-u6(&
public class MP3:AudioMedia Hi?],5,/
{ '\9A78NV{;
public override void Play() 9 Hm!B )Y
{ xT@\FwPr
MessageBox.Show("Play the mp3 file."); O9opX\9
} [P[syi#]t
} i$ Zhk1
oV*3Mec
public class WAV:AudioMedia %1#5
7-
{ tUtl>>6Iu
public override void Play() VQX#P<
{ kcQ
|Zg
MessageBox.Show("Play the wav file."); %DiZ&}^Ck
} zRB1V99k
} 8Uc#>Ae'_
j88H3bi0
public class MediaPlayer r|&qXb x
{ I@[.W!w
//根据需要完成任务的单向分派 5:O"T
public void Play(AudioMedia media) u%&zY97/
{ qgu.c`GmW
media.Play();
`/#6k>
} B&tl6?7h
} ?+51 B-
到此,我们通过单向分派技术使OOD思想得到进一步的体现。现在的设计,即满足了类之间的层次关系,又保证了类的最小化原则,同时又体现了面向对象设计原则(开—闭原则、里氏代换原则)更利于扩展。(止此,你会发现play方法名的更改是多么必要)。 sR #( \
如果现在又增加了对WMA、MP4等音频文件的播放,只需要设计WMA类,MP4类,并继承AudioMedia,在相应的子类中重写Play方法就可以了,MediaPlayer类对象的Play方法根本不用任何改变。 L8!xn&uyP=
如果让媒体播放器能够支持视频文件,必须另外设计视频媒体的类。因视频文件和音频文件有很多不同的地方,不可能让视频继承音频。假设我们播放器支持RM和MPEG格式的视频。视频类代码如下: Z,ag5 w`]L
public abstract class VideoMedia 7XdLZ4ub
{ N2C^'dFj
public abstract void Play(); _w(SHWh2
} ]F-{)j
VN*^pAzlF
public class RM:VideoMedia m%m8002
{ w(s"r p}
public override void Play() "Sl";.
{ DUa`8cE}
MessageBox.Show("Play the rm file."); yay{lP}b"
} :)bm+xWFF
} l 4(-yWC$H
3TO$J
public class MPEG:VideoMedia J}lBKP:-*
{ h@l5MH=|%
public override void Play() J,k9?nkY /
{ 3%'$AM}+s
MessageBox.Show("Play the mpeg file."); A"p7N?|%
} Q-(twh
} oT.g@kf=H
WM:we*k8h
这样设计还是有点糟糕,这样就无法实用原有的MediaPlayer类了。因为你要播放的视频RM文件并不是音频媒体AudioMedia的子类。 K6_{AuL}4
不过,我们可以这样想,无论音频媒体还是视频媒体都是媒体,有很多相似的功能,如播放、暂停、停止等,为此我们把“媒体”这个概念抽象出来做为一个接口。(虽然也可以用抽象类,但在C#里只支持类的单继承,不过c#支持接口的多继承)。根据接口的定义,你完全可以将相同功能的一系列对象实现同一个接口。让音频媒体类及视频媒体类都继承媒体这个接口。代码如下: $5|/X&"O)/
&R>x;&Gj
public interface IMedia K@`F*^A}V
{ D.4=4"qMi
void Play(); <SOC
} ^#p Su
W8R@Pf
public abstract class AudioMedia:IMedia $
^m_M.1
{
oj[Wzeg%
public abstract void Play(); 4 mPCAA7
} 4Rp2
O$LvHv!
public abstract class VideoMedia:IMedia cYq<.A(hVj
{ {088j?[hzk
public abstract void Play(); b\ F(.8
} P$4G2>D8dg
u.gnvdU
这样再更改MediaPlayer类的代码: rx
CSs
public class MediaPlayer 2VA\{M
{ 7;+:J;xf66
public void Play(IMedia media) ;}ileLTl
{ AMGb6enl
media.Play(); :"|}oKT%mP
} _T7tq
} sTU`@}}
现在看来,程序是不是有很大的灵活性和可扩展性了。 '/J}T -,Z
总结一下,从MediaPlayer类的演变,我们可以得出这样一个结论:在调用类对象的属性和方法时,尽量避免将具体类对象作为传递参数,而应传递其抽象对象,更好地是传递接口,将实际的调用和具体对象完全剥离开,这样可以很好地体现了软件工程的灵活性、扩展性。 _ ^r KOd
现在看起来似乎很完美了,但我们忽略了MediaPlayer的调用者这个事实。仍然需要条件语句来实现。例如,在客户端程序代码中,用户通过选择cbbMediaType组合框的选项,决定播放音频媒体还是视频媒体,然后单击Play按钮执行。 -tlRe12
Public void BtnPlay_Click(object sender,EventArgs e) 8S "vRR
{ ]=m
'| 0}
IMedia media = null; @or&GcQ*
switch (cbbMediaType.SelectItem.ToString().ToLower()) _t_X`
{ Kzfa4C
case ("mp3"): 4jfkCU
media = new MP3(); eR4%4gW)
break;
MD%_Z/NL
//其它类型略; DP6 M4
case ("rm"): %XMwjBM
media = new RM(); J]^)vxm3
break; Pq ZMuUd
//其它类型略; qM~ev E$%
} ^F"Q~?D)
MediaPlayer player = new MediaPlayer(); ,b%T[s7
player.Play(media); >jD,%yG
} Z?kLAhy!
X>(?
2.2设计模式、条件外置及反射技术的应用 ,A
T!:&<X
随着需求的增加,程序将会越来越复杂。此时就应调整设计思想,充分考虑到代码的重构和设计模式的应用。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气爽,不用为代码设计而烦恼了。 e
"5S;
为了实现软件工程的三个主要目标:重用性、灵活性和扩展性。我们不访用设计模式、条件外置及反射来实现。 QqA~y$'ut
使用工厂模式,能够很好地根据需要,调用不同的对象(即动态调用),保证了代码的灵活性。 ^p?O1qTg
虽然这里有两种不同类型的媒体AudioMedia和VideoMedia(以后可能更多),但它们同时又都实现IMedia接口,所以我们可以将其视为一种产品。媒体工厂接口如下: .H
{
public interface IMediaFactory 4mq+{c0
{ gJ6C&8tl
IMedia CreateMedia(); eLXG _Qb"
} [4KW64%l
rnz9TmN:*1
然后为具体的媒体文件对象搭建工厂,并统一实现媒体工厂接口: 8.3888
public class MP3Factory:IMediaFactory ua#sW
{ cLj@+?/
public IMedia CreateMedia() ]PJb 9$f2
{ o7'
cC?u
return new MP3(); ;3wj(o0
} @5+ JXD
} FTZ][
//其它工厂略; pCS2sq8RC
!$P+hX`
public class RMFactory:IMediaFactory b1Bu5%bt,:
{ 28>PmH]7
public IMedia CreateMedia() P{v>o,a.
{ a<9cj@h
return new RM(); f|G,pDLx
} b37P[Q3
} %y;E1pva
//其它工厂略; *$mDu,'8
3)ac
写到这里,也许有人会问,为什么不直接给AudioMedia和VideoMedia类搭建工厂呢?很简单,因为在AudioMedia和VideoMedia中,分别还有不同的类型派生,如果为它们搭建工厂,则在CreateMedia()方法中,仍然要使用条件判断语句,代码缺乏灵活性,不利扩展。 W&