如何正确实现C ++中的工厂方法模式

C ++中有这样一件事,使我长时间感到不舒服,因为我真的不知道该怎么做,尽管听起来很简单:

如何正确实现C ++中的Factory Method?

目标:使客户可以使用工厂方法而不是对象的构造函数实例化一些对象,而不会造成不可接受的后果和性能问题。

“工厂方法模式”是指对象内的静态工厂方法或其他类中定义的方法或全局函数。 一般来说,“将类X的实例化的正常方式重定向到构造函数以外的任何地方”的概念。

让我浏览一下我想到的一些可能的答案。


0)不要制造工厂,制造建设者。

这听起来不错(通常是最好的解决方案),但不是一般的补救措施。 首先,有时候对象构建是一个复杂的任务,足以证明它可以提取到另一个类中。 但即使把这个事实放在一边,即使对于使用构造函数的简单对象也是如此。

我所知道的最简单的例子是一个2-D Vector类。 这么简单,但棘手。 我希望能够从笛卡儿坐标和极坐标两者构造它。 显然,我做不到:

struct Vec2 {
    Vec2(float x, float y);
    Vec2(float angle, float magnitude); // not a valid overload!
    // ...
};

那么我的自然思维方式是:

struct Vec2 {
    static Vec2 fromLinear(float x, float y);
    static Vec2 fromPolar(float angle, float magnitude);
    // ...
};

其中,而不是构造函数,导致我使用静态工厂方法...这基本上意味着我以某种方式实现工厂模式(“班级成为自己的工厂”)。 这看起来不错(并且适合这种特殊情况),但在某些情况下会失败,我将在第2点中描述。请继续阅读。

另一种情况:试图通过一些API的两个不透明类型定义(例如不相关域的GUID,或者一个GUID和一个位域)来重载,类型在语义上完全不同(理论上是有效的重载),但实际上却是同样的事情 - 像无符号整数或void指针。


1)Java方式

Java很简单,因为我们只有动态分配的对象。 制造工厂和以下一样微不足道:

class FooFactory {
    public Foo createFooInSomeWay() {
        // can be a static method as well,
        //  if we don't need the factory to provide its own object semantics
        //  and just serve as a group of methods
        return new Foo(some, args);
    }
}

在C ++中,这转换为:

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
};

凉? 通常的确如此。 但是,这迫使用户只使用动态分配。 静态分配是C ++复杂的原因,但它也常常使它变得强大。 另外,我相信存在一些不允许动态分配的目标(关键字:嵌入式)。 这并不意味着这些平台的用户喜欢编写干净的OOP。

无论如何,不​​考虑哲学:在一般情况下,我不想强​​迫工厂的用户被限制为动态分配。


2)按价值回报

好的,所以我们知道1)当我们想要动态分配时很酷。 为什么我们不添加静态分配?

class FooFactory {
public:
    Foo* createFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooInSomeWay() {
        return Foo(some, args);
    }
};

什么? 我们不能通过返回类型重载? 哦,当然,我们不能。 所以让我们改变方法名称来反映这一点。 是的,我已经写了上面的无效代码示例,只是为了强调我不喜欢改变方法名称的必要性,例如因为我们现在不能实现与语言无关的工厂设计,因为我们必须更改名称 - 并且此代码的每个用户都需要记住实现与规范的不同之处。

class FooFactory {
public:
    Foo* createDynamicFooInSomeWay() {
        return new Foo(some, args);
    }
    Foo createFooObjectInSomeWay() {
        return Foo(some, args);
    }
};

好的...我们有。 这很丑陋,因为我们需要更改方法名称。 这是不完美的,因为我们需要两次编写相同的代码。 但一旦完成,它就会起作用。 对?

那么,通常。 但有时它不会。 在创建Foo时,我们实际上依赖于编译器为我们做了返回值优化,因为C ++标准对于编译器供应商来说是不够的,不必指定对象在什么时候创建以及何时在返回时复制C ++中的临时对象。 所以如果Foo的拷贝成本很高,这种方法是有风险的。

如果Foo根本不可复制呢? 那么,呃。 (请注意,在带有保证副本的C ++ 17中,对于上面的代码,不可复制不再是问题)

结论:通过返回对象来创建工厂确实是某些情况下的解决方案(例如之前提到的2-D向量),但仍然不是构造函数的一般替代品。


3)两相结构

有人可能提出的另一件事是分离对象分配和初始化的问题。 这通常会导致这样的代码:

class Foo {
public:
    Foo() {
        // empty or almost empty
    }
    // ...
};

class FooFactory {
public:
    void createFooInSomeWay(Foo& foo, some, args);
};

void clientCode() {
    Foo staticFoo;
    auto_ptr<Foo> dynamicFoo = new Foo();
    FooFactory factory;
    factory.createFooInSomeWay(&staticFoo);
    factory.createFooInSomeWay(&dynamicFoo.get());
    // ...
}

人们可能会认为它就像一种魅力。 我们代码中唯一支付的价格...

既然我已经写完所有这些,并且把它作为最后一个,我也一定不喜欢它。 :)为什么?

首先...我真诚地不喜欢两阶段建筑的概念,当我使用它时我感到内疚。 如果我用断言“如果它存在,它处于有效状态”来设计我的对象,那么我觉得我的代码更安全并且不太容易出错。 我喜欢这样。

不得不放弃那个约定,改变我的对象的设计,仅仅是为了制造它的工厂......好,很笨重。

