首页 文章

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

提问于
浏览
272

C中的这一件事让我感到不舒服很长一段时间,因为我老实说不知道该怎么做,尽管听起来很简单:

如何正确实现C中的工厂方法?

目标:允许客户端使用工厂方法而不是对象的构造函数来实例化某个对象,而不会产生不可接受的后果和性能损失 .

“工厂方法模式”是指对象内部的静态工厂方法或另一个类中定义的方法,或全局函数 . 通常只是“将类X的实例化的正常方式重定向到构造函数之外的任何其他位置的概念” .

让我略过一些我想到过的可能答案 .


0)不要制造工厂,制造施 Worker 员 .

这听起来不错(实际上通常是最好的解决方案),但不是一般的补救措施 . 首先,有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类 . 但即便将这个事实放在一边,即使对于仅使用构造函数的简单对象,也常常不会这样做 .

我所知道的最简单的例子是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的两个opaque typedef(例如不相关域的GUID,或GUID和位域)重载,类型在语义上完全不同(所以 - 理论上 - 有效的重载)但实际上它们实际上是同样的事情 - 像无符号的int或void指针 .


1)Java Way

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)按 Value 返回

好的,所以我们知道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根本不可复制怎么办?好吧,doh . (请注意,在具有保证副本省略的C 17中,对于上面的代码,不可复制是不再有问题的)

