面向对象设计(OOD)思想——还是以播放器为例(ZT) ~h;
2J&~b 8 :
有了思想才能飞翔,缺乏灵活就象少了轮子的汽车,难以飞奔。为了更好的理解设计思想,结合一个尽可能简洁的实例来说明OOD、设计模式及重构。通过下面的代码,详细地阐述面向对象设计思想。 b&&l
一、传统过程化设计思想 72Y6gcg
假定我们要设计一个媒体播放器(只从软件设计的角度,不涉及硬件)。该媒体播放器目前只支持音频文件mp3和wav。按照结构化设计思想,设计出来的播放器的代码如下: NGl
8*Af
public class MediaPlayer 3,{eH6,O7M
{ ,S=[#
private void PlayMp3() rD SYR\cg
{ 9|Jv>Ur=)2
MessageBox.Show("Play the mp3 file."); &TQ~!ZMOR"
} il@>b
Z6i~Dy3
private void PlayWav() =zK4jiM1
{ 4hwb]
Yz
MessageBox.Show("Play the wav file."); J#F5by%8
} *0!p_Hco
Hf]:mhH
public void Play(string audioType) 9AX}V6\+
{ n2B%}LLa
switch (audioType.ToLower())
1?FG3X 5
{ 4i)5=H
case ("mp3"): Jp]?tlT
PlayMp3(); KxX [8
break; yef\Y3X
case ("wav"): U,EoCAm>
PlayWav(); 2RX]~}
break; b^h_`
} a- rR`
} @`4T6eL5
} ^WO3,
从传统的过程化设计思想来看,这是一段既实用又简洁的代码。 {jB>]7
如果,客户又提出新的要求:要播放器不仅仅播放mp3和wav文件,还要播放其他音频文件如wma、mp4等,为此我们要不断地增加相应地播放方法和修改条件语句,直止条件语句足够长。 e,e(t7c?d
如果,客户感到这个媒体播放器功能太少了,只能闻其声,不能见其人,太单一。如果在听着优美音乐的同时又能看到歌唱者潇洒、英俊的舞姿那就更好了。从代码设计的角度看,他们希望媒体播放器支持视频文件了。也许你会想,不会再增加视频这方面的代码,可以,在增加视频媒体的播放方法,在修改条件判断语句,如果还有其他,还可以同样地增加、修改。到此你也许会提出,要是不修改或很少修改原来的代码就能增添其他功能该多好啊! 'QT~o-U
这样看,原来的软件设计结构似乎有点问题。事实上,随着功能的不断增加,你越来越发现这个设计非常的糟糕,因为它根本没有为未来的需求变更提供最起码的扩展。为了应接不暇的变更需求,你不得不不厌其烦地修改原来的代码,使其适应需求变化,甚至在修改代码时,由于过多的代码依赖关系弄得人焦头烂额,直止一塌糊涂。 ?`Yu~a{
二、面向对象设计思想 'lA}E
还是以设计一个媒体播放器为例,设计要求相同。不访我们换个设计思路利用面向对象设计思想(OOD)来做做看如何! _)
x{TnK
根据OOD的思想,我们应该把mp3和wav分别看作是两个独立的对象。代码设计如下: xyk%\&"7
public class MP3 ?o;ip
{ Mu[lk=jC
public void Play() #:gl+
{ [8sYE h
MessageBox.Show("Play the mp3 file."); KQNQ<OE4
} [q2:d^_FA
} JfN
'11,$
y%i9 b&gDd
public class WAV Qq`S=:}~x
{ rz%~=Ca2j
public void Play() :C} I6v=
{ lK=Is
v+
MessageBox.Show("Play the wav file."); u_^mN9h
} IRm}?hHf
} <@;}q^`
|gO7`F2
Public class MediaPlayer T(?w}i
{ 0NU%z.(%s
switch (audioType.ToLower()) HfVHjF)
{ ?uSoJM`wa!
case ("mp3"): FAdTm#tgW]
MP3 m = new MP3(); 2j%=o?me^p
m.Play(); wBXa;.
break; M\m:H3[
case ("wav"): `CS\"|z
WAV w = new WAV(); FE!jN-#
w.Play(); GLtWo+g0
break; {q)d
} H_RfIX)X
} iN
Oj@3x
现在我们重构代码,建立统一的Play()方法,(在后面的设计中,你会发现这样改名是多么的重要!)更改媒体播放类MediaPlayer的代码。如果这样的设计代码,实质上没有多大的变化,只是对原来过程化设计思想的一种替代,并没有击中要害,亦然没有灵活性、可扩展性。 w<`0D)mQ
2.1单向分派技术的应用(在这里用类的多态来实现的) I2$DlEke
我们不访这样设想:既然mp3和wav都属于音频文件,都具有音频文件的共性,应该建立一个共同的AudioMedia父类。 \
T#|<=
public class AudioMedia K`Kv .4
{ .8|wc
public void Play() 6
H P66B
{ 6v3l^~kc'
MessageBox.Show("Play the AudioMedia file."); @@oJ@;
} GB|>eZLv<
} tVAo o-%
现在引入继承思想,OOD就有点雏形了(不是说有了继承就有了OOD思想,这里只是从继承的角度谈一谈OOD思想,当然从其他角度如合成、聚合等角度也能很好地体现OOD思想)。 &<e18L7a
其实在现实生活中,我们的播放器播放的只能是某种具体类型的音频文件如mp3,因此这个AudioMedia类只能是音频媒体的一个抽象化概念,并没有实际的使用情况。对应在OOD设计中,既这个类永远不会被实例化。为此我们应将其改为抽象类,如下: L8h3kT
public abstract class AudioMedia uMw6b=/U
{ Q&]|W
Xv
public abstract void Play(); w/*G!o-<
} toPbFU'
7?whxi Qs
public class MP3:AudioMedia -4Hb]#*2
{ Q0R05*
public override void Play() MWv@]P_0p!
{ a
-Pz<*
MessageBox.Show("Play the mp3 file."); -13}]Gls7Q
} 9-T<gYl
} >XgJo7u
e
n~m)r3&
public class WAV:AudioMedia Sxq@W8W
{ ck{S
public override void Play() }?,?2U,8:
{ Q^f{H.
MessageBox.Show("Play the wav file."); ^5E9p@d"J
} N4+Cg t(
} IrL%0&*hS
2V)+ba|+
public class MediaPlayer RdPk1?}K
{ i4|R0>b
//根据需要完成任务的单向分派 \lQ3j8U
public void Play(AudioMedia media) bIiuna\
{ y{@\8B]
media.Play(); oM!&S'M/
} e|{R2z"^
} X+]>pA
到此,我们通过单向分派技术使OOD思想得到进一步的体现。现在的设计,即满足了类之间的层次关系,又保证了类的最小化原则,同时又体现了面向对象设计原则(开—闭原则、里氏代换原则)更利于扩展。(止此,你会发现play方法名的更改是多么必要)。 lZ-U/$od
如果现在又增加了对WMA、MP4等音频文件的播放,只需要设计WMA类,MP4类,并继承AudioMedia,在相应的子类中重写Play方法就可以了,MediaPlayer类对象的Play方法根本不用任何改变。 ,N(Yjq"R
如果让媒体播放器能够支持视频文件,必须另外设计视频媒体的类。因视频文件和音频文件有很多不同的地方,不可能让视频继承音频。假设我们播放器支持RM和MPEG格式的视频。视频类代码如下: 53:~a
public abstract class VideoMedia <8b1OdA
{ (U&
public abstract void Play(); -SM_JR3<
} 5bt>MoKxv
i6KfH\{N
public class RM:VideoMedia > mO*.' Gm
{ N 5*Qnb8
public override void Play() 4tCM2it%
{ Vr},+Rj
MessageBox.Show("Play the rm file."); !4a fU:
} csW\Q][
} 9s"st\u
4
<9,h!
public class MPEG:VideoMedia MG vz-E1e
{ )7*'r@
public override void Play() cK1^jH<|
{ 7G_<+rn
MessageBox.Show("Play the mpeg file."); J|
N 6r
} <{cY2cx~3
} 6
^3RfF^W
xw9ZRu<z
这样设计还是有点糟糕,这样就无法实用原有的MediaPlayer类了。因为你要播放的视频RM文件并不是音频媒体AudioMedia的子类。 F~6]II
不过,我们可以这样想,无论音频媒体还是视频媒体都是媒体,有很多相似的功能,如播放、暂停、停止等,为此我们把“媒体”这个概念抽象出来做为一个接口。(虽然也可以用抽象类,但在C#里只支持类的单继承,不过c#支持接口的多继承)。根据接口的定义,你完全可以将相同功能的一系列对象实现同一个接口。让音频媒体类及视频媒体类都继承媒体这个接口。代码如下: ,5$G0
Fy{yg]O"
public interface IMedia ;<garDf
{ R278 ^E
void Play(); T]wI)
} 1M&Lb.J6
>Y08/OAI.2
public abstract class AudioMedia:IMedia jlP*RX
{ Sh!c]r>\Q
public abstract void Play(); `*vO8v
} l48$8Mgrr
'UsR/h5T
public abstract class VideoMedia:IMedia 44*#qLN
{ @6G)(NGD
public abstract void Play(); OY{fxBb
} ;"nO'wN:h
eP]y\S*P
这样再更改MediaPlayer类的代码: 7.Y;nem:(
public class MediaPlayer HZAT_
{ o5s6$\"
public void Play(IMedia media) vm|u~Yd,s
{ +H3~Infr4f
media.Play(); X "7CN Td
} B`-uZ9k
} 8s6[-F5
现在看来,程序是不是有很大的灵活性和可扩展性了。 "?zWCH
总结一下,从MediaPlayer类的演变,我们可以得出这样一个结论:在调用类对象的属性和方法时,尽量避免将具体类对象作为传递参数,而应传递其抽象对象,更好地是传递接口,将实际的调用和具体对象完全剥离开,这样可以很好地体现了软件工程的灵活性、扩展性。 zj r($?
现在看起来似乎很完美了,但我们忽略了MediaPlayer的调用者这个事实。仍然需要条件语句来实现。例如,在客户端程序代码中,用户通过选择cbbMediaType组合框的选项,决定播放音频媒体还是视频媒体,然后单击Play按钮执行。 eV*QUjS~
Public void BtnPlay_Click(object sender,EventArgs e) qI uo8o}
{ ,<L4tp+y0
IMedia media = null; u9f^wn
switch (cbbMediaType.SelectItem.ToString().ToLower()) 16/ V5
{ 06&;GW!-
case ("mp3"): W$`v^1M2o
media = new MP3(); `e,}7zGR
break; m
.(ja
//其它类型略; s8,YQ5-
case ("rm"): o)5zvnu7
media = new RM(); twr{jdY9
break; /^xv1F{
//其它类型略; ZFtR#r(~41
} 4N,[Gs<7
MediaPlayer player = new MediaPlayer(); *Vl#]81~
player.Play(media); KhWy
} >`03EsU
P{)D_Bi
2.2设计模式、条件外置及反射技术的应用 g*b`o87PI
随着需求的增加,程序将会越来越复杂。此时就应调整设计思想,充分考虑到代码的重构和设计模式的应用。最后当设计渐趋完美后,你会发现,即使需求不断增加,你也可以神清气爽,不用为代码设计而烦恼了。 -
2L(])t6
为了实现软件工程的三个主要目标:重用性、灵活性和扩展性。我们不访用设计模式、条件外置及反射来实现。 (@}^ 3jpT
使用工厂模式,能够很好地根据需要,调用不同的对象(即动态调用),保证了代码的灵活性。 z~h?"'
虽然这里有两种不同类型的媒体AudioMedia和VideoMedia(以后可能更多),但它们同时又都实现IMedia接口,所以我们可以将其视为一种产品。媒体工厂接口如下: =Oy& f:s
public interface IMediaFactory ?Vg~7Eu0
{ fSbLkd 9
IMedia CreateMedia(); j:cu;6|
} t/t6o&
GbO j%
a
然后为具体的媒体文件对象搭建工厂,并统一实现媒体工厂接口: neu+h6#H
public class MP3Factory:IMediaFactory A>gZl)c
{ YWn""8p;P
public IMedia CreateMedia() f0g/`j@Up
{ sX6\AYF1M
return new MP3(); q,ie)`
} 7=N%$]DKZ
} $EY[CA
E
//其它工厂略; Xd:{.AXW
tkV[^OeU>
public class RMFactory:IMediaFactory q*lk9{>
{ H'3
pHb
public IMedia CreateMedia() 3Y#
{ PV,Z@qm@^
return new RM(); |I1,9ex
} nq?+b >//
} s7yKxg+`{
//其它工厂略; (KC08
)>h3IR
写到这里,也许有人会问,为什么不直接给AudioMedia和VideoMedia类搭建工厂呢?很简单,因为在AudioMedia和VideoMedia中,分别还有不同的类型派生,如果为它们搭建工厂,则在CreateMedia()方法中,仍然要使用条件判断语句,代码缺乏灵活性,不利扩展。 )*}\fmOv{
还有一个问题,就是真的有必要实现AudioMedia和VideoMedia两个抽象类吗?让其子类直接实现接口不是更简单?对于本文提到的需求,是能实现的。但不排除AudioMedia和VideoMedia它们还会存在其他区别,如音频文件还需给声卡提供接口,而视频文件还需给显卡提供接口。如果让MP3、WAV、RM、MPEG直接实现IMedia接口,而不通过AudioMedia和VideoMedia,在满足其它需求的设计上也是不合理的。现在客户端程序代码发生了稍许的改变: 0Lj;t/mG
Public void BtnPlay_Click(object sender,EventArgs e) 9)+!*(D
{ ^
q ba<#e
IMediaFactory factory = null; iWeUsS%zpV
switch (cbbMediaType.SelectItem.ToString().ToLower()) 5)f 'wVe
{ 10zM8<bl
//音频媒体 x3Cn:F
case ("mp3"): UZt3Ua&J
factory = new MP3Factory(); &c-V
QP(
break; WY|~E%k
//视频媒体 CX/[L)|Ru
case ("rm"): b(N+_=
n
factory = new RMFactory(); `Zuo`GP*1
break; Bs0~P 4^
//其他类型略; (zsmJe
} aW:*!d#
MediaPlayer player = new MediaPlayer(); >AV9 K
player.Play(factory.CreateMedia()); H%n/;DW
} j6^.Q/{^
l1zPL3"u_^
到这里,我们再回过头来看MediaPlayer类。这个类中通过单向分派,根据传递参数的不同,分别实现了不同对象的Play方法。在不用工厂模式时,这个类对象会运行得很好。作为一个类库或组件设计者来看,他提供了一个不错的接口,供客户端程序调用。 *H/)S 5
利用工厂模式后,现在看来MediaPlayer类已经多余。所以,我们要记住的是,重构并不仅仅是往原来的代码添加新的内容。当我们发现一些不必要的设计时,还需要果断地删掉这些冗余代码。修改后的代码如下: sB:e:PK
Public void BtnPlay_Click(object sender,EventArgs e) _K?v^oM#
{ -ioO8D&!
IMediaFactory factory = null; gAvNm[=wD2
switch (cbbMediaType.SelectItem.ToString().ToLower()) 0*]0#2Z
{ prO&"t
>
case ("mp3"): o]p$
w[5
factory = new MP3Factory(); o!h::j0,~
break; VB/75xK_
//其他类型略; =UO7!vr;[
case ("rm"): I[Bp}6G
factory = new RMFactory(); hFoeVM[h
break; }6LcimQyK
//其他类型略; -U>)B
} ,hNs{-*
IMedia media = factory.CreateMedia(); Z-t qSw8n
media.Play(); c)Q-yPMl)
} kxe{HxM$Z
%}b
如果你在最开始没有体会到IMedia接口的好处,在这里你应该已经明白了。我们在工厂模式中用到了该接口;而在客户端程序中,仍然要使用该接口。使用接口有什么好处?那就是你的主程序可以在没有具体业务类的时候,同样可以编译通过。因此,即使你增加了新的业务,你的客户端程序是不用改动的。 vB7]L9=@"
不过,这样写客户端代码还是不够理想的,依然不够灵活,在判断具体创建哪个工厂的时候,仍需条件判断。现在看来,如果执行者没有完全和具体类分开,一旦更改了具体类的业务,例如增加了新的工厂类,仍然需要更改客户端程序代码。 }c8e t'HYf
我们可以通过反射技术、条件外置很好地做到客户端的灵活性。 %m lH
条件外置来实现,即通过应用程序的配置文件来实现。我们可以把每种媒体文件类的类型信息放在配置文件中,然后根据配置文件来选择创建具体的对象。并且,这种创建对象的方法将使用反射技术来完成。首先,创建配置文件: ) 5`^@zx
_Iy)p{y
<appSettings> oSYJXs
<add key="mp3" value="MediaLibrary.MP3Factory" /> eYRd#w
<add key="wav" value=" MediaLibrary.WAVFactory" /> Zu#^a|PE*
<add key="rm" value=" MediaLibrary.RMFactory" /> vKoQ!7g
<add key="mpeg" value=" MediaLibrary.MPEGFactory" /> }6u}?>S
</appSettings> 'GW~~UhdW
_Hq)@AI
然后,在客户端程序代码中,自定义一个初始化方法如:InitMediaType(),读取配置文件的所有key值,填充cbbMediaType组合框控件中: q\P{h ij
private void InitMediaType() 7KC2%s#7
{ CiU^U|~ 'L
cbbMediaType.Items.Clear(); (Z@-e^R
foreach (string key in ConfigurationSettings.AppSettings.AllKeys) 4%v-)HGh
{ P<1&kUZL
cbbMediaType.Item.Add(key); e#6H[t
} NB3+kf ,
cbbMediaType.SelectedIndex = 0; \K2S.j
} C.=%8|Zy
}rVLWt
最后,更改客户端程序的Play按钮单击事件: siuDg,uqK5
Public void BtnPlay_Click(object sender,EventArgs e) IwXQbJ3v_
{ )q!dMZ(
string mediaType = cbbMediaType.SelectItem.ToString().ToLower(); vG}\Amx+
string factoryDllName = ConfigurationSettings.AppSettings[mediaType].ToString(); sWA-_ 4
//MediaLibray为引用的媒体文件及工厂的程序集; >PWDo
IMediaFactory factory = (IMediaFactory)Activator.CreateInstance(“MediaLibrary”, Xhyc2DKa_
factoryDllName).Unwrap();
e'|P^G>g
IMedia media = factory.CreateMedia(); FzsW^u+
media.Play(); h/aG."U
} "5,Cy3
,
Z1 &MuV
这样可以很好地体现了软件工程的三个主要目标:重用性、灵活性和扩展性。 0a?[@ -Sz
设想一下,如果我们要增加某种媒体文件的播放功能,如AVI文件。那么,我们只需要在原来的业务程序集中创建AVI类,继承于VideoMedia类。另外在工厂业务中创建AVIFactory类,并实现IMediaFactory接口。假设这个新的工厂类型为MediaLiabrary.AVIFactory,则在配置文件中添加如下一行: IH=%%AS
<add key="AVI" value="MediaLiabrary.AVIFactory" />。 Ka{QjW!%d<
而客户端程序呢?根本不需要做任何改变,甚至不用重新编译,程序就能自如地运行!