关闭

由浅入深学“工厂模式”

1105人阅读 评论(1) 收藏 举报

1、  传授设计模式中存在的问题

我个人最近对设计模式中的工厂模式进行了比较深入的学习,通过学习感悟出现在很多设计模式传道者,在讲述设计模式的过程中存在一些问题,使得设计模式变得很难理解。设计模式本身很抽象,但是这些传道者在讲述的过程中,将一个抽象的东西讲的更抽象,从而使我们望而却步。有些人在讲述的时候,没有考虑读者的注意力。比如我在看《C#设计模式》的时候,在讲述抽象工厂模式的时候,直接就进入了一个示例,而且这个例子比较复杂,涉及了比较多的概念、术语,而且还有算法要处理。但是这些和要讲述的核心内容无关,我想要看懂他的示例就要将这个东西都搞懂,就分散了我的注意力。我个人总结,学习设计模式的方法是,首先找到一个突破口,比如可以先学习构造型模式中简单的模式,将它理解、熟练应用。通过对一、两个模式有一个深入的认识之后,再学习其它比较复杂一点的模式就容易多了,这是一种迭代的思想在学习中的应用。另外学习任何一种设计模式的过程应该是具体-抽象-再具体这个的一个过程。这句话的意思是首先通过一个比较具体一点的例子来帮助理解设计模式,在理解之后将你的理解扩展到解决这一类问题,上升到一定的理论高度。然后就是再到具体,也就是应用设计模式,应用理论解决自己遇到的实际问题。

 

2、学习工厂模式的预备知识:

首先声明这些预备知识并不是工厂模式仅仅需要,因为我先讲述工厂模式,所以在学习工厂模式之前将这些问题提出。

 2.1 Upcasting

Upcasting中文翻译有好几个,比如向上类型转换、向上转型、上溯造型。我个人比较喜欢向上转型这个翻译,即简单又含义明确。向上转型这个概念,我在Bruce Eckel在的Thinking in c++Thinking in Java中都看到过,我不是很确定这个概念是否是他提出来的。向上转型是将把一个派生类当作它的基类使用。我们将一个更特殊的类型转换到一个更常规的类型,这当然是安全的。派生类是基类的一个超集。它可以包含比基类更多的方法,但它至少包含了基类的方法。向上转型给我们带来的好处就是我们可以将不同的派生通过一种统一的方式进行处理。向上转型带来的弊端就是我们向上转型的过程会丢失派生类的接口。既然有向上转型,也就有向下转型即DownCasting我们在此不做详细讨论。下面使用一个例子来示例向上转型。

     public class Base

     {

         public void Test()

         {

              MessageBox.Show("OK");

         }

     }

     public class Derive:Base

     {}

     private void button1_Click(object sender, System.EventArgs e)

     {

         Base b=new Derive();

         b.Test();

     }

 在有名的OOD的设计原则中有一个叫做里氏代换原则(Liskov Substitution Principle, LSP)。它的实质也就是讲向上转型。它的内容是:任何接收父类型的地方,都应当能够接收子类型,换句话说如果使用的是一个基类的话,那么一定适用于其子类,而且程序察觉不出基类对象和子类对象的区别。LSP是继承复用的基石,只有当派生类可以替换掉基类,软件的功能不受到影响时,基类才能真正被复用。

 

2.2 多态

我不敢想象离开了多态后的设计模式是一个什么样子。什么是多态,我喜欢总结这样一句话来回答这个问题,“一个接口,多种实现”。注意这里的接口不仅仅表示Interface关键字,是广义上的接口。在C#中实现接口我们有两种途径,一种是借助继承来实现,一种是借助Interface来实现。

  

3、工厂设计模式理论

 3.1 概述

工厂模式具体包括了简单工厂、工厂方法、抽象工厂,它们是按照从简单到复杂的顺序排列的,属于设计模式中的创建型,其中简单工厂并不属于GOF23中模式。但是它是理解其它的工厂模式的一个很好的基础,所以很多人在讲述设计模式的时候会提到简单工厂模式。创建型模式关注的是对象的创建,创建型模式将创建对象的过程进行了抽象,也可以理解为将创建对象的过程进行了封装,作为客户程序仅仅需要去使用对象,而不再关心创建对象过程中的逻辑。

 3.2 不使用任何模式

我们现在有这样的一个设计,影像家电(VideoWiring)包括了DVDVCD。在基类VideoWiring中有PlayVideo方法,子类重载了这个方法。

 


我们如何来调用
PlayVideo进行播放呢。我们可以看到下面的代码可以实现。

 

public abstract class VideoWiring
{
    
public abstract string PlayVideo();
}


