面向对象的多态(二)
书接上一回,我们先回顾一下上一篇文章里面出现问题的代码。
{
switch (browser)
{
case "IE":
NavigateInIE();
case "FF":
NavigateInFF();
}
}
void Back(string browser)
{
switch (browser)
{
case "IE":
BackInIE();
case "FF":
BackInFF();
}
}
在尝试解决重复代码时,我们会把相同的代码放到一个公共函数,把不同的代码以参数形式传递过去。在上面的代码中,我把不同的代码用红色标记出来了。但是在试图抽取公共函数的时候,碰到了点麻烦。我们说了,不同的内容会作为参数,但是,现在的情况下,不同的内容是函数,函数怎么传递呢? 貌似我们要开始考虑面向对象了,因为:1、对象可以作为参数传递。2、对象里面可以定义函数(方法)。
我们现在知道的信息是,各种NavigateInXxx和BackInXxx函数是需要传递的,那么这些函数都应该封装到对象里面,因为这些都是IE/Friefox的行为,我们很容易想到,要定义IE/Firefox类,因为方法被封装在了各自的类里面,InXxx的后缀就可以不要了。
{
void Navigate(); //原来的NavigateInIE();
void Back(); //原来的BackInIE();
}
class Firefox
{
void Navigate(); //原来的NavigateInFF();
void Back(); //原来的BackInFF();
}
我们又看到各种重复的代码,根据面向对象的思想,我们会试着为两个很类似的类建立父类Browser,然后让IE和Firefox都继承于Browser。
{
//声明为抽象函数,因为Browser并不知道IE/Firefox自己的具体实现
abstract void Navigate();
abstract void Back();
}
class IE : Browser
{
override void Navigate(); //原来的NavigateInIE();
override void Back(); //原来的BackInIE();
}
class Firefox : Browser
{
override void Navigate(); //原来的NavigateInFF();
override void Back(); //原来的BackInFF();
}
至此,我们可以说,我的代码是面向对象的。但是,似乎除了多写一个Browser类,我们并没有得到任何的好处,IE和Firefox有各自的Navigate和Back的实现,没法通过继承父类得到,还是得自己重现一遍。那么,Browser类创建出来有什么用呢?
我们回到Navigate函数,现在因为把NavigateInIE和NavigateInFF分别封装到了IE和Firefox对象里面,我的代码可以写成这样:
{
switch(browser)
{
case "IE":
(IE)browserObj.Navigate(); //把类型由Browser转义成IE
case "FF":
(Firefox)browserObj.Navigate(); //把类型由Browser转义成Firefox
}
}
这样稍微能看出来一点Browser类的作用了。 Navigate的第一个参数browser,可以接收"IE"和"FF"字符串,第二个参数,也应该可以接收IE对象和Firefox对象。怎么做到这点呢?用IE类和Firefox类的父类Browser作为参数类型即可。
至少,我们成功地把一个函数封装在对象里,通过这个对象传递给了另一个函数。看看还有没有优化的余地。在调用browserObj.Navigate()的时候,我先把Browser类型转换成了具体的子类(IE/Firefox),但实际上并不需要这样。由于面向对象多态的支持,在代码运行时,运行环境有办法知道browserObj的真实类型,并不需要显示地转义。继续改进Navigate函数。
{
switch(browser)
{
case "IE":
browserObj.Navigate();
case "FF":
browserObj.Navigate();
}
}
这时候,会有点点凌乱,无论传递过去的brower是什么,走哪条选择支,最后被执行的语句都是browserObj.Navigate(); 那我为什么还要把browser传递过去呢?进一步地说,我是不是连选择判断都可以去掉呢?继续修改Navigate函数。
{
browserObj.Navigate();
}
神奇的事情发生了,之前引起维护困难的switch...case...语句消失了。 这时候,要测试IE的Navigate功能,代码如下。
我把IE对象传递过去,真正想传递的其实是把IE的Navigate方法。 当browserObj.Navigate()执行的时候,真正执行的是ie.Navigate()而不是browser.Navigate(),这就是传说中的多态...
而至此,Navigate函数也已经没有存在的必要了:我要调用Navigate函数的时候,要传过去一个browser对象,那我为什么不直接调用browser.Navigate()呢?Back同理。
说了那么多,把老板交下来的任务都忘了。回到我们的自动化测试,因为上面做的各种封装,我们的test script会变成这个样子。
{
browser.Navigate();
ValidateNavigate();
browser.Back();
ValidateBack();
}
SimpleTestCase(new IE()); //在IE下测试
SimpleTestCase(new Firefox()); //在Firefox下测试
和之前其实没什么两样, 但一旦需求变更来到,我们就能看到不同了。还是那个问题,现在我们的脚本要支持在Chrome下测试,还是把要做的事情列出来:
1、增加Chrome类。
2、为Chrome类实现(录制)Navigate方法。
3、为Chrome类实现(录制)Back方法。
4、增加test script的调用。 SimpleTestCase(new Chrome());
我们把在前一种实现中,为了应对变化所需要做的事情也复制过来
1、增加(录制)NavigateInChrome方法
2、增加(录制)BackInChrome方法
3、修改Navigate方法,为Chrome增加一条选择支
4、修改Back方法,为Chrome增加一条选择支
5、增加test script的调用,SimpleTestCase("Chrome")
有什么区别呢?
从工作量的角度看,第二种方式要多写一点搭建类框架的代码,但是不用修改switch case选择支,如前文说的,当我们实现的浏览器功能有100个的时候,可以发现比起省下来修改switch case的时间,多出来的搭建类框架的代码,工作量不值一提。
更重要的是,有没有发现第二种方式要做的,都是“增加”代码,而不是“修改”现有代码?这就是所谓的开放封闭原则。