设计模式之原型模式(Prototype)

原本在《设计模式--可复用面向对象团软件基础》(简称《设计模式》)书中介绍这个模式的时候选的例子太过复杂.倒不是因为GOF选材不好.而是本来这个模式就复杂,想要将该模式很好的应用的话,需要考虑的也较多.但这对模式的初学者并不好,往往是看着看着就迷茫了.而忘记的该模式本身想表达的意思.

我倒想以个简单的方式来说明这个模式,分享一下经验.希望可以抛砖引玉,让更多的朋友可以亲近,或是不再害怕模式.

1,概述.

在《设计模式》中有几个原则:

这便符合《设计模式》本书想带给大家的收获--创建可复用的软件. 原型(Prototype)这个模式给我们带来两个优势:

  1. 是尽量采用组合;
  2. 可以尽可能减少定义类的数量(注:多做多错,杨老师的口头禅,呵呵.)

如果一个类A仅依赖另一个类B,那么可以考虑使用原型(Prototype)模式.是不是说的有些迷糊,我们看一看实例.

2,举例说明.

我们拿做汉堡(Hamburger,也就是类似KFC的那种)为例.要做汉堡我们需要配料(Material)再加上两片面包.根据配料不同我们可以做出不同的汉堡,比如牛肉汉堡,蔬菜汉堡等等.

经过一段时间的思考,我们可能这样定义出表示配料(Material)的类,以及表示汉堡的类(Hamburger).类图如下:

从上图可以看出,当前我们定义了四个具体类(Concrete class).他们分别表示两个配料的类,和两种汉堡的类. 也许看看代码,更容易理解一些.现给出第一次设计的完整代码.

//Platform: WinXp + VC6.0
#include <iostream.h>
#include <stdio.h>
/*-----------------表示配料的类-----------------------*/
class Material
{
protected:
    char *materialName;
public:
    virtual void Feed() = 0;
    char *MaterialName()
    {
        return materialName;
    }
};
class Vegetable:public Material
{
public:
    virtual void Feed()
    {
        Clean();
        cout<<"Added the vegetable..."<<endl;
        materialName = "Vegetable";
    }
    void Clean() //清洗蔬菜
    {
        cout<<"Cleaning the vegetable. "<<endl;
    }

};
class Beef:public Material
{
public:
    virtual void Feed()
    {
        Roast();
        cout<<"Added the beef..."<<endl;
        materialName = "Beef";
    }
    void Roast() //牛肉是需要烤一下

    {
        cout<<"Roasting the beef."<<endl;
    }
};
/*-----------------表示汉堡的类-----------------------*/
class Hamburger
{
public:
    virtual void MakeHamburger() = 0;
};

class BeefHam:public Hamburger
{
private:
    Beef _beef;
public:
    virtual void MakeHamburger()
    {
        _beef.Feed(); //添加配料

        cout<<_beef.MaterialName()<<"汉堡已经做好,请慢用."<<endl;
    }
};
class VegeHam:public Hamburger
{
private:
    Vegetable _vegetable;
public:
    virtual void MakeHamburger()
    {
        _vegetable.Feed(); //添加配料

        cout<<_vegetable.MaterialName()<<"汉堡已经做好,请慢用."<<endl;
    }
};


void main()
{
    cout<<"开始制作牛肉汉堡."<<endl;
    BeefHam beefHam;
    beefHam.MakeHamburger();

    cout<<endl;
    cout<<"开始制作蔬菜汉堡."<<endl;
    VegeHam vegeHam;
    vegeHam.MakeHamburger();
}

输出结果如下:

开始制作牛肉汉堡.
Roasting the beef.
Added the beef...
Beef汉堡已经做好,请慢用.

开始制作蔬菜汉堡.
Cleaning the vegetable.
Added the vegetable...
Vegetable汉堡已经做好,请慢用.

这样设计的话,我们的复用性并不好.从UML图中我们可以看出.我们从Hamburger派生出的类依赖于Material的派生类即:VegetableBeef类.

如果这时我们需要再做另外一种鸡肉汉堡的话,我们必须添加两个类,其中一个类继承于Material,再一个继承于Hamburger.即添加以下代码:

//派生自Material
class Chicken:public Material
{
public:
    virtual void Feed()
    {
        HandleChicken();
        cout<<"Added the beef..."<<endl;
        materialName = "Chicken";
    }
    void HandleChicken() //处理鸡肉
    {
        cout<<"Roasting the beef."<<endl;
    }
};
//派生自Hamburger
class ChickenHam:public Hamburger
{
private:
    Chicken _chicken;
public:
    virtual void MakeHamburger()
    {
        _chicken.Feed(); //添加配料
        cout<<_chicken.MaterialName()<<"汉堡已经做好,请慢用."<<endl;
    }
};

