面向对象设计(OOD)思想——还是以播放器为例(ZT) <Umr2Vw-
[vY#9W"!
有了思想才能飞翔,缺乏灵活就象少了轮子的汽车,难以飞奔。为了更好的理解设计思想,结合一个尽可能简洁的实例来说明OOD、设计模式及重构。通过下面的代码,详细地阐述面向对象设计思想。 +MZI \>
一、传统过程化设计思想 D;&\)
假定我们要设计一个媒体播放器(只从软件设计的角度,不涉及硬件)。该媒体播放器目前只支持音频文件mp3和wav。按照结构化设计思想,设计出来的播放器的代码如下: G^sx/H76J
public class MediaPlayer dS8ydG2
{ g< xE}[gF
private void PlayMp3() BRy3D\}
{ PJ)l{c
MessageBox.Show("Play the mp3 file."); ]$uC~b
} -{}h6r
boI&q>-6Re
private void PlayWav() NB4Q,iq$
{ UZdGV?o ?
MessageBox.Show("Play the wav file."); K {kd:pr
} "=w:LRw
Er;q s *f
public void Play(string audioType) NLr a"Z
{ P!"{-m'
switch (audioType.ToLower()) A%2B3@1'q
{ ;mYZ@g%e
case ("mp3"): h
w^
V
PlayMp3(); }N}Js*
break; h% KEg667
case ("wav"): gW<6dP'v
PlayWav(); zYP6m3n
break; $KQ q~|
} `KtP;nG
} Rc:}%a%e
} suzK)rJ9i
从传统的过程化设计思想来看,这是一段既实用又简洁的代码。 HUMy\u84H
如果,客户又提出新的要求:要播放器不仅仅播放mp3和wav文件,还要播放其他音频文件如wma、mp4等,为此我们要不断地增加相应地播放方法和修改条件语句,直止条件语句足够长。 z#Fel/L`O
如果,客户感到这个媒体播放器功能太少了,只能闻其声,不能见其人,太单一。如果在听着优美音乐的同时又能看到歌唱者潇洒、英俊的舞姿那就更好了。从代码设计的角度看,他们希望媒体播放器支持视频文件了。也许你会想,不会再增加视频这方面的代码,可以,在增加视频媒体的播放方法,在修改条件判断语句,如果还有其他,还可以同样地增加、修改。到此你也许会提出,要是不修改或很少修改原来的代码就能增添其他功能该多好啊! P z~jW):E
这样看,原来的软件设计结构似乎有点问题。事实上,随着功能的不断增加,你越来越发现这个设计非常的糟糕,因为它根本没有为未来的需求变更提供最起码的扩展。为了应接不暇的变更需求,你不得不不厌其烦地修改原来的代码,使其适应需求变化,甚至在修改代码时,由于过多的代码依赖关系弄得人焦头烂额,直止一塌糊涂。 Q5}XD
二、面向对象设计思想 2{.g7bO
还是以设计一个媒体播放器为例,设计要求相同。不访我们换个设计思路利用面向对象设计思想(OOD)来做做看如何! Yn[>Y)
根据OOD的思想,我们应该把mp3和wav分别看作是两个独立的对象。代码设计如下: Z;V(YK(WO.
public class MP3 H[nco#
{ O_qwD6s-_
public void Play() /@|iI<|
{ /{: XYeX
MessageBox.Show("Play the mp3 file."); \gA<yz-;N
} L}CU"
} "-djA, `
0TaI"/ai
public class WAV OX 'V
{ J;NIa[a
public void Play() n?nzm "g
{ <T?-A}0uO
MessageBox.Show("Play the wav file."); `kJ)E;v;3
} :'B(DzUR
} <$yA*
PDPK|FU
Public class MediaPlayer Ufl\
uq3'H
{ &ocuZ-5`
switch (audioType.ToLower()) aXQS0>G%(
{ aPzn4}~/_
case ("mp3"): /='0W3+o*L
MP3 m = new MP3(); pHoHngyi&
m.Play(); }2@Z{5sh)
break; y%ij)vQY
case ("wav"): G"`
}"T0}
WAV w = new WAV(); <!g]q1
w.Play(); J/jkb3
break; :>.{w$Ln%
} L'r&'y[
} r}&&e BY
f
现在我们重构代码,建立统一的Play()方法,(在后面的设计中,你会发现这样改名是多么的重要!)更改媒体播放类MediaPlayer的代码。如果这样的设计代码,实质上没有多大的变化,只是对原来过程化设计思想的一种替代,并没有击中要害,亦然没有灵活性、可扩展性。 _Iminet
2.1单向分派技术的应用(在这里用类的多态来实现的) pH mqwB~|
我们不访这样设想:既然mp3和wav都属于音频文件,都具有音频文件的共性,应该建立一个共同的AudioMedia父类。 ^7wqb'xg
public class AudioMedia |,p"<a!+{w
{ )M.s<Y
public void Play() J/'Fj?
{ u>)h
MessageBox.Show("Play the AudioMedia file."); 9c[X[Qc
} cN0
*<
} >U,&V%y
现在引入继承思想,OOD就有点雏形了(不是说有了继承就有了OOD思想,这里只是从继承的角度谈一谈OOD思想,当然从其他角度如合成、聚合等角度也能很好地体现OOD思想)。 {7ji m
其实在现实生活中,我们的播放器播放的只能是某种具体类型的音频文件如mp3,因此这个AudioMedia类只能是音频媒体的一个抽象化概念,并没有实际的使用情况。对应在OOD设计中,既这个类永远不会被实例化。为此我们应将其改为抽象类,如下: w.w{L=p:<"
public abstract class AudioMedia aSvv(iV
{ b"A,q
public abstract void Play(); QZ l#^-on
} )`rD]0ua;
mA{?E9W
public class MP3:AudioMedia 5aizWz
{ y62f{ks_/
public override void Play() &'$Bk5 D@G
{ yf9"Rc~+
MessageBox.Show("Play the mp3 file."); >Xk42zvqn
}
I8?
} `jGeS[FhR
;Y\LsmZ;F
public class WAV:AudioMedia #zTy7ZS,0
{ VIz(@
public override void Play() -y-}g[`
{ u:f ]|Q
MessageBox.Show("Play the wav file."); RFL*
qd4
} 7S}0Kuk)
} UsyNn39
L]H'$~xx*
public class MediaPlayer \1!Q.V
{ 6p4BsWPx
//根据需要完成任务的单向分派 -`I|=lBz{H
public void Play(AudioMedia media) QCW4gIp
{ d}4NL:=&
media.Play(); ;[::&qf
} ^$: w
} D{N8q^Cs9
到此,我们通过单向分派技术使OOD思想得到进一步的体现。现在的设计,即满足了类之间的层次关系,又保证了类的最小化原则,同时又体现了面向对象设计原则(开—闭原则、里氏代换原则)更利于扩展。(止此,你会发现play方法名的更改是多么必要)。 Zu.hcDw1
如果现在又增加了对WMA、MP4等音频文件的播放,只需要设计WMA类,MP4类,并继承AudioMedia,在相应的子类中重写Play方法就可以了,MediaPlayer类对象的Play方法根本不用任何改变。 Ncz4LKzt
如果让媒体播放器能够支持视频文件,必须另外设计视频媒体的类。因视频文件和音频文件有很多不同的地方,不可能让视频继承音频。假设我们播放器支持RM和MPEG格式的视频。视频类代码如下: \Ip)Lm0
public abstract class VideoMedia ^[7Mp
{ +r3)\L{U
public abstract void Play(); oh8:1E,I
} m`-);y
cF8
2wg
public class RM:VideoMedia G$>?UQ[
{ K5qCPt`'
public override void Play() `f>!/Zm%9
{ @3?>[R
MessageBox.Show("Play the rm file."); !G3AD3
} G x[ZHpy;
} uI-T]N:W8x
@GN2v,WA?
public class MPEG:VideoMedia F-/z@tM
{ S[F06.(1
public override void Play() - Z,Qj"V
{
8z1z<\
MessageBox.Show("Play the mpeg file."); )$n%4 :
} rRq60A
} xG/Q%A
N]+6<
这样设计还是有点糟糕,这样就无法实用原有的MediaPlayer类了。因为你要播放的视频RM文件并不是音频媒体AudioMedia的子类。 13s/m&
不过,我们可以这样想,无论音频媒体还是视频媒体都是媒体,有很多相似的功能,如播放、暂停、停止等,为此我们把“媒体”这个概念抽象出来做为一个接口。(虽然也可以用抽象类,但在C#里只支持类的单继承,不过c#支持接口的多继承)。根据接口的定义,你完全可以将相同功能的一系列对象实现同一个接口。让音频媒体类及视频媒体类都继承媒体这个接口。代码如下: wD9K\%jIr!
zTg\\z;
public interface IMedia |'?vlUCd
{ yNns6
void Play(); f)*"X[)o
} )aIcA
^o3,YH
public abstract class AudioMedia:IMedia |qw0:c=7!
{ ~*iF`T6
public abstract void Play(); %y6Q3@
} ^#:;6^Su
\(Sly&gL
public abstract class VideoMedia:IMedia 8TP$ ?8l
{ -%`~3*L
public abstract void Play(); <VxA&bb7c
} ^~H}N$W"-q
\(\a=
这样再更改MediaPlayer类的代码: ]UvB+M]Lv)
public class MediaPlayer aS [[
AL
{ b&rBWp0#
public void Play(IMedia media) [9">}l
{ zq};{~u(
media.Play(); ,{0Y:/T'
} Yqs=jTq`{
} :TQp,CEa
现在看来,程序是不是有很大的灵活性和可扩展性了。 =*\.zr
总结一下,从MediaPlayer类的演变,我们可以得出这样一个结论:在调用类对象的属性和方法时,尽量避免将具体类对象作为传递参数,而应传递其抽象对象,更好地是传递接口,将实际的调用和具体对象完全剥离开,这样可以很好地体现了软件工程的灵活性、扩展性。 U?0|2hR~
现在看起来似乎很完美了,但我们忽略了MediaPlayer的调用者这个事实。仍然需要条件语句来实现。例如,在客户端程序代码中,用户通过选择cbbMediaType组合框的选项,决定播放音频媒体还是视频媒体,然后单击Play按钮执行。 ,R{&x7
Public void BtnPlay_Click(object sender,EventArgs e) H~j@n!)
{ ;"MChk
IMedia media = null; (]#^q8)]\9
switch (cbbMediaType.SelectItem.ToString().ToLower()) 3:WqUb\QK
{ ['mpxtG
case ("mp3"): DsHF9Mn
media = new MP3(); _EMI%P&s
break; s>>&3jfM
//其它类型略; xvTtA61Vp
case ("rm"): Zy(i_B-b
media = new RM(); (K[{X0T
break; Jh\KVmfXN
//其它类型略; pQa51 nc
} <j'K7We/tP
MediaPlayer player = new MediaPlayer(); qf0pi&q
player.Play(media); oXG_6E!^
} _BewaI;w
UXct+l
2.2设计模式、条件外置及反射技术的应用 <X8Urum
随着需求的增加,程序将会越来越复杂。此时就应调整设计思想,充分考虑到代码的重构和设计模式的应用。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气爽,不用为代码设计而烦恼了。 y<Koc>8
为了实现软件工程的三个主要目标:重用性、灵活性和扩展性。我们不访用设计模式、条件外置及反射来实现。 `PUqz&
使用工厂模式,能够很好地根据需要,调用不同的对象(即动态调用),保证了代码的灵活性。 KTm^}')C8
虽然这里有两种不同类型的媒体AudioMedia和VideoMedia(以后可能更多),但它们同时又都实现IMedia接口,所以我们可以将其视为一种产品。媒体工厂接口如下: `LkrG9KV{
public interface IMediaFactory `
i[26Qb
{ 2y!n c%
IMedia CreateMedia(); /!Rva"
} qR@ESJ_
']nIa7
然后为具体的媒体文件对象搭建工厂,并统一实现媒体工厂接口: |P5dv>tb
F
public class MP3Factory:IMediaFactory r8m}B#W7
{ +
f6LG 0q
public IMedia CreateMedia() 9
/H~hEVK
{ pf8'xdExH)
return new MP3(); L~&S<5?
} _A,_RM$Y
} XC3)#D#HGh
//其它工厂略; {-5)nS^_
,buX|
public class RMFactory:IMediaFactory BwHJr(n
{ F8w7N$/V",
public IMedia CreateMedia() ,l&?%H9q
{ 5p.rd0T]l3
return new RM(); ci;2XLAM
} H>+/k-n-
} 'dj3y/
k%
//其它工厂略; ?A@y4<8R|
(XOz_K6c%K
写到这里,也许有人会问,为什么不直接给AudioMedia和VideoMedia类搭建工厂呢?很简单,因为在AudioMedia和VideoMedia中,分别还有不同的类型派生,如果为它们搭建工厂,则在CreateMedia()方法中,仍然要使用条件判断语句,代码缺乏灵活性,不利扩展。 I{$suPk
还有一个问题,就是真的有必要实现AudioMedia和VideoMedia两个抽象类吗?让其子类直接实现接口不是更简单?对于本文提到的需求,是能实现的。但不排除AudioMedia和VideoMedia它们还会存在其他区别,如音频文件还需给声卡提供接口,而视频文件还需给显卡提供接口。如果让MP3、WAV、RM、MPEG直接实现IMedia接口,而不通过AudioMedia和VideoMedia,在满足其它需求的设计上也是不合理的。现在客户端程序代码发生了稍许的改变: nFni1cCD
Public void BtnPlay_Click(object sender,EventArgs e) "r6qFxY
{ 1L<TzQ
IMediaFactory factory = null; XoyxS:=>|[
switch (cbbMediaType.SelectItem.ToString().ToLower()) hE:~~ox
{ M{L<aYe
//音频媒体 @*hv|zjs
case ("mp3"): Qy:yz
factory = new MP3Factory(); $j- Fm:ZIA
break; ho>@ $9
//视频媒体 /o4_rzR?
case ("rm"): >9w^C1"
factory = new RMFactory(); s$(%]~P
break; V_
6K ?~j
//其他类型略; 6@|!m '
} w^L`"
MediaPlayer player = new MediaPlayer(); /[6wm1?!
player.Play(factory.CreateMedia()); `6rLd>=R
} u/W{JPlL
A6szTX#0
到这里,我们再回过头来看MediaPlayer类。这个类中通过单向分派,根据传递参数的不同,分别实现了不同对象的Play方法。在不用工厂模式时,这个类对象会运行得很好。作为一个类库或组件设计者来看,他提供了一个不错的接口,供客户端程序调用。 G:e=9qTf
利用工厂模式后,现在看来MediaPlayer类已经多余。所以,我们要记住的是,重构并不仅仅是往原来的代码添加新的内容。当我们发现一些不必要的设计时,还需要果断地删掉这些冗余代码。修改后的代码如下: 54z`KX
73
Public void BtnPlay_Click(object sender,EventArgs e) ;*5z&1O
{ %>K(IRpMW
IMediaFactory factory = null; `pbCPa{Y
switch (cbbMediaType.SelectItem.ToString().ToLower()) "0!#De
{ m&A bH&;
case ("mp3"): (B\Kb4m
factory = new MP3Factory(); xDe^>(,"
break; bN?*p($/
//其他类型略; y6am(ugE
case ("rm"):
Gh_5$@ hF
factory = new RMFactory(); 9{u/|,rq1
break; kYS#P(1
//其他类型略; >0kL9_9{
} T \34<+n1N
IMedia media = factory.CreateMedia(); ,pcyU\68v
media.Play(); J*g<]P&p0
} a/E(GQ,,
y2%[/L:u~
如果你在最开始没有体会到IMedia接口的好处,在这里你应该已经明白了。我们在工厂模式中用到了该接口;而在客户端程序中,仍然要使用该接口。使用接口有什么好处?那就是你的主程序可以在没有具体业务类的时候,同样可以编译通过。因此,即使你增加了新的业务,你的客户端程序是不用改动的。 uEPm[oyX
不过,这样写客户端代码还是不够理想的,依然不够灵活,在判断具体创建哪个工厂的时候,仍需条件判断。现在看来,如果执行者没有完全和具体类分开,一旦更改了具体类的业务,例如增加了新的工厂类,仍然需要更改客户端程序代码。 .dzw5R&
我们可以通过反射技术、条件外置很好地做到客户端的灵活性。 ]UOzz1
条件外置来实现,即通过应用程序的配置文件来实现。我们可以把每种媒体文件类的类型信息放在配置文件中,然后根据配置文件来选择创建具体的对象。并且,这种创建对象的方法将使用反射技术来完成。首先,创建配置文件: Wr Wz+5M8
V,ZRX}O
<appSettings> (6[<