我知道上述不会让很多人信服,所以让我给出一些更坚实的论点。 使用两相结构,您不能:

  • 初始化const或引用成员变量,
  • 将参数传递给基类构造函数和成员对象构造函数。
  • 而且可能还会有一些我现在无法想到的弊端,而且我从来没有觉得特别有必要,因为上面的要点已经说服了我。

    所以:甚至没有接近实施工厂的一个好的通用解决方案。


    结论:

    我们希望有一种对象实例化的方式,它可以:

  • 允许统一实例化,无论分配如何,
  • 为施工方法提供不同的,有意义的名称(因此不依赖于偏重过载),
  • 不会引入显着的性能下降,并且最好是严重的代码膨胀,特别是在客户端,
  • 一般,如:可能为任何职业介绍。
  • 我相信我已经证明,我提到的方式不符合这些要求。

    任何提示? 请给我提供一个解决方案,我不想认为这种语言不会让我正确实施这样一个小概念。


    首先,有时候对象构建是一个复杂的任务,足以证明它可以提取到另一个类中。

    我相信这一点是不正确的。 复杂性并不重要。 相关性是什么。 如果一个对象可以在一个步骤中构建(不像在构建器模式中),构造器是正确的地方。 如果你确实需要另一个类来执行这个工作,那么它应该是一个从构造函数中使用的帮助类。

    Vec2(float x, float y);
    Vec2(float angle, float magnitude); // not a valid overload!
    

    有一个简单的解决方法:

    struct Cartesian {
      inline Cartesian(float x, float y): x(x), y(y) {}
      float x, y;
    };
    struct Polar {
      inline Polar(float angle, float magnitude): angle(angle), magnitude(magnitude) {}
      float angle, magnitude;
    };
    Vec2(const Cartesian &cartesian);
    Vec2(const Polar &polar);
    

    唯一的缺点是它看起来有点冗长:

    Vec2 v2(Vec2::Cartesian(3.0f, 4.0f));
    

    但好处是你可以立即看到你使用的坐标类型,同时你不必担心复制。 如果你想复制,并且它很昂贵(当然,通过配置文件证明),你可能希望使用类似Qt的共享类来避免复制开销。

    至于分配类型,使用工厂模式的主要原因通常是多态。 构造函数不能是虚拟的,即使可以,也没有多大意义。 当使用静态或堆栈分配时,您不能以多态的方式创建对象,因为编译器需要知道确切的大小。 所以它只适用于指针和引用。 并且从工厂返回引用也不起作用,因为虽然技术上可以通过引用删除对象,但它可能相当混乱且容易出错,请参阅返回C ++引用变量的做法是否邪恶? 例如。 所以指针是唯一剩下的东西,它也包含智能指针。 换句话说,工厂在与动态分配一起使用时最为有用,因此您可以这样做:

    class Abstract {
      public:
        virtual void do() = 0;
    };
    
    class Factory {
      public:
        Abstract *create();
    };
    
    Factory f;
    Abstract *a = f.create();
    a->do();
    

    在其他情况下,工厂只是帮助解决你提到的超载问题。 如果有可能以统一的方式使用它们将会很好,但它可能不会造成太大的损害。


    简单工厂示例:

    // Factory returns object and ownership
    // Caller responsible for deletion.
    #include <memory>
    class FactoryReleaseOwnership{
      public:
        std::unique_ptr<Foo> createFooInSomeWay(){
          return std::unique_ptr<Foo>(new Foo(some, args));
        }
    };
    
    // Factory retains object ownership
    // Thus returning a reference.
    #include <boost/ptr_container/ptr_vector.hpp>
    class FactoryRetainOwnership{
      boost::ptr_vector<Foo>  myFoo;
      public:
        Foo& createFooInSomeWay(){
          // Must take care that factory last longer than all references.
          // Could make myFoo static so it last as long as the application.
          myFoo.push_back(new Foo(some, args));
          return myFoo.back();
        }
    };
    

    你有没有想过不使用工厂,而是很好地使用类型系统? 我可以想到两种不同的方法来做这种事情:

    选项1:

    struct linear {
        linear(float x, float y) : x_(x), y_(y){}
        float x_;
        float y_;
    };
    
    struct polar {
        polar(float angle, float magnitude) : angle_(angle),  magnitude_(magnitude) {}
        float angle_;
        float magnitude_;
    };
    
    
    struct Vec2 {
        explicit Vec2(const linear &l) { /* ... */ }
        explicit Vec2(const polar &p) { /* ... */ }
    };
    

    它可以让你写下如下内容:

    Vec2 v(linear(1.0, 2.0));
    

    选项2:

    你可以像STL一样使用“标签”来处理迭代器等。 例如:

    struct linear_coord_tag linear_coord {}; // declare type and a global
    struct polar_coord_tag polar_coord {};
    
    struct Vec2 {
        Vec2(float x, float y, const linear_coord_tag &) { /* ... */ }
        Vec2(float angle, float magnitude, const polar_coord_tag &) { /* ... */ }
    };
    

    第二种方法可以让你编写看起来像这样的代码:

    Vec2 v(1.0, 2.0, linear_coord);
    

    这也是很好的和富有表现力的,同时允许你为每个构造函数设计独特的原型。

    链接地址: http://www.djcxy.com/p/20363.html

    上一篇: How to implement the factory method pattern in C++ correctly

    下一篇: Does Uninstall action run during product upgrade?