面向对象设计(OOD)思想——还是以播放器为例(ZT) )Ac,F6w
J2VPOn
有了思想才能飞翔,缺乏灵活就象少了轮子的汽车,难以飞奔。为了更好的理解设计思想,结合一个尽可能简洁的实例来说明OOD、设计模式及重构。通过下面的代码,详细地阐述面向对象设计思想。 (t@)`N{
一、传统过程化设计思想 wz:e\ !
假定我们要设计一个媒体播放器(只从软件设计的角度,不涉及硬件)。该媒体播放器目前只支持音频文件mp3和wav。按照结构化设计思想,设计出来的播放器的代码如下: d5gwc5X
public class MediaPlayer NzQvciJ@"
{ }?Y -I>
w
private void PlayMp3() iptA#<Yj
{ L!Y|`P#Yr
MessageBox.Show("Play the mp3 file."); Ln,<|,fZN
} X^eyrqv
Ljz)%y[s
private void PlayWav() 2T2<I/")O
{ G^)]FwTs
MessageBox.Show("Play the wav file."); a^J(TW/
} ]C,j80+pK
}VJ>}i*
public void Play(string audioType) ,g7O
{ hTLf$_|P
switch (audioType.ToLower()) yg}O9!M J
{ ct-Bq
case ("mp3"): YM_ [
PlayMp3(); ^aAs=KditO
break; {"Sv~L|J;
case ("wav"): \UK}B
PlayWav(); 5\quh2Q_
break; Ro2V-6/
} #1J,!seJ
} wL),/i&<
} c.]QIIdK
从传统的过程化设计思想来看,这是一段既实用又简洁的代码。 1w7tRw
如果,客户又提出新的要求:要播放器不仅仅播放mp3和wav文件,还要播放其他音频文件如wma、mp4等,为此我们要不断地增加相应地播放方法和修改条件语句,直止条件语句足够长。 }kmAUaa,Z
如果,客户感到这个媒体播放器功能太少了,只能闻其声,不能见其人,太单一。如果在听着优美音乐的同时又能看到歌唱者潇洒、英俊的舞姿那就更好了。从代码设计的角度看,他们希望媒体播放器支持视频文件了。也许你会想,不会再增加视频这方面的代码,可以,在增加视频媒体的播放方法,在修改条件判断语句,如果还有其他,还可以同样地增加、修改。到此你也许会提出,要是不修改或很少修改原来的代码就能增添其他功能该多好啊! cF15Mm2
这样看,原来的软件设计结构似乎有点问题。事实上,随着功能的不断增加,你越来越发现这个设计非常的糟糕,因为它根本没有为未来的需求变更提供最起码的扩展。为了应接不暇的变更需求,你不得不不厌其烦地修改原来的代码,使其适应需求变化,甚至在修改代码时,由于过多的代码依赖关系弄得人焦头烂额,直止一塌糊涂。 I*a@_EO
二、面向对象设计思想 #(614-r/
还是以设计一个媒体播放器为例,设计要求相同。不访我们换个设计思路利用面向对象设计思想(OOD)来做做看如何! ?fy37m(M}
根据OOD的思想,我们应该把mp3和wav分别看作是两个独立的对象。代码设计如下: /Kli C\
public class MP3 d {U%q
d
{ +&G(AW
public void Play() |"LHo
H
{ fU$Jh/#":
MessageBox.Show("Play the mp3 file."); P
I"KY@>H
} ZUHW*U.
} @~hy'6/
9]=J+ (M
public class WAV jq)Bj#'7
{ n+=qT$w)
public void Play() $;Fx Zkp
{ Xf&YcHo
MessageBox.Show("Play the wav file."); Gn
9oInY1
} eWv:wNouk
} ^oPFLez56
9~~NxWY%x
Public class MediaPlayer 1<m`38'
{ L-?ty@-i
switch (audioType.ToLower()) !8UIyw
{ +C!GV.q[
case ("mp3"): QYo04`Rl
MP3 m = new MP3(); :&
Dv!z
m.Play(); kfas4mkc
break; *.nSv@F
case ("wav"): aWTurnee^
WAV w = new WAV();
ZJs~,Q
w.Play(); D1y`J&A>Q
break; -hnNaA
} G)s.~ T
} ri4z^1\
现在我们重构代码,建立统一的Play()方法,(在后面的设计中,你会发现这样改名是多么的重要!)更改媒体播放类MediaPlayer的代码。如果这样的设计代码,实质上没有多大的变化,只是对原来过程化设计思想的一种替代,并没有击中要害,亦然没有灵活性、可扩展性。 "|(.W3f1
2.1单向分派技术的应用(在这里用类的多态来实现的) m@kLZimD
我们不访这样设想:既然mp3和wav都属于音频文件,都具有音频文件的共性,应该建立一个共同的AudioMedia父类。 ddN(L`nd
public class AudioMedia VCc=dME
{ ^9,^BHlC0
public void Play() =A,B'n\R
{ `G!HGzVx;j
MessageBox.Show("Play the AudioMedia file."); 4$VDJ
} 5OWyxO3{
} }6a}8EyFP
现在引入继承思想,OOD就有点雏形了(不是说有了继承就有了OOD思想,这里只是从继承的角度谈一谈OOD思想,当然从其他角度如合成、聚合等角度也能很好地体现OOD思想)。 bEcN_7
其实在现实生活中,我们的播放器播放的只能是某种具体类型的音频文件如mp3,因此这个AudioMedia类只能是音频媒体的一个抽象化概念,并没有实际的使用情况。对应在OOD设计中,既这个类永远不会被实例化。为此我们应将其改为抽象类,如下: *ilh/Hd>
public abstract class AudioMedia )I*(yUj
{ eV}" L:bgJ
public abstract void Play(); B\R X
} ShC$ue?Q
':_9o5I
public class MP3:AudioMedia ktfm
{ .:&`PaMt
public override void Play() mTu>S
{ 9+9g (6
MessageBox.Show("Play the mp3 file."); yOz6a :r
} '8)kFR^9
} 8'@5X-nD
15J"iN2"W
public class WAV:AudioMedia Y910\h@V
{ yH"i5L9
public override void Play() Szt2 "AR
{ [(Z(8{3i
MessageBox.Show("Play the wav file."); ^=^\=9"
b
} KJyCfMH&:@
} A{\?]]/
X>`03?L
public class MediaPlayer vW eg1
{ =cV|o]
//根据需要完成任务的单向分派 Z4Q]By:/L
public void Play(AudioMedia media) O'(Us!aq
{ ( gg )?
media.Play(); AJB
NM
} sm'_0EUg
} j=T8b
到此,我们通过单向分派技术使OOD思想得到进一步的体现。现在的设计,即满足了类之间的层次关系,又保证了类的最小化原则,同时又体现了面向对象设计原则(开—闭原则、里氏代换原则)更利于扩展。(止此,你会发现play方法名的更改是多么必要)。 f@YdL6&d-
如果现在又增加了对WMA、MP4等音频文件的播放,只需要设计WMA类,MP4类,并继承AudioMedia,在相应的子类中重写Play方法就可以了,MediaPlayer类对象的Play方法根本不用任何改变。 BhDg\oxZ
如果让媒体播放器能够支持视频文件,必须另外设计视频媒体的类。因视频文件和音频文件有很多不同的地方,不可能让视频继承音频。假设我们播放器支持RM和MPEG格式的视频。视频类代码如下: +0U=UV)U
public abstract class VideoMedia s1wlO y
{ d@ 8M_
O |
public abstract void Play(); :AlvWf$d
} m2^vH+wD
cdkEK
public class RM:VideoMedia &o x
{ yfV]f
LZ
public override void Play() V5$Gb6?K
{ P^"RH&ZQJ
MessageBox.Show("Play the rm file."); '|=Pw
} ?WXftzdf6u
} )rP,+ B?W
\azMF} mb
public class MPEG:VideoMedia v\|jkzR5Y
{ `w#VYs|k
public override void Play() nxV!mh_
{ O EaL2T
MessageBox.Show("Play the mpeg file."); 6oLOA}q
} eb`3'&zV&)
} &c!6e<o[p
vC>2%Zgf-
这样设计还是有点糟糕,这样就无法实用原有的MediaPlayer类了。因为你要播放的视频RM文件并不是音频媒体AudioMedia的子类。 W7A!QS
不过,我们可以这样想,无论音频媒体还是视频媒体都是媒体,有很多相似的功能,如播放、暂停、停止等,为此我们把“媒体”这个概念抽象出来做为一个接口。(虽然也可以用抽象类,但在C#里只支持类的单继承,不过c#支持接口的多继承)。根据接口的定义,你完全可以将相同功能的一系列对象实现同一个接口。让音频媒体类及视频媒体类都继承媒体这个接口。代码如下: Ox#vW6;)
G7CkP
public interface IMedia U&6A)SW,k
{ h[qZM
void Play(); ?7wcv$K5
} k^|z.$+
]@Y!,bw&
public abstract class AudioMedia:IMedia IrZ\;!NK
{ &4evh<z
public abstract void Play(); >3D1:0Sg
} Vx.c`/
X<IW5*
public abstract class VideoMedia:IMedia oS$7k3s
fj
{ 40MKf/9
public abstract void Play(); D$4GNeB+#
} 'z,kxra|n
\5&Mg81
这样再更改MediaPlayer类的代码: R98YGW_
dT
public class MediaPlayer ^@8XJ[C,_
{ `},:dDHI
public void Play(IMedia media) :k?`gm$
{ ;/kd.Q
media.Play(); B|a <=~
} Dks n
} Drtg7v{@\
现在看来,程序是不是有很大的灵活性和可扩展性了。 OKm,iIp]
总结一下,从MediaPlayer类的演变,我们可以得出这样一个结论:在调用类对象的属性和方法时,尽量避免将具体类对象作为传递参数,而应传递其抽象对象,更好地是传递接口,将实际的调用和具体对象完全剥离开,这样可以很好地体现了软件工程的灵活性、扩展性。 ?bM%#x{e
现在看起来似乎很完美了,但我们忽略了MediaPlayer的调用者这个事实。仍然需要条件语句来实现。例如,在客户端程序代码中,用户通过选择cbbMediaType组合框的选项,决定播放音频媒体还是视频媒体,然后单击Play按钮执行。 Uf+y$n-
Public void BtnPlay_Click(object sender,EventArgs e) TYD( 6N
{ !m:WoQ/
IMedia media = null; ;"IWm<]h;-
switch (cbbMediaType.SelectItem.ToString().ToLower()) Uv[a
~'
{ ($`IHKF1.l
case ("mp3"): _Ycz@Jn
media = new MP3(); /9kxDbj
break; XdThl
//其它类型略; 7#+Ih-&EQ
case ("rm"): ~Yc~_)hD
media = new RM(); % t,42jQ9
break; ^A&{g.0
//其它类型略; (*r2bm2FPO
} ]T/%Bau
MediaPlayer player = new MediaPlayer(); yLLA:5Q1
player.Play(media); U@).jpN
} _Zav Y<6
!I1p`_(_7
2.2设计模式、条件外置及反射技术的应用 =7TWzUCO#
随着需求的增加,程序将会越来越复杂。此时就应调整设计思想,充分考虑到代码的重构和设计模式的应用。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气爽,不用为代码设计而烦恼了。 Trh
t2Iv
为了实现软件工程的三个主要目标:重用性、灵活性和扩展性。我们不访用设计模式、条件外置及反射来实现。 b+:mV7eX
使用工厂模式,能够很好地根据需要,调用不同的对象(即动态调用),保证了代码的灵活性。
Txo{6nd/
虽然这里有两种不同类型的媒体AudioMedia和VideoMedia(以后可能更多),但它们同时又都实现IMedia接口,所以我们可以将其视为一种产品。媒体工厂接口如下: ZiY2N*,VO
public interface IMediaFactory 7Z:3xb&>
{ 9\?&u_ U"
IMedia CreateMedia(); p*jU)@a0
} $]#8D>E&