02-工厂方法与抽象工厂模式

引子

接下来以造墙为例,探讨一下工厂方法模式。

首先,我们先定义一个墙。墙应该有:

  • 底部起止二维坐标。
  • 墙的海拔(相对于某个基线的高度或z坐标)。
  • 墙的高度。

代码如下:

class Wall
{
protected:
    Wall(Point2D start, Point2D end, int elevation, int height)
        : start(start), end(end), elevation(elevation), height(height) {}

private:
    Point2D start, end;
    int elevation, height;
};

接下来,为了让墙更接近实际,引入材质和厚度,创建新的墙类SolidWall

enum class Material
{
    brick, concrete
};

class SolidWall : public Wall
{
protected:
    SolidWall(Point2D start, Point2D end, int elevation, int height,
              int width, Material material)
        : Wall(start, end, elevation, height), width(width), material(material) {}
public:
    static SolidWall create_brick(Point2D start, Point2D end, int elevation, int height)
    {
        return {start, end, elevation, height, 120, Material::brick};
    }
    
    static unique_ptr<SolidWall> create_concrete(Point2D start, Point2D end, int elevation, int height)
    {
        return make_unique<SolidWall>(start, end, elevation, height, 375, Material::concrete);
    }
    
private:
    int width;
    Material material;
};

我们添加了两个静态方法,用于创建两种墙,可自己决定返回什么对象。这两种静态方法都被称为 工厂方法,它们强制用户创建指定类型而非任意类型的墙。

我们现在可以造两种墙了,但还无法判断墙与墙之间的相交。我们需要跟踪记录创建好的每一堵墙才能解决这个问题,但显然在SolidWall类里是无法完成这个任务的。该怎么办呢,请看 工厂方法模式

工厂方法模式

概念

factory-method-pattern

如图,定义一个用于创建对象的接口,让子类决定实例化哪一个类,工厂方法使一个类的实例化延迟到其子类,这就是工厂方法模式的思路。

有了工厂方法模式这一概念后,我们就可以把这两个工厂方法移动到WallFactory中,它负责生成并管理WallSolidWall

class WallFactory
{
public:
    static SolidWall create_brick(Point2D start, Point2D end, int elevation, int height)
    {
        const auto this_wall = new SolidWall(start, end, elevation, height. 120, Material::brick);
        // 防止墙相交
        for (const auto wall : walls)
        {
            if (auto p = wall.lock())
            {
                if (this_wall->intersects(*p))
                {
                    delete this_wall;
                    return {};
                }
			}
        }
        shared_ptr<SolidWall> ptr(this_wall);
        walls.push_back(ptr);
        return ptr;
    }
    
    static unique_ptr<SolidWall> create_concrete(Point2D start, Point2D end, int elevation, int height) 
    {/* ... */}
    
private:
    static vector<weak_ptr<Wall>> walls;
};

多态工厂方法

可以通过返回普通/智能指针的方式让工厂方法返回多态类型(不能返回值,因为按值传递会造成对象切割)。

例如我们定义一个枚举类WallType用于指定要建造的墙:

enum class WallType
{
    basic, solid_brick, solid_concrete
};

那么多态工厂方法可被定义如下:

static shared_ptr<Wall> create_wall(WallType type, Point2D start, Point2D end, 
                                    int elevation, int height)
{
    switch (type)
    {
        case WallType::solid_concrete:
            return make_shared<SolidWall>(start, end, elevation, height, 375, Material::concrete);
        case WallType::solid_brick:
            return make_shared<SolidWall>(start, end, elevation, height, 120, Material::brick);
		case WallType::basic:
            return shared_ptr<Wall>{new Wall(start, end, elevation, height)};
	}
    return {};
}

当使用多态工厂方法时,需要格外注意:调用任何没有使用关键字virtual限定的方法都将只会得到基类中该方法所定义的行为。例如,如果WallSolidWall都重载了ostream& operator<<,在不用dynamic_pointer_cast()的情况下,只会看到基类Wall的输出。