public class VCD: VideoWiring  
{
    
public override string PlayVideo()
    
{
        
return "正在播放播放VCD";
    }

}


public class DVD: VideoWiring  
{
    
public override string PlayVideo()
    
{
        
return "正在播放播放DVD";
    }

}

 

下面是调用对象的方法进行播放的代码:

dvd.PlayVideo();这样的语句。

        private void PlayVideo()
        
{
            DVD dvd
=new DVD();
            MessageBox.Show(dvd.PlayVideo());
            VCD vcd
=new VCD();
            MessageBox.Show(VCD.PlayVideo());
        }


上面的代码可以实现功能但是不好,为什么呢?类实现了多态,但是我们在调用的时候并没有利用多态。如果我们有很多的影像家电产品,就需要写很多的类似

下面是使用多态完成播放功能的代码:

        private void PlayVideo()
        
{
            VideoWiring vw;
            vw
=new DVD();
            Play(vw);
            vw
=new VCD();
            Play(vw);
        }

        
private void Play(VideoWiring vw)
        
{
            
string str=vw.PlayVideo();
            MessageBox.Show(str);
        }


无论是什么影像家电产品,我们都可以使用一个统一的方式进行播放,即vw.PlayVideo()。

 

我们再讨论一下,上面的代码存在的问题。虽然上的代码很短,应该不会有问题,但是我们定位的目标应该更高些,应该考虑怎样达到良好的封装效果,减少错误修改的机会。我们自然的应该考虑对象创建的问题了,能不能让不同的影像家电产品的创建方式相同,而且这个创建过程对使用者封装,也就是说让对象的创建象播放功能那样简单、统一。如果能够实现,会给我们的系统带来更大的可扩展性和尽量少的修改量。“哇!那该多好呀”。“不要羡慕了,来看看简单工厂模式,听说它能够实现”。

 3.3 简单工厂模式

我们使用简单工厂对上面的代码继续改进,根据上面的分析我们考虑对对象创建进行近一步的封装。使用一个类专门来完成对对象创建的封装,这个类我们称为工厂,因为它的作用很单一就生成出一个个的类。下面是一个工厂类的示例代码:

public class Create
{
    
public static VideoWiring factory(string  VideoName)
    
{
        
switch(VideoName)
        
{
            
case "DVD":
                
return new DVD();
            
case "VCD":
                
return new VCD();
        }

        
return null;
    }

}


这样我们的客户端代码又可以更加有效简洁了:

注意:在上面的两段代码示例中我们就已经使用了向上转型。首先注意在Create类的factory方法中使用了return new DVD();这样的语句,但是这个函数的返回值却是VideoWiring,DVD类的基类。所以我们的客户程序才可以使用VideoWiring vw=Create.factory("DVD")这样的语句。这样客户程序并不关心创建是如何完成的,以及创建的对象是什么,我们都可以调用基类统一的接口实现他们的功能。使用UML表示如下图所示:

        private void PlayVideo()
        
{
            VideoWiring vw
=Create.factory("DVD");
            vw.PlayVideo();
            vw
=Create.factory("VCD");
            vw.PlayVideo();
        }

 

 


我们将工厂模式推广到一般的情况,它的类图如下所示:

 


角色说明:

工厂类(Creator):根据业务逻辑创建具体产品,由客户程序直接调用。

抽象产品(Product):作为具体产品的基类,提供统一的接口,也是工厂类要返回的类型。

具体产品(Concrete Product):工厂类真正要创建的类型。上图中仅仅展示了一个具体产品,有多个产品的时候类似。

下面我们对简单工厂模式进行总结。使用简单工厂的好处是:1、充分利用了多态性不管什么具体产品都返回抽象产品。2、充分利用了封装性,内部产品发生变化时外部使用者不会受到影响。他的缺点是:如果增加了新的产品,就必须得修改工厂(Factory)抽象工厂模式可以向客户端提供一个接口,使得客户端在不必指定产品的具体类型的情况下,创建多个产品族中的产品对象。这就是抽象工厂模式的用意。

 3.4 工厂方法

有了简单工厂模式后,已经给我们带来了一些好处,但是还存在一些问题,如果我们又多了一个影像家电产品MP4之后,我们可以使MP4类从VideoWiring派生,但是却要修改Create类的代码使它能够生产出MP4这个产品来。不好的地方就在于,我们每次多一个产品的时候都需要修改Create而不是保持原来的代码不修改仅仅进行一种扩展。在Create类中修改不是每次都简单的多一个Case语句就能够解决问题。因为Create类中还封装了创建对象的逻辑,有可能还需要修改这些逻辑。这就违反了面向对象设计中一个很重要的原则“开-闭”原则。

 

