设计模式随笔-从“有病”说起(工厂模式前传) (转)
十年前,我有一个很有钱的朋友,他家有三辆汽车(VOLVO(沃尔沃)、BENCH(奔驰)、MAZDA(马自达)),还雇了司机为他开车。不过,这个人上车后跟司机说的话取决于他坐的车:当他坐上VOLVO后,会跟司机说“开沃尔沃车!”,坐上BENCH后他说“开奔驰车!”,坐上MAZDA后他说“开马自达车!”。
大家猜这个人怎么着?.....有病!
其实我这个朋友叫“C”。
注:我对C一直很虔诚,上大学时,C语言是我最喜爱的语言。而且它的功能要远比我在后面例子中描述的功能强大的多(毕竟还有杀手锏“指针”呢),我并不想让这段故事给C留下什么不好的印象,只是举例而已(BASIC和VFP什么的连举例资格都没有呢 )。
把上面的故事用C写下来的化就是(我在Tubro C++ 1.0下调试通过):
#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#改写上面的程序的话,我们可以将程序写成:
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#将本是很简单的问题搞复杂了呢?让我们分析一下:
- 从代码长度来看,显然C语言的代码长度要远少于C#的代码。两程序完成的是相同的功能。
- 从代码结构上看,C语言的结构也要比C#清晰,属于典型的结构化程序设计。
- 从“有病”的角度看,显然C语言程序“有病”,而C#程序更为容易接受。
C#程序通过对车的抽象,实现只需“开车”,就可以调用任何车的开车方法。这就是我们常说的“多态性”。将多态性浓缩到两行代码上,就是(以下简称方法一):
c.Drive();
不要小看这两行代码,隐藏在其中的深意还需要我们好好挖掘一下。
有人可能会问,不就是开车吗,开奔驰就是开奔驰,干吗要把奔驰赋值给车,然后调用车的开车,再通过多态性转而调用奔驰的开车。如此麻烦,不如直接就调用奔驰的开车(以下简称方法二):
b.Drive();
到底谁好谁坏,我们可以从两个角度来看这个问题:
一、从迪米特法则的角度来看:
(关于迪米特法则,请参考:C#设计模式(3))
迪米特法则可以简单的表述成最小知识原则,也叫做“使民无知”。一个对象应当对其它对象知道的越少越好。
如果客户在进行代码调用时,使用了方法二的方法,那么当不开奔驰转开沃尔沃时,必须将客户端所有Bench的代码改为Volvo。如果两个车都可能开的化,那么客户端不得不跟两个对象都打交道。
如果采用方法一的方法,客户只需要知道“车”就行了,反正车都可以开。至于什么车,客户并不关心,关键的是能开就行。这不但很好的应用了迪米特法则,同时也应用了里氏代换原则(参见:C#设计模式(2)):“一个子类可以替换掉父类”。这允许在客户不知情的情况下就可以代换不同类型的车。
二、从开放封闭原则的角度来看:
(关于开放封闭原则,请参考:C#设计模式(2))
开放封闭原则要求对修改封闭,对扩展开放。在上面的两个例子种,方法二没有很好遵循开放封闭原则,当添加新类型汽车后,不得不修改代码以适应这种改变。而方法一具有很强的适应性,只需要给Car对象添加一个子类就可以了,客户由于只知道有“车”,所以加一种新车后,根本不需要改变客户端代码。因此也提高了系统的可维护性。
工厂模式中之所以引入“工厂”的概念,而抛弃直接使用 new 实例化对象,其中一个根本的原因也在于此。通过对“简单工厂模式”、“工厂方法模式”以及“抽象工厂模式”的学习我们会很强的感受到这点。