抽象工厂模式

概念

abstract-factory-pattern

有时候我们也许会参与整个族类对象的创建,这个场景十分罕见,抽象工厂模式是一种只在复杂系统中出现的模式,如上图。它 为创建一组相关或相互依赖的对象提供一个接口,而且无需指定它们的具体类

假设我们经营一家蜜雪冰城,有茶、咖啡等热饮。首先,定义热饮如下:

class HotDrink
{
public:
    virtual void prepare(int volume) = 0;
    // 其他方法省略...
};

有了抽象的热饮,就能定义热饮的 抽象工厂 了:

class HotDrinkFactory
{
public:
    virtual unique_ptr<HotDrink> make() const = 0;
    // 其他方法省略...
};

接下来定义一个具体的热饮,比如茶:

class Tea : public HotDrink
{
public:
    void prepare(int volume) override { cout << "Hot Tea " << volume << " ml OK.\n"; }
    // 其他方法省略...
};

咖啡的定义与此类似。有了具体的饮品,就能定义它们具体的工厂了。以咖啡为例:

class CoffeeFactory : public HotDrinkFactory
{
public:
    unique_ptr<HotDrink> make() const override { return make_unique<Coffee>(); }
    // 其他方法省略...
};

除了热饮外,还有冷饮,因此我们需要一个更高级的接口:

class DrinkFactory
{
public:
    DrinkFactory()
    {
        hot_factories["coffee"] = make_unique<CoffeeFactory>();
        hot_factories["tea"] = make_unique<TeaFactory>();
    }
    
    // 实现冷饮后就能用Drink了
    unique_ptr<HotDrink> make_hot_drink(const string& name, const int volume)
    {
        auto drink = hot_factories[name]->make();
        drink->prepare(volume);
        return drink;
    }
private:
    map<string, unique_ptr<HotDrinkFactory>> hot_factories;
};

一些寄巧

嵌套工厂

如果准备从一开始就让工厂和对象打交道,可以创建嵌套的工厂,即 在对象内部定义工厂。这里以墙的为例:

class Wall
{
// 省略一堆变量&方法
private:
    class BasicWallFactory
    {
	private:
        BasicWallFactory() = default;
    
    public:
        shared_ptr<Wall> create(Point2D start, Point2D end, int elevation, int height)
        {
            return shared_ptr<Wall>(new Wall(start, end, elevation, height));
        }
	};
    
public:
    static BasicWallFactory factory;
};

有关嵌套类BasicWallFactory有以下注意点:

  • 在类Wall外部其他地方无法直接初始化BasicWallFactory
  • 这里的工厂方法不是静态的

我们接下来可以按照以下方式来使用工厂:

auto basic = Wall::factory.create({0, 0}, {5000, 0}, 0, 3000);

函数式工厂

当我们使用术语“工厂”时,通常指的是以下两个概念之一:

  • 指一个类,这个类可以创建对象(就是上边的一堆例子)
  • 指一个 函数,当调用这个函数时,可以创建一个对象。

如下例所示,这个函数也是一种工厂:

template <typename T>
void consturct(function<T()> f)
{
    T t = f();
    // 使用t.....
}

将上面DrinkFactory的例子做修改,让它应用函数式工厂:

class DrinkFactory
{
public:    	
    DrinkFactory()
    {
        factories["tea"] = []{ return make_unique<Tea>(); }
        factories["coffee"] = []{ return make_unique<Coffee>(); }
    }
    
    unique_ptr<HotDrink> make_drink(const string& name) { return factories[name](); }
private:
    map<string, function<unique_ptr<HotDrink>()>> factories;
};

总结

使用工厂的好处包括:

  • 可以直到已经创建的特定类型的对象数量
  • 可以修改或完全替换整个对象的创建过程
  • 如果使用shared_ptr,还能获取该对象在其他地方被引用的数量

参考资料

  • 《C++20设计模式 可复用的面向对象设计方法》
  • 工厂方法模式 · Design Patterns (hypc-pub.github.io)