“开-闭”原则the Open Closed Principle OCP

在面向对象设计中,如何通过很小的设计改变就可以应对设计需求的变化,这是令设计者极为关注的问题。开闭原则就是软件实体在扩展性方面应该是开放的而在更改性方面应该是封闭的。这个原则说的是,在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展。通过扩展已有的软件系统,可以提供新的行为,以满足对软件的新需求,使变化中的软件系统有一定的适应性和灵活性。已有的软件模块,特别是最重要的抽象层模块不能再修改,这就使得变化中的软件系统有一定的稳定性和延续性。因此在进行面向对象设计时要尽量考虑接口封装机制、抽象机制和多态技术。

 

前边设计(简单工厂)中存在的问题就是它分装了创建不同对象的逻辑,当有新的产品的时候不易扩展。在开闭原则的指导下我们考虑如何重新修改前边的设计,我们要尽量使用抽象机制和多态技术。我们放弃对创建不同对象的逻辑的封装,也采用类似产品的方式,抽象出抽象工厂,具体工厂,具体工厂从抽象工厂派生,每个具体工厂中生产一种具体的产品。“太棒了,告诉你,你的这个想法就是工厂方法模式”。

 

下面使用工厂方法模式修改前边的设计:

 

 

public abstract class Create
{
    
public abstract VideoWiring factory();
}


public class DVDCreate: Create 
{
    
public override VideoWiring factory()
    
{
        
return new DVD();
    }

}


public class VCDCreate: Create 
{
    
public override VideoWiring factory()
    
{
        
return new VCD();
    }

}

 

VideoWiring、DVD、VCD三个类的代码和前边的相同,下面我们看看在客户端如何使用。

下面我们考虑需要扩展一个新的产品MP4的时候如何处理。

        private void PlayVideo()
        
{
            VideoWiring dvd,vcd;
            Create dvdCreate,vcdCreate;
            dvdCreate
=new DVDCreate();
            dvd
=dvdCreate.factory();
            Play(dvd);
            vcdCreate
=new VCDCreate();
            vcd
=vcdCreate.factory();
            Play(vcd);
        }

   

 


我们来看看增加的代码:

public class MP4Create: Create 
{
    
public override VideoWiring factory()
    
{
        
return new MP4();
    }

}


public class MP4: VideoWiring 
{
    
public override string PlayVideo()
    
{
        
return "正在播放MP4";
    }

}


我们再看看客户端代码:

MP4的时候没有修改原来的代码,而仅仅是对原来的功能进行扩展系统便有了MP4这个产品的功能。

        private void PlayVideo()
        
{
            VideoWiring dvd,vcd;
            Create dvdCreate,vcdCreate;
            dvdCreate
=new DVDCreate();
            dvd
=dvdCreate.factory();
            Play(dvd);
            vcdCreate
=new VCDCreate();
            vcd
=vcdCreate.factory();
            Play(vcd);
    
            
//下面是新增的代码
            VideoWiring mp4;
            Create mp4Create;
            mp4Create
=new MP4Create();
            mp4
=mp4Create.factory();
            Play(mp4);

        }


我们可以看出使用了工厂方法模式后,很好的满足了开闭原则,当我们增加了一个新的产品

 

将工厂方法模式推广到一般情况:

 


角色说明:

抽象工厂(Creator):定义具体工厂的接口,所有的创建对象的工厂类都必须实现这些接口。

具体工厂(ConcreteCreator):具体工厂包含与应用密切相关的逻辑。复杂创建具体的产品。

抽象产品(Product):所有产品的基类。

具体产品(ConcreteProduct):实现抽象产品申明的接口。工厂方法模式所创建的每个对象都是某个具体产品的实例。

  

工厂方法模式的用意是定义一个创建产品对象的工厂接口,将实际创建工作推迟到子类中。工厂方法模式是简单工厂模式的进一步抽象和推广。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。在工厂方法模式中,核心的工厂类不再负责所有的产品的创建,而是将具体创建的工作交给子类去做。这个核心类则成为了一个抽象工厂角色,仅负责给出具体工厂子类必须实现的接口,而不接触哪一个产品类应当被实例化这种细节。这种进一步抽象化的结果,使这种工厂方法模式可以用来允许系统在不修改具体工厂角色的情况下引进新的产品。

3.5 抽象工厂模式

