面向对象设计(OOD)思想——还是以播放器为例(ZT) Vt:~q{9*k
/
F4z g3
有了思想才能飞翔,缺乏灵活就象少了轮子的汽车,难以飞奔。为了更好的理解设计思想,结合一个尽可能简洁的实例来说明OOD、设计模式及重构。通过下面的代码,详细地阐述面向对象设计思想。 e> e}vZlX
一、传统过程化设计思想 @#T|Y&
假定我们要设计一个媒体播放器(只从软件设计的角度,不涉及硬件)。该媒体播放器目前只支持音频文件mp3和wav。按照结构化设计思想,设计出来的播放器的代码如下: @tNz Q8
public class MediaPlayer R;uvkg[o
{ v3~? ;f,l
private void PlayMp3() 'vbsv T
{ }ppN k:B
MessageBox.Show("Play the mp3 file."); <Tzrj1"Q3
} 0zmE>/O+
r1 !@hT
private void PlayWav() `yrB->|vG
{ L*xhGoC=
MessageBox.Show("Play the wav file."); ?PeJlpYzV
} zPn+V7F
4'/nax$Bx;
public void Play(string audioType) ls\WXCH
{ {Aw#?#GPW
switch (audioType.ToLower()) %[Wh [zZy
{ N
+Yxz;Mg
case ("mp3"): ;K:zmH
PlayMp3(); `u3EU*~W
break; y\4L{GlBM
case ("wav"): )~)J?l3{
PlayWav(); f-vCm 5f
break; Dp,L/1GQ8
} 89pEfl j2
} %g{X ?
}
4":KoS`,j
从传统的过程化设计思想来看,这是一段既实用又简洁的代码。 _|kxY'_[8
如果,客户又提出新的要求:要播放器不仅仅播放mp3和wav文件,还要播放其他音频文件如wma、mp4等,为此我们要不断地增加相应地播放方法和修改条件语句,直止条件语句足够长。 J=9FRC
如果,客户感到这个媒体播放器功能太少了,只能闻其声,不能见其人,太单一。如果在听着优美音乐的同时又能看到歌唱者潇洒、英俊的舞姿那就更好了。从代码设计的角度看,他们希望媒体播放器支持视频文件了。也许你会想,不会再增加视频这方面的代码,可以,在增加视频媒体的播放方法,在修改条件判断语句,如果还有其他,还可以同样地增加、修改。到此你也许会提出,要是不修改或很少修改原来的代码就能增添其他功能该多好啊! YxYH2*q@
这样看,原来的软件设计结构似乎有点问题。事实上,随着功能的不断增加,你越来越发现这个设计非常的糟糕,因为它根本没有为未来的需求变更提供最起码的扩展。为了应接不暇的变更需求,你不得不不厌其烦地修改原来的代码,使其适应需求变化,甚至在修改代码时,由于过多的代码依赖关系弄得人焦头烂额,直止一塌糊涂。 >JHryS.j$4
二、面向对象设计思想 :~"CuB/
还是以设计一个媒体播放器为例,设计要求相同。不访我们换个设计思路利用面向对象设计思想(OOD)来做做看如何! g:g\>@Umo
根据OOD的思想,我们应该把mp3和wav分别看作是两个独立的对象。代码设计如下: FH?U(-
public class MP3 \)#kquH/l
{ at#ja_ hd
public void Play() D=j-!{zB
{ BKCA<
MessageBox.Show("Play the mp3 file."); q;T{|5/O
} x9UX!Z5*>
} k:t]s_`<
Yb|c\[ %
public class WAV 2b}t,&bv?
{ KrgFKRgGj
public void Play() #xsE3Wj-X
{ wN_Vfb
MessageBox.Show("Play the wav file."); MU@UfB|;u
} 44ek
IV+?
} EH+"~-v)ae
gX@HO|.t
Public class MediaPlayer >?2M
}TV3
{ h5*JkRm
switch (audioType.ToLower()) 1gL2ia
{ b|l:fT?&
case ("mp3"): #^u$
MP3 m = new MP3(); `uv2H$
m.Play(); W#9BNKL
break; u_w#gjiC
case ("wav"): 2Q/x@aT,h
WAV w = new WAV(); B3pCy~*5
w.Play(); o |{5M|nD
break; \tf<B\oa
} >c1qpk/
} `x+ B+)0X
现在我们重构代码,建立统一的Play()方法,(在后面的设计中,你会发现这样改名是多么的重要!)更改媒体播放类MediaPlayer的代码。如果这样的设计代码,实质上没有多大的变化,只是对原来过程化设计思想的一种替代,并没有击中要害,亦然没有灵活性、可扩展性。 *'Sd/%8{
2.1单向分派技术的应用(在这里用类的多态来实现的) n`? py
我们不访这样设想:既然mp3和wav都属于音频文件,都具有音频文件的共性,应该建立一个共同的AudioMedia父类。 !,wIQy_e4
public class AudioMedia s 1A.+
{ N({MPO9
public void Play() fx41,0;gZq
{ q(
MessageBox.Show("Play the AudioMedia file."); 1-8mFIK
} dP9qSwTa
} Q3kdlxXR
现在引入继承思想,OOD就有点雏形了(不是说有了继承就有了OOD思想,这里只是从继承的角度谈一谈OOD思想,当然从其他角度如合成、聚合等角度也能很好地体现OOD思想)。 -]0OKE&
其实在现实生活中,我们的播放器播放的只能是某种具体类型的音频文件如mp3,因此这个AudioMedia类只能是音频媒体的一个抽象化概念,并没有实际的使用情况。对应在OOD设计中,既这个类永远不会被实例化。为此我们应将其改为抽象类,如下: =Gpylj7?~
public abstract class AudioMedia ^n*)7K[
{ f%is~e~wc
public abstract void Play(); ePOG}k($/%
} ],@rS9K
C)[,4wt,
public class MP3:AudioMedia xgwY@'GN
{ b1(T4w6
public override void Play() >!eAM )
{ [^WC lRF
MessageBox.Show("Play the mp3 file."); Fco`^kql.D
} {{$Nqn,pH
} K0u|U`
tURu0`](
public class WAV:AudioMedia 5bRJS70M
{ G)EU_UE9
public override void Play() 8zZvht*
{ VQc_|z_s
MessageBox.Show("Play the wav file."); v> LIvi|]
} "3X2VFwoJ
} VACQ+
&|s0P
public class MediaPlayer lUOF4U&r
{ [T8WThs
//根据需要完成任务的单向分派 }~YA5^VQ$
public void Play(AudioMedia media) E-T)*`e
{ u4t7Ie*Q
media.Play(); kYzIp
} )X1{
} >nJ\BPx
到此,我们通过单向分派技术使OOD思想得到进一步的体现。现在的设计,即满足了类之间的层次关系,又保证了类的最小化原则,同时又体现了面向对象设计原则(开—闭原则、里氏代换原则)更利于扩展。(止此,你会发现play方法名的更改是多么必要)。 %R}qg6dL
如果现在又增加了对WMA、MP4等音频文件的播放,只需要设计WMA类,MP4类,并继承AudioMedia,在相应的子类中重写Play方法就可以了,MediaPlayer类对象的Play方法根本不用任何改变。 ,82S=N5V!
如果让媒体播放器能够支持视频文件,必须另外设计视频媒体的类。因视频文件和音频文件有很多不同的地方,不可能让视频继承音频。假设我们播放器支持RM和MPEG格式的视频。视频类代码如下: Y>dF5&(kb
public abstract class VideoMedia /K+r?
]kf
{ -RE^tW*Yy
public abstract void Play(); 3atBX5
} QE^$=\l0
3lf=b~Zi)
public class RM:VideoMedia Zd3S:),&
{ tIWmp30S
public override void Play() |6.l7u?d
{ p2hB8zL
MessageBox.Show("Play the rm file."); )1]ZtU
} vDp8__^
} bg!/%[ {M
W,K;6TZhh
public class MPEG:VideoMedia JgxtlYjl
{ \Z?9{J
public override void Play() h1j!IG
{ ty8q11[8
MessageBox.Show("Play the mpeg file."); "Bh}}!13
} /
kF)
} 8V~k5#&Ow
Cz9xZA{[M
这样设计还是有点糟糕,这样就无法实用原有的MediaPlayer类了。因为你要播放的视频RM文件并不是音频媒体AudioMedia的子类。 ,kyJAju>
不过,我们可以这样想,无论音频媒体还是视频媒体都是媒体,有很多相似的功能,如播放、暂停、停止等,为此我们把“媒体”这个概念抽象出来做为一个接口。(虽然也可以用抽象类,但在C#里只支持类的单继承,不过c#支持接口的多继承)。根据接口的定义,你完全可以将相同功能的一系列对象实现同一个接口。让音频媒体类及视频媒体类都继承媒体这个接口。代码如下: $jjfC
p\ Q5,eg
public interface IMedia W/=.@JjI
{ ayn) 5q/z
void Play(); :">!r.Q
} BOX{]EOj
T(#J_Y
public abstract class AudioMedia:IMedia R}-(cc%5
{ IB(6+n,6s
public abstract void Play(); d?y4GkK
} 3(="YbZ
"H=6j)Cb
public abstract class VideoMedia:IMedia Jz\%%C
{ '*Z1tDFS
public abstract void Play(); `XJG(Oas\
} R
MR;1
2*p
这样再更改MediaPlayer类的代码: YDIG,%uv
public class MediaPlayer RF`.xQ26=
{ OTvPU kp*
public void Play(IMedia media) Fe# 1
{ 9>=;FY
media.Play(); g&^quZ"H
} +G$4pt|=
} >f|||H}Snw
现在看来,程序是不是有很大的灵活性和可扩展性了。 Ryl:a\
总结一下,从MediaPlayer类的演变,我们可以得出这样一个结论:在调用类对象的属性和方法时,尽量避免将具体类对象作为传递参数,而应传递其抽象对象,更好地是传递接口,将实际的调用和具体对象完全剥离开,这样可以很好地体现了软件工程的灵活性、扩展性。 "SNn^p59k
现在看起来似乎很完美了,但我们忽略了MediaPlayer的调用者这个事实。仍然需要条件语句来实现。例如,在客户端程序代码中,用户通过选择cbbMediaType组合框的选项,决定播放音频媒体还是视频媒体,然后单击Play按钮执行。 |'e^QpU5
Public void BtnPlay_Click(object sender,EventArgs e) ^-TE([ bW
{ l#g\X'bK
IMedia media = null; Z]A{ d[
switch (cbbMediaType.SelectItem.ToString().ToLower()) )!3V/`I
{ M-$%Rzl_
case ("mp3"): lXx=But
media = new MP3(); ^6jV_QM#
break; sG(~^hJ_
//其它类型略; 9Uh"iMB
case ("rm"): g1;:KzVv
media = new RM(); zv|2:4H
break; u]g%@3Pn
//其它类型略; )1Y{Q Y}l
} X@ --m6-
MediaPlayer player = new MediaPlayer(); V )3KS-
player.Play(media); w_ akn t T
} 0 3L]
YC*"Thuu
2.2设计模式、条件外置及反射技术的应用 w0[6t#$F
随着需求的增加,程序将会越来越复杂。此时就应调整设计思想,充分考虑到代码的重构和设计模式的应用。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气爽,不用为代码设计而烦恼了。 ZFA`s
qT
为了实现软件工程的三个主要目标:重用性、灵活性和扩展性。我们不访用设计模式、条件外置及反射来实现。 t0( A4E
使用工厂模式,能够很好地根据需要,调用不同的对象(即动态调用),保证了代码的灵活性。 T"(&b~m2b4
虽然这里有两种不同类型的媒体AudioMedia和VideoMedia(以后可能更多),但它们同时又都实现IMedia接口,所以我们可以将其视为一种产品。媒体工厂接口如下: 1Rt33\1J0
public interface IMediaFactory dhC$W!N7!
{ 0XOp3
IMedia CreateMedia(); -$t{>gO#Y
} ^gN6/>]qrY
Wt*cIZ
然后为具体的媒体文件对象搭建工厂,并统一实现媒体工厂接口: u^^vB\"^
public class MP3Factory:IMediaFactory JOj;^h
{ 0B[="rTS7#
public IMedia CreateMedia() v|Pv 03%?7
{ bYcV$KJk
return new MP3(); #{6{TFx\
} /RG:W0=K
} PoHg,n]
//其它工厂略; O24m;oHM
cA&9e<
public class RMFactory:IMediaFactory L s
G\OG
{ kAKK bmE
public IMedia CreateMedia() rExnxQ<e
{ -fM1nH&
return new RM(); b\ X@gq
} ~b(i&DVK
} @tF\p
//其它工厂略; \|n-
O=}=2
8mCxn@yV
写到这里,也许有人会问,为什么不直接给AudioMedia和VideoMedia类搭建工厂呢?很简单,因为在AudioMedia和VideoMedia中,分别还有不同的类型派生,如果为它们搭建工厂,则在CreateMedia()方法中,仍然要使用条件判断语句,代码缺乏灵活性,不利扩展。 EHSlK5bD,
还有一个问题,就是真的有必要实现AudioMedia和VideoMedia两个抽象类吗?让其子类直接实现接口不是更简单?对于本文提到的需求,是能实现的。但不排除AudioMedia和VideoMedia它们还会存在其他区别,如音频文件还需给声卡提供接口,而视频文件还需给显卡提供接口。如果让MP3、WAV、RM、MPEG直接实现IMedia接口,而不通过AudioMedia和VideoMedia,在满足其它需求的设计上也是不合理的。现在客户端程序代码发生了稍许的改变: OP;v bZ
Public void BtnPlay_Click(object sender,EventArgs e) _Mi5g_
{ 2kqu p)82e
IMediaFactory factory = null; q'+)t7!
switch (cbbMediaType.SelectItem.ToString().ToLower()) |e=,oV"
{ a y4 %
//音频媒体 ]v?@g:iE
case ("mp3"): #./fY;:cj
factory = new MP3Factory(); -Sqz5lo
break; $&Gu)4'+
//视频媒体 ?(xnSW@r
case ("rm"): LY+@o<>
factory = new RMFactory(); &PL8