结论:通过返回对象来 Build 工厂确实是某些情况的解决方案(例如前面提到的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 或引用成员变量,

  • 将参数传递给基类构造函数和成员对象构造函数 .

可能还有一些我现在无法想到的缺点,我甚至不觉得特别有责任,因为上面的要点已经说服了我 .

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


结论:

我们想要一种对象实例化的方法,它将:

  • 允许统一实例化,无论分配如何,

  • 为构造方法赋予不同的,有意义的名称(因此不依赖于副参数重载),

  • 没有引入显着的性能损失,并且最好是显着的代码膨胀,特别是在客户端,

  • 是通用的,如:可以为任何类引入 .

我相信我已经证明我提到的方式不符合这些要求 .

任何提示?请给我一个解决方案,我不想认为这种语言不会让我正确地实现这样一个琐碎的概念 .

10 回答

  • 35

    首先,有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类 .

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

    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's shared classes的东西来避免复制开销 .

    至于分配类型,使用工厂模式的主要原因通常是多态 . 构造函数可以很有意义 . 当使用静态或堆栈分配时,您也可以工作,因为虽然技术上可以通过引用删除对象,但它可能相当混乱且容易出错,例如,请参阅Is the practice of returning a C++ reference variable, evil? . 所以指针是唯一剩下的东西,包括智能指针 . 换句话说,工厂在与动态分配一起使用时最有用,所以你可以这样做:

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

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

  • 1

    简单工厂示例:

    // 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();
        }
    };
    
  • 10

    您是否考虑过根本不使用工厂,而是充分利用类型系统?我可以想到两种不同的方法来做这种事情:

    Option 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));
    

    Option 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);
    

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

  • 1

    您可以在以下网址阅读:#637368_

    最好的解决方案是“评论和讨论”,请参阅“不需要静态创建方法” .

    从这个想法,我做了一个工厂 . 请注意,我正在使用Qt,但您可以为std等效项更改QMap和QString .

    #ifndef FACTORY_H
    #define FACTORY_H
    
    #include <QMap>
    #include <QString>
    
    template <typename T>
    class Factory
    {
    public:
        template <typename TDerived>
        void registerType(QString name)
        {
            static_assert(std::is_base_of<T, TDerived>::value, "Factory::registerType doesn't accept this type because doesn't derive from base class");
            _createFuncs[name] = &createFunc<TDerived>;
        }
    
        T* create(QString name) {
            typename QMap<QString,PCreateFunc>::const_iterator it = _createFuncs.find(name);
            if (it != _createFuncs.end()) {
                return it.value()();
            }
            return nullptr;
        }
    
    private:
        template <typename TDerived>
        static T* createFunc()
        {
            return new TDerived();
        }
    
        typedef T* (*PCreateFunc)();
        QMap<QString,PCreateFunc> _createFuncs;
    };
    
    #endif // FACTORY_H
    

    样品用法:

    Factory<BaseClass> f;
    f.registerType<Descendant1>("Descendant1");
    f.registerType<Descendant2>("Descendant2");
    Descendant1* d1 = static_cast<Descendant1*>(f.create("Descendant1"));
    Descendant2* d2 = static_cast<Descendant2*>(f.create("Descendant2"));
    BaseClass *b1 = f.create("Descendant1");
    BaseClass *b2 = f.create("Descendant2");
    
  • 15

    我大多同意接受的答案,但现有答案中没有涉及C 11选项:

    • 按值返回工厂方法结果,和

    • 提供廉价的移动构造函数 .

    例:

    struct sandwich {
      // Factory methods.
      static sandwich ham();
      static sandwich spam();
      // Move constructor.
      sandwich(sandwich &&);
      // etc.
    };
    

    然后你可以在堆栈上构造对象:

    sandwich mine{sandwich::ham()};
    

    作为其他事物的子对象:

    auto lunch = std::make_pair(sandwich::spam(), apple{});
    

    或动态分配:

    auto ptr = std::make_shared<sandwich>(sandwich::ham());
    

    我什么时候可以用这个?

    如果在公共构造函数中,如果没有一些初步计算,就不可能为所有类成员提供有意义的初始化器,那么我可能会将该构造函数转换为静态方法 . 静态方法执行初步计算,然后通过私有构造函数返回值结果,该构造函数仅执行成员初始化 .

    我说“可能”因为它取决于哪种方法能够提供最清晰的代码而不会产生不必要的低效率 .

  • 5

    Loki有Factory MethodAbstract Factory . 两者都在Andei Alexandrescu的Modern C Design中进行了详细记录 . 工厂方法可能更接近你所看到的,尽管它仍然有点不同(至少如果内存服务,它需要您在工厂可以创建该类型的对象之前注册一个类型) .

  • 1

    我不会试图回答我的所有问题,因为我认为它太宽泛了 . 只是几个笔记:

    有些情况下,对象构造是一个复杂的任务,足以证明它被提取到另一个类 .

    那个班实际上是Builder,而不是工厂 .

    在一般情况下,我不想强迫工厂用户限制动态分配 .

    然后你可以让你的工厂将它封装在一个智能指针中 . 我相信这样你就可以吃蛋糕了 .

    这也消除了与按 Value 返回相关的问题 .

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

    确实 . 所有设计模式都有其(特定于语言)的约束和缺点 . 建议仅在它们帮助您解决问题时使用它们,而不是为了它们自己 .

    如果你是在“完美”的工厂实施后,那么,祝你好运 .

  • 40

    Factory Pattern

    class Point
    {
    public:
      static Point Cartesian(double x, double y);
    private:
    };
    

    如果你的编译器不支持返回值优化,抛弃它,它可能根本不包含太多优化...

  • 24

    我知道这个问题已在3年前得到解答,但这可能是你所寻找的 .

    谷歌几周前发布了一个库,允许简单灵活的动态对象分配 . 这是:http://google-opensource.blogspot.fr/2014/01/introducing-infact-library.html

  • 88

    这是我的c 11风格解决方案 . 参数'base'用于所有子类的基类 . 创建者,是std :: function对象来创建子类实例,可能是绑定到你的子类'静态成员函数'create(some args)' . 这可能不完美但对我有用 . 它有点'一般'的解决方案 .

    template <class base, class... params> class factory {
    public:
      factory() {}
      factory(const factory &) = delete;
      factory &operator=(const factory &) = delete;
    
      auto create(const std::string name, params... args) {
        auto key = your_hash_func(name.c_str(), name.size());
        return std::move(create(key, args...));
      }
    
      auto create(key_t key, params... args) {
        std::unique_ptr<base> obj{creators_[key](args...)};
        return obj;
      }
    
      void register_creator(const std::string name,
                            std::function<base *(params...)> &&creator) {
        auto key = your_hash_func(name.c_str(), name.size());
        creators_[key] = std::move(creator);
      }
    
    protected:
      std::unordered_map<key_t, std::function<base *(params...)>> creators_;
    };
    

    关于使用的一个例子 .

    class base {
    public:
      base(int val) : val_(val) {}
    
      virtual ~base() { std::cout << "base destroyed\n"; }
    
    protected:
      int val_ = 0;
    };
    
    class foo : public base {
    public:
      foo(int val) : base(val) { std::cout << "foo " << val << " \n"; }
    
      static foo *create(int val) { return new foo(val); }
    
      virtual ~foo() { std::cout << "foo destroyed\n"; }
    };
    
    class bar : public base {
    public:
      bar(int val) : base(val) { std::cout << "bar " << val << "\n"; }
    
      static bar *create(int val) { return new bar(val); }
    
      virtual ~bar() { std::cout << "bar destroyed\n"; }
    };
    
    int main() {
      common::factory<base, int> factory;
    
      auto foo_creator = std::bind(&foo::create, std::placeholders::_1);
      auto bar_creator = std::bind(&bar::create, std::placeholders::_1);
    
      factory.register_creator("foo", foo_creator);
      factory.register_creator("bar", bar_creator);
    
      {
        auto foo_obj = std::move(factory.create("foo", 80));
        foo_obj.reset();
      }
    
      {
        auto bar_obj = std::move(factory.create("bar", 90));
        bar_obj.reset();
      }
    }
    

相关问题