我们继续对影像家电产品的情形进行分析,我们已经可以使用工厂方法比较好的实现了产品的创建,但是在以前的分析中我们并没有考虑产品种类及生产厂家这样的问题。就拿DVD来说TCL可以生产、LG也生产等等很多厂家都生产。DVD是产品种类中的一种,产品种类这个概念在有些书上称为产品族。从另外一个角度来看TCL可以生产DVDVCD等等很多产品,这些产品在一起就可以构成一个产品结构。当我们考虑了这些问题后,提出了两个概念:产品种类、产品结构。我们在工厂方法中讨论的是一个个单一的产品的创建,如果我们对这个问题进行进一步的研究、拓展,就应该从单一的产品过度到多个产品种类,在工厂方法中我们考虑DVD是一个单一的产品,现在我们认为DVD是一个产品种类,有TCL生产的DVD,有LG生产的DVDVCD是另一个产品种类,有TCL生产的VCD,有LG生产的VCD。就这个问题我们重新分析,有两个产品种类分别是DVDVCD,有两个工厂是TCLLG,它们分别生产DVDVCD。我们使用下面的类图来表示:




 

DVD是抽象类它提供统一的接口,LGDVDTCLDVD是两个具体的类。VCDDVD类似。有一个抽象的工厂Create,从它派生了两个具体的类TCLCreateLGCreateCreate中提供了两个抽象方法factoryDVDfactoryVCD它们提供了两个接口,用于创建DVD产品和VCD产品。在TCLCreateLGCreate中实现这两个方法。这样TCLCreate就可以创建自己的DVDVCD,同样LGCreate也可以传经自己的产品。

下面是代码结构:

public abstract class Create

{

     public abstract DVD factoryDVD();

     public abstract VCD factoryVCD();

}

 

public class LGCreate: Create

{

     public override DVD factoryDVD()

     {

         return new LGDVD();

     }

 

     public override VCD factoryVCD()

     {

         return new LGVCD();

     }

}

 

public class TCLCreate: Create

{

     public override DVD factoryDVD()

     {

         return new TCLDVD();

     }

 

     public override VCD factoryVCD()

     {

         return new TCLVCD();

     }

}

 

public abstract class DVD

{

     public abstract string PlayVideo();

}

 

public class LGDVD: DVD

{

     public override string PlayVideo()

     {

         return "LG的DVD在播放";

     }

}

 

public class TCLDVD: DVD

{

     public override string PlayVideo()

     {

         return "TCL的DVD正在播放";

     }

}

 

public abstract class VCD

{

     public abstract string PlayVideo();

}

 

public class LGVCD: VCD

{

     public override string PlayVideo()

     {

         return "LG的VCD正在播放";

     }

}

 

public class TCLVCD: VCD

{

     public override string PlayVideo()

     {

         return "TCL的VCD正在播放";

     }

}

 

客户端使用抽象工厂代码如下:

          private void button1_Click(object sender, System.EventArgs e)

         {

              Create TCL,LG;

              TCL=new TCLCreate();

              LG=new LGCreate();

              PlayDVD(TCL);   //输出“TCL的DVD在播放”
 

             PlayDVD(LG);     //输出“LG的DVD在播放”

 

         }

          private void PlayDVD(Create create)

         {

              DVD dvd=create.factoryDVD();

              MessageBox.Show(dvd.PlayVideo());

         }

 

下面将抽象工厂模式推广到一般情况,它的类图如下所示:

 

抽象工厂:提供所有具体工厂的接口,与应用系统的具体商业逻辑无关。基本上为每一个产品种类提供一个创建方法。

具体工厂:具体负责创建产品结构中每个产品。它包含了创建不同产品的商业逻辑。它实现抽象工厂中的接口。

抽象产品:定义产品的共同接口。

具体产品:是客户需要创建的具体对象。

 

在工厂方法中每个工厂负责创建一个产品,在抽象工厂中每个工厂创建一系列产品。在上面举例中使用TCLLG这样的实际的工厂,在实际的应用中,往往是我们根据产品抽象了类,它们主要负责一系列产品的创建,将这些负责抽象工厂的类称为具体工厂,从这些具体工厂更进一步进行抽象,抽象出的工厂称为抽象工厂。下面我们看看抽象工厂模式的扩展。

 

抽象工厂的扩展包括了新增一个产品种类及新增一个工厂。当在现有的抽象产品下添加新的具体产品时我们仅仅需要增加一个新的工厂就可以。比如现在有了Haier(海尔)的DVDVCD,我们很容易就实现扩展,而且也满足了“开闭原则”。如下图所示:



 

当我们有了一个新的产品的增加的时候,就不能很好的满足“开闭原则”了,因为我们需要修改每个产出的方法从而是现有的工厂可以创建出新的产品。比如我们增加一个Mp4产品种类。

 

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:97007次
    • 积分:1345
    • 等级:
    • 排名:千里之外
    • 原创:28篇
    • 转载:28篇
    • 译文:0篇
    • 评论:21条
    最新评论
    Google Analytics