Switch语句,僵化的毒药

       在《Head First Design Patterns》一书中,用了大量的代码实例来讲解设计模式。该书的代码是用Java写的,Mark McFadden将其改作了C#版本的代码,下载地址:HeadFirstDesignPatternCSharp。在书中讲解Abstract Factory模式时,用PizzaStore来举例说明。这个例子非常生动,也有利于读者对Abstract Factory的理解。其中,PizzaStore的类图结构如下:

switch1.gif


       继承PizzaStore抽象类的子类NYPizzaStore和ChicagoPizzStore各自override了CreatePizza()方法,根据传入的字符串type,创建不同类型的Pizza。该方法在基类PizzaStore中被OrderPizza()方法调用。OrderPizza()方法的代码如下:

    public Pizza OrderPizza(string type)

    {

        Pizza pizza;

        pizza = CreatePizza(type);

 

        pizza.Prepare();

        pizza.Bake();

        pizza.Cut();

        pizza.Box();

        return pizza;

}

CreatePizza()方法为虚方法,在子类NYPizzaStore中,override该方法如下:

    protected override Pizza CreatePizza(string type)

    {

        Pizza pizza = null;

        IPizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

 

        switch (type)

        {

            case "cheese":

                pizza = new CheesePizza(ingredientFactory);

                pizza.Name = "New York Style Cheese Pizza";

                break;

            case "clam":

                pizza = new ClamPizza(ingredientFactory);

                pizza.Name = "New York Style Clam Pizza";

                break;

            case "pepperoni":

                pizza = new PepperoniPizza(ingredientFactory);

                pizza.Name = "New York Style Pepperoni Pizza";

                break;

        }

        return pizza;

}

然而在该方法中,却出现了讨厌的switch语句。switch语句虽然在条件判断中会被经常用到,但在本例中却不利于程序的扩展。例如增加一种Pizza,就必须修改各个PizzaStore的子类。毫无疑问,是switch语句导致了最终整个程序的僵化。那么,如何消除switch语句呢?仔细分析程序的结构,Pizza根据类型而分为CheesePizza, ClamPizza, PepperoniPizza,同时又根据PizzaStore的不同分为New York和Chicago的Pizza。这是一种类型的组合,如何对每种类型都创建一个类,这样需要定义的类对象太多。作者在解决这个问题时,是在各种类型的Pizza类的构造函数中,引入了IPizzaIngredientFactory,该工厂负责Pizza各种配料的制作(PizzaStore的不同,主要是有这些配料的制作方式不一样),这种方式将Factory模式和Bridge模式结合,保证了程序的可扩展。

在CreatePizza方法中,既然是根据type来创建不同的Pizza,也就说这个方法的责任就是用来创建Pizza的。那么,我们完全可以为程序再引入一个工厂类PizzaFactory(也可以用接口),用它来专门负责各种Pizza的创建,类图如下:

switch2.gif


在这些创建Pizza的方法中,还需要引入IPizzaIngredientFactory对象,以决定Pizza是New York Style,还是Chicago Style。代码如下:

    public abstract class PizzaFactory

    {

        public abstract Pizza CreatePizza(IPizzaIngredientFactory ingredientFactory);

}

    public class CheesePizzaFactory : PizzaFactory

    {

        public override Pizza CreatePizza(IPizzaIngredientFactory ingredientFactory)

        {

            return new CheesePizza(ingredientFactory);

        }

}

在引入该工厂类后,我们就可以对NYPizzaStore和ChicagoPizzaStore类的CreatePizza()方法做如下的修改:

    public class NYPizzaStore : PizzaStore

    {

         protected override Pizza CreatePizza(PizzaFactory pizzaFactory)

         {           

             IPizzaIngredientFactory ingredientFactory = new NYPizzaIngredientFactory();

              return pizzaFactory.CreatePizza(ingredientFactory);

         }       

}

    public class ChicagoPizzaStore : PizzaStore

    {

         protected override Pizza CreatePizza(PizzaFactory pizzaFactory)

         {           

             IPizzaIngredientFactory ingredientFactory = new ChicagoPizzaIngredientFactory();

              return pizzaFactory.CreatePizza(ingredientFactory);

         }       

}