之所以需要如此,是因为ChickenHam这个类,紧紧依赖于Material的派生类Chicken.这反映在类图上就是被标为黄色的两个派生类:

也许这时大家发现了些端倪,类乎派生自Hamburger的类,除了各自拥有的配料(beef,chicken,vegetable)不一样以外,其它的并没有什么不同.冒似可以自动化生成Hamburger的派生类.

如果你已想到这一步了,那离原型模式也就不远了.我们先给出原型模式的UML图示,让朋友们自己对号入座.并看看这带来优势--类似于自动生成一个具体(Concrete)的Hamburger类.这是一件美妙的事.

看一看,这个Prototype的图示.我们可以将配料类看做一个原型(Prototype),我们也为该原型定义一个virtual方法.让其可以Clone出一个具体的配料类.

这会带来一个好处:我们在Client位置,即Hamburger类,只需参数化传递需要Clone的配料类即可,而不必我们每次都派生出一个具体化(Concrete)的Hamburger类.看起来类图是这样(注意,我将MaterialHamburger类位置左右调换了一下)

从图上可以看出,我们新增的Chicken类并没有新增一个相应的Hamburger类,而是定义了一个纯虚函数Clone,让其返回原型类(些处指Vegetable,Chicken或Beef)的引用.通过这些引用,我们可以不用派生Hamburger而直接是在Hamburger的方法中引用(或者说是参考)原型对象(Prototype Object).

现在给出经过重新设计后,采用Prototype模式的完整Source.

//Platform: WinXp + VC6.0
#include <iostream.h>
class Material
{
protected:
    char *materialName;
public:
    virtual Material* Clone() = 0;
    virtual void Feed() = 0;
    char *MaterialName()
    {
        return materialName;
    }
};
class Vegetable:public Material
{
public:
    virtual Material* Clone()
    {
        return this;
    }
    virtual void Feed()
    {
        cout<<"Added the vegetable..."<<endl;
        materialName = "Vegetable";
    }
};
class Beef:public Material
{
public:
    virtual Material* Clone()
    {
        return this;
    }
    virtual void Feed()
    {
        cout<<"Added the beef..."<<endl;
        materialName = "Beef";
    }
};

/*--------此处新增一个Material派生类即可----------*/
class Chicken:public Material
{
public:
    virtual Material* Clone()
    {
        return this;
    }
    virtual void Feed()
    {
        HandleChicken();
        cout<<"Added the beef..."<<endl;
        materialName = "Chicken";
    }
    void HandleChicken() //处理鸡肉

    {
        cout<<"Roasting the beef."<<endl;
    }
};

class Hamburger
{
private:
    Material *_pMaterial;   //优先采用组合,而不是继承
public:
    Hamburger(Material *pMaterial):_pMaterial(pMaterial)
    {}
    void MakeHamburger();
};
void Hamburger::MakeHamburger()
{
    //注意此处clone可以是深拷贝(deep copy)
    Material *pPrototype = _pMaterial->Clone();
    pPrototype->Feed();
    cout<<pPrototype->MaterialName()<<"汉堡已经做好,请慢用."<<endl;
}

void main()
{
    //创建汉堡配料
    Beef beef;
    Vegetable vegetable;


    cout<<"开始制作牛肉汉堡."<<endl;
    Hamburger beefHam(&beef);
    beefHam.MakeHamburger();

    cout<<endl;
    cout<<"开始制作蔬菜汉堡."<<endl;
    Hamburger vegeHam(&vegetable);
    vegeHam.MakeHamburger();

    cout<<endl;
    cout<<"开始制作鸡肉汉堡."<<endl;
    /*新增一个配料,然后当做参数传递即可*/
    Chicken chicken;
    Hamburger chickenHam(&chicken);
    chickenHam.MakeHamburger();

}

3,总结.

Protetype模式,本身似乎可以看做一个类型生成器(Class Generator).但又跟工厂方法(Factory Method)有些区别,工厂方法所产生的类,是我们已经在代码里显示定义过了的.而Prototype模式模似了(Simulate)了一组相关的派生类.

原理简单但要是实施起来还是有很多地方需要细细考虑,单从原型类(Prototype class)的派生类(即,Concrete Prototype class)怎么样实现Clone这个方法来讲就非常的复杂.因为你不得不考虑"深拷贝"和"浅拷贝"(Shallow copy and deep copy)的情况.

模式本身并不难,想要实现的目的也很明显.但在实际的项目中应用模式,用好模式.那还是相当有难度的.当你有一堆较烂的设计,你可以尝试通过重构(Refactoring),真至找到很好的解决方案.这些解决方案中,肯定会有模式的身影,不信你试试...

@ 2008-01-28 08:00

Comments:

Sharing your thoughts: