First we try, then we trust

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  183 随笔 :: 111 文章 :: 3312 评论 :: 358 引用

十年前,我有一个很有钱的朋友,他家有三辆汽车(VOLVO(沃尔沃)、BENCH(奔驰)、MAZDA(马自达)),还雇了司机为他开车。不过,这个人上车后跟司机说的话取决于他坐的车:当他坐上VOLVO后,会跟司机说“开沃尔沃车!”,坐上BENCH后他说“开奔驰车!”,坐上MAZDA后他说“开马自达车!”。

大家猜这个人怎么着?.....有病!

其实我这个朋友叫“C”。

注:我对C一直很虔诚,上大学时,C语言是我最喜爱的语言。而且它的功能要远比我在后面例子中描述的功能强大的多(毕竟还有杀手锏“指针”呢),我并不想让这段故事给C留下什么不好的印象,只是举例而已(BASIC和VFP什么的连举例资格都没有呢  )。

把上面的故事用C写下来的化就是(我在Tubro C++ 1.0下调试通过):

/****** CARTEST.C *******/

#include
<stdio.h>

void PrintHelp();
void DriveVolvo();
void DriveBench();
void DriveMazda();

main(
int argc, char *argvs[])
{
  
if(argc < 2)
  
{
    PrintHelp();
    
return;
  }

  
  
// C 先生的开车法则
  if(strcmp(argvs[1],"V")==0)
    DriveVolvo();     
//开沃尔沃车!
  else if(strcmp(argvs[1], "B")==0)
    DriveBench();     
//开奔驰车!
  else if(strcmp(argvs[1], "M")==0)
    DriveMazda();     
//开马自达车!
  else
    PrintHelp();

  
return;
}


void PrintHelp()
{
  printf(
"Please input a correct car type.(V, B, M) ");
}


void DriveVolvo()
{
  printf(
"Driving Volvo  ");
}


void DriveBench()
{
  printf(
"Driving Bench  ");
}

void DriveMazda()
{
  printf(
"Driving Mazda  ");
}

程序编译成可执行文件后,在命令提示符下输入: CARTEST V 或 CARTEST B 或 CARTEST M。程序自动完成开不同车的功能。

现在让我们看看C先生病在哪里?其实,C先生之所以“有病”,就是在他发号的施令上,实际上只要说声“开车”就行了,他却不厌其烦的在里面加上车名(DriveVolvo(); DriveBench(); DriveMazda();)。

如果用用C#改写上面的程序的话,我们可以将程序写成:

using System;

public class Client
{
  
public static void Main(string[] argvs)
  
{
    Car c;

    
if(argvs.Length < 1)
    
{
      PrintHelp();
      
return;
    }


    
// 司机将车开来
    if(argvs[0== "V")
      c 
= new Volvo();
    
else if(argvs[0== "B")
      c 
= new Bench();
    
else if(argvs[0== "M")
      c 
= new Mazda();
    
else
    
{
      PrintHelp();
      
return;
    }


    
// C#先生发号施令“开车!”
    c.Drive();
  }


  
private static void PrintHelp()
  
{
    Console.WriteLine(
"Please input a correct car type.(V, B, M)");
    Console.WriteLine(
"For example: CarTest M");
  }

}


public abstract class Car
{
  
public abstract void Drive();
}


public class Volvo : Car
{
  
public override void Drive()
  
{
    Console.WriteLine(
"Driving Volvo ");
  }

}


public class Bench : Car
{
  
public override void Drive()
  
{
    Console.WriteLine(
"Driving Bench ");
  }

}


public class Mazda : Car
{
  
public override void Drive()
  
{
    Console.WriteLine(
"Driving Mazda ");
  }

}

现在问题就出来了,这两种做法哪种更好一些呢?是不是C#将本是很简单的问题搞复杂了呢?让我们分析一下:

  1. 从代码长度来看,显然C语言的代码长度要远少于C#的代码。两程序完成的是相同的功能。
  2. 从代码结构上看,C语言的结构也要比C#清晰,属于典型的结构化程序设计。
  3. 从“有病”的角度看,显然C语言程序“有病”,而C#程序更为容易接受。

C#程序通过对车的抽象,实现只需“开车”,就可以调用任何车的开车方法。这就是我们常说的“多态性”。将多态性浓缩到两行代码上,就是(以下简称方法一):

Car c = new Bench();
c.Drive();

不要小看这两行代码,隐藏在其中的深意还需要我们好好挖掘一下。

有人可能会问,不就是开车吗,开奔驰就是开奔驰,干吗要把奔驰赋值给车,然后调用车的开车,再通过多态性转而调用奔驰的开车。如此麻烦,不如直接就调用奔驰的开车(以下简称方法二):

Bench b = new Bench();
b.Drive();

到底谁好谁坏,我们可以从两个角度来看这个问题:

一、从迪米特法则的角度来看:

(关于迪米特法则,请参考:C#设计模式(3)

迪米特法则可以简单的表述成最小知识原则,也叫做“使民无知”。一个对象应当对其它对象知道的越少越好。

如果客户在进行代码调用时,使用了方法二的方法,那么当不开奔驰转开沃尔沃时,必须将客户端所有Bench的代码改为Volvo。如果两个车都可能开的化,那么客户端不得不跟两个对象都打交道。

如果采用方法一的方法,客户只需要知道“车”就行了,反正车都可以开。至于什么车,客户并不关心,关键的是能开就行。这不但很好的应用了迪米特法则,同时也应用了里氏代换原则(参见:C#设计模式(2)):“一个子类可以替换掉父类”。这允许在客户不知情的情况下就可以代换不同类型的车。

二、从开放封闭原则的角度来看:

(关于开放封闭原则,请参考:C#设计模式(2)

开放封闭原则要求对修改封闭,对扩展开放。在上面的两个例子种,方法二没有很好遵循开放封闭原则,当添加新类型汽车后,不得不修改代码以适应这种改变。而方法一具有很强的适应性,只需要给Car对象添加一个子类就可以了,客户由于只知道有“车”,所以加一种新车后,根本不需要改变客户端代码。因此也提高了系统的可维护性。

工厂模式中之所以引入“工厂”的概念,而抛弃直接使用 new 实例化对象,其中一个根本的原因也在于此。通过对“简单工厂模式”、“工厂方法模式”以及“抽象工厂模式”的学习我们会很强的感受到这点。

posted on 2004-09-09 13:44 吕震宇 阅读(8181) 评论(22)  编辑 收藏

评论

#1楼 2004-09-09 15:47 灵感之源
有意思
 回复 引用   

#2楼 2004-09-09 16:49 myrat
文笔不错:D
 回复 引用   

#3楼 2004-09-09 17:39 AlleNny
嗯,是的,因为公司要求,我现在从C#回到了C,好痛苦啊,唉!
 回复 引用   

#4楼 2004-09-09 18:06 Yu
抬杠。

不知道要是这样开车怎么办。

#define Drive_GoGoGo if (pDrive != NULL)(*pDrive)()

void main(int argc, char *argvs[])
{
    void (*pDrive)(void) = NULL;

  if(argc < 2)
  {
    PrintHelp();
    return;
  }
  
  if(strcmp(argvs[1],"V")==0)
pDrive = &DriveVolvo;
  else if(strcmp(argvs[1], "B")==0)
pDrive = &DriveBench;
  else if(strcmp(argvs[1], "M")==0)
pDrive = &DriveMazda;
  else
    PrintHelp();

  Drive_GoGoGo;

  return;
}
 回复 引用   

楼上也有点...直接。
 回复 引用   

#6楼 2004-09-09 18:49 吕震宇
@Yu

还是请出杀手锏“指向函数的指针”了。实际上,我在上面说了C语言远比上面写到的强大,不然UNIX怎么会用C语言写的呢?

我想表述的只是面向对象的一个侧面,还是不要太叫真吧。
 回复 引用   

@Yu:

实质上面向对象中多态的内部实现机制归根结底不就是这样的吗?可以说,你这段代码还是利用了多态的思想在非面向对象的语言中改善了代码的结构呀,正好应了搂主的初衷。:)
 回复 引用   

#8楼 2004-09-09 22:58 leak
写的非常好,不过我个人觉得这个例子还不能非常好的直接的体现工厂的概念!
 回复 引用   

#9楼 2004-09-10 07:11 吕震宇
@leak

所以题目叫“工厂模式前传”,也就是学习工厂模式前需要打下的一些基础,否则学习工厂模式时很容易忽视一些细节问题。

至于工厂的概念需要具体在“工厂模式”中学习。
 回复 引用   

#10楼 2004-09-10 09:02 Yu
hi,吕震宇

   最开始已经是在说抬杠了(*^_^*),并无意说其他的。:P
   我只是觉得你说的知识本身是不依存于语言,甚至不依存于面向对象。
或者说这些想法也许源于面向对象或者与之有着千丝万缕的联系,但是只要
想法一致,在任何地方都可以应用。

hi,JGTM'2004 [MVP]

   是的是的。:)
 回复 引用   

#11楼 2004-09-10 14:44 寒枫天伤
怎么联系你?
有一些关于对象理论的问题想与你探讨一下,我的MSN:William_Fire@msn.com
 回复 引用   

#12楼 2004-09-10 14:48 吕震宇
@寒枫天伤

我的MSN:zhenyu_lv@hotmail.com

不过这周末要去北京,回来再联系 :)
 回复 引用   

#13楼 2005-01-06 17:12 Stephen      
把多态写得非常形象!
 回复 引用 查看   

#14楼 2005-01-31 22:15 frogman
吕老师的课程结合实际,通俗易懂呀
 回复 引用   

@吕震宇
一个字“好”,对我目前正在学习的设计模式很有帮助,谢谢!
如有机会还得多多向你请教!
 回复 引用   

#16楼 2005-11-26 14:40       
我正在学习设计模式,希望能和大家多多交流.
 回复 引用 查看   

#17楼 2005-12-12 12:32 陈涛[未注册用户]
吕老师写得设计模式使我受益非潜,感谢!
 回复 引用   

开放封闭原则要求对修改封闭,对扩展开放。在上面的两个例子种,方法二没有很好遵循开放封闭原则,当添加新类型汽车后,不得不修改代码以适应这种改变。而方法一具有很强的适应性,只需要给Car对象添加一个子类就可以了,客户由于只知道有“车”,所以加一种新车后,根本不需要改变客户端代码。因此也提高了系统的可维护性。

工厂模式中之所以引入“工厂”的概念,而抛弃直接使用 new 实例化对象,其中一个根本的原因也在于此。通过对“简单工厂模式”、“工厂方法模式”以及“抽象工厂模式”的学习我们会很强的感受到这点。


对这段,我想请问,其中的"根本不需要改变客户端代码"的意思,说下,我的理解先,客户端:调用产品类或调用工作类的地方.这样的话,在工厂方法模式中,产品有加入新车,那么工厂子类也应有新车的相应子类,现在在客户端,把调用新车变为调用新车的工厂子类,也就是说客户端一样要有对新对象的调用,在代码上也应有体现,那么,不改变客户端代码,怎么讲?
如前例所示:加入新车bench后,方法1客户端:Car c = new Bench();
c.Drive();方法2客户端:Bench b = new Bench();
b.Drive();都有代码的加入
 回复 引用   

#19楼 2006-05-18 12:55 盼答[未注册用户]
若三辆车分别是:普通车、洒水车、垃圾车,而洒水车和垃圾车均继承自普通车,自身却有洒水()和装载垃圾()两个方法。这样的关系若要符合上述原则,该如何设计?
 回复 引用   

#20楼 2006-06-12 23:04 y2er[未注册用户]
看那一堆的if--else,就知道哪个都不符合“开放-封闭”原则。
另外,同意YU的看法。
 回复 引用   

此种方法,根本还没体现工厂模式,只是把抽象的意思表达出来了

Car c = new Bench();//这里不是照样还要变
c.Drive();
 回复 引用