在引入该工厂后,不仅消除了讨厌的switch语句,同时也使得CreatePizza()方法更加简单。要Create不同的Pizza,只需要将不同Pizza的Factory对象传递给CreatePizza()方法就可以了。相应的, PizzaStore抽象类的OrderPizza()方法中的string类型参数,也需要修改为PizzaFactory类型:

    public Pizza OrderPizza(PizzaFactory pizzaFactory)

    {

        Pizza pizza;

        pizza = CreatePizza(pizzaFactory);

 

        pizza.Prepare();

        pizza.Bake();

        pizza.Cut();

        pizza.Box();

        return pizza;

}

当我们增加新类型的Pizza时,仅需要在PizzaFactory中增加相应的Factory类,而PizzaStore的所有子类,都不需要做任何修改。显然这种做法,更有利于程序的扩展。

posted @ 2005-12-16 11:20 张逸 阅读(6739) 评论(26) 编辑 收藏

 回复 引用 查看   
#1楼 2005-12-16 11:46 Terrylee      
 我最近刚写了一篇随笔
 
请多指教:)

 回复 引用   
#2楼 2005-12-16 13:15 yuxs[未注册用户]
Switch语句具有高效率的能力,怎么能叫“僵化”?

很多时候感觉使用了更好的结构,但软件运行时性能在很多时候更为重要。

 回复 引用   
#3楼 2005-12-16 13:20 xiao_p[未注册用户]
I hate switch cery much!!~~

 回复 引用 查看   
#4楼 2005-12-16 13:29 闭关|那一剑的风情      
大量的if,else
以及switch
的确超级讨厌,
程序关键部位的swtich,更是扩展性能超差无比

 回复 引用 查看   
#5楼 2005-12-16 13:44 第一控制.NET      
@yuxs
性能确实还好。但是,程序的可读、扩展、维护就都不行了。必要的时候,用重构的方法引入设计模式,才是王道。

 回复 引用   
#6楼 2005-12-16 14:43 封[未注册用户]
不是所有的地方都需要、或“值得”扩展,所以Switch语句有其价值的地方。
 回复 引用   
#7楼 2005-12-16 14:51 Sharper[未注册用户]
很多程度上为了模式而模式!
难道添加 XXXPizza, XXXPiazzaFactory就很好?
方便了? 有扩展性了?

 回复 引用 查看   
#8楼 2005-12-16 15:01 第一控制.NET      
@封
我没说要消灭switch
@Sharper
如果需要扩展,确实就好了。有些东西你体会不到,但不代表不存在。

 回复 引用   
#9楼 2005-12-16 15:59 过客21[未注册用户]
哈哈,很多时候功能都有一堆BUG,还谈什么维护,不要把可读、扩展、维护放在第一位!
 回复 引用 查看   
#10楼[楼主] 2005-12-16 18:22 wayfarer      
这是个权衡的问题。以本例来看,引入switch说明他的设计还不完善。如果说不考虑扩展,那么本例中前面引入的工厂类,也没有必要了。所以,本文试图让这个方案更完美。

做设计时,适当的考虑扩展性,是完全有必要的。本例中,Pizza种类的新增在现实生活中,可能性非常大,这就需要考虑今后的扩展。

 回复 引用   
#11楼 2005-12-16 18:37 huangyiiiiii[未注册用户]
设计模式讨论的是如何抽象的问题, 和要不要抽象是两回事,有些回复的朋友可能没搞清楚
至于要不要抽象,就取决于会不会变化了

 回复 引用 查看   
#12楼 2005-12-16 18:57 补丁      
在惟一的访问点放置switch并不会对程序带来负面影响
对于上述例子,只有创建时有这种代码,在使用创建的对象时完全不会再看到类似的代码
这是没有问题的
switch并不是洪水猛兽
如果需求变化了,可能在其他地方也出现这种语句了,再应用重构手段修正就好了
为了所谓的完善的设计在这种细枝末节的地方纠缠实在是太浪费精力了

 回复 引用   
#13楼 2005-12-17 08:42 __登峰__[未注册用户]
要识别扩展点吧。如果是不需要的地方,何必穿上花俏的外裳呢?
要代价的

 回复 引用 查看   
#14楼 2005-12-17 18:02 横刀天笑      
用switch 真不能说是 “僵化的毒药”
实际上用 switch 这种方法就是 简单工厂模式
在一些小的项目中 而且 扩展性不是无限的项目中 比你的这种更有效
最起码 用switch 程序简单 明白 不容易出错 性能更佳

 回复 引用 查看   
#15楼 2005-12-17 18:12 dragonpig      
利与弊是可以相互转化的。关键在于如何运用。
工厂模式使用很平凡,建议得到语言级支持。

 回复 引用 查看   
#16楼 2005-12-17 22:30 Ying-Shen      
在修改后的方法中,现要求调用方给出具体工厂对象,实际是把从pizza名->factory对象的职责推了出去,换句话说如果OrderPizza接口还要是pizzaName的话,这里还需要维护一个映射表。这样一来abstract factory的成本就显得比较高了。

再一看要解决这里的问题有没有必要Abstract Factory啊?
我的理解是Factory系列设计是对对象创建过程的抽象,Factory method封装了根据输入要求创建对应产品的这个具体过程。但是当创建产品的具体过程也出现多样化的时候,就需要增加一层抽象,就变成了abstract factory. 可见abstract factory主要是针对产品创建过程的变化进行封装。这里产品的创建方式就switch的用法看来就一种,new出来。所以这里用factory method就可以了。这里就为了创建不同派生类的对象而使用abstract factory, 这种做法更像是在C++程序里面因为缺乏类型信息的一种妥协设计。在Java/C#中则完全没有必要,只要维护一个pizzaName -> pizzaClassName的映射表,然后用Activator.CreateInstance就可以解决hard-code switch的问题,当然Activator.CreateInstance的本质也是个Factory Method,但是这个不用我来写:)。

还有就是这里把Pizza类设计为抽象类,通过派生CheesePizza,ClamPizza PepperoniPizza,这种设计本身就不正交,扩展起来有组合爆炸的倾向,例子里面使用abstract factory 结果Pizza的类型爆炸也波及到PizzaFactory类层次中,增加了不少的garbage code. 记得以前《模式的乐趣》里面还专门讲过汉堡的组合爆炸问题。

 回复 引用   
#17楼 2005-12-29 08:55 xxx[未注册用户]
这个地方的设计有点儿别扭,你通过什么方式确定要传递哪个具体的PizzaFactory对象给protected override Pizza CreatePizza(PizzaFactory pizzaFactory)?毫无疑问地,你可能会在其它地方引进类似switch这样的语句。到不如直接在工厂里边的CreatePizza方法中直接用switch语句,这样其实并不带来太大的维护性问题,因为只需要在一个最明显的地方做修改。

 回复 引用 查看   
#18楼 2007-03-21 18:34 YanziMyWife      
你这里应该是结合使用了建造者模式而不是桥接模式吧??而且就这样也消除不了switch吧??(我个人认为工厂方法就不是为解决你所说问题的)
 回复 引用   
#19楼 2007-07-01 15:28 fishbone[未注册用户]
【引用】# re: Switch语句,僵化的毒药 2005-12-29 08:55 xxx
这个地方的设计有点儿别扭,你通过什么方式确定要传递哪个具体的PizzaFactory对象给protected override Pizza CreatePizza(PizzaFactory pizzaFactory)?毫无疑问地,你可能会在其它地方引进类似switch这样的语句。到不如直接在工厂里边的CreatePizza方法中直接用switch语句,这样其实并不带来太大的维护性问题,因为只需要在一个最明显的地方做修改。 【引用】

严重同意,不知道在确定调用具体哪一个pizzaFactory的时候怎么去除switch

 回复 引用   
#20楼 2007-09-04 13:46 joe[未注册用户]
可以使用配置文件
 回复 引用 查看   
#21楼 2009-01-07 20:28 HCOONa      
挺好
 回复 引用 查看   
#22楼 2011-07-25 02:36 秋色烽火      
使用反射 使用配置文件 ,明确有这些技术的前提下,才能理解设计模式的精妙