关于c++:谈-C17-里的-Factory-模式

7次阅读

共计 12184 个字符,预计需要花费 31 分钟才能阅读完成。

本文不适宜初学者,你应该曾经对 Factory 模式有所理解,你对于 C++17 的常见个性也不生疏。

Factory Pattern

回顾下工厂模式,并思考实现一个通用的工厂模板类以达成业务端低代码的指标。

FROM: Refactoring Guru

实践

Factory 模式是 Creational Patterns 中的一种。

创立型模式

所谓的创立型模式,次要蕴含这几种:

  • Abstract factory 形象工厂模式。一组具备同一主题的对象创立工厂被独自封装起来,而多组不同的对象工厂具备对立形象的创立接口,该形象的创立接口即为形象工厂。
  • Builder 构建者模式。目标是为了结构出一个简单对象,有必要将其蕴含的各种属性分门别类地顺次设定,以便令结构过程易于治理。个别采纳链式调用形式,而在属性结构结束之后,一个发令枪(例如 build())将指挥该简单对象被最终结构为实例。
  • Factory method 古典的工厂模式。工厂办法模式。个别有一个动态的 create() 以便创建对象的实例。
  • Prototype 原型模式。通过复制一个已有类型的形式创立新实例,即 clone()
  • Singleton 单例模式。全局只有一个对象实例。

以上为 GoF 的经典划分。不过,简直三十年过来了,当初还有更多的创立型模式:

  • 和 Builder 略有不同的 生成器模式(generator pattern)
  • 提早初始化模式。Kotlin 中的 lazyinit 关键字是它的一种语言性反对。
  • 对象池模式。如果对象的创立相当耗时或者耗资源,那么一次性提前创立一组对象,须要时取用,用完后放回池子里。
  • 等等。

工厂模式

本文中提到工厂模式时,泛指 Factory Method,Factory,Abstract Factory 等等。综合起来看,工厂模式是指借助于一个厂房 Factory 来创立产品 Product 的某种编程范式。其目标是为了让消费者(业务端代码)不去关怀产品怎么制作进去的(简略地通过 Factory.create() 就可能失去),只需间接关怀怎么应用产品就行了。

从另一角度看,工厂模式具备这样的个性:我晓得工厂可能造清污产品,然而肥皂还是香皂就无所谓了,我想有点香味的,工厂就将会造香皂给我,我没有要求的,工厂造给我的可能就是肥皂了。也就是说,接口是那个样子,但工厂将会造出来的肯定合乎这个接口约定,但到底是那个类的实例就不肯定了(通常会由创立参数来决定)。

在编程实际上,工厂模式总是随同着一个产品根类,这是一个接口类,通常蕴含一系列形象办法作为业务端操作接口。对于简单的产品族来说则在该接口类的根底上会持续派生若干品类。

Factory Method

最古典的工厂模式是 Factory Method,由 GoF 首次阐述的一种 Pattern。

以 Point 为例,

namespace hicc::dp::factory::inner {

    class Transport {
    public:
        virtual ~Transport() {}
        virtual void deliver() = 0;};

    class Trunk : public Transport {
        float x, y;

    public:
        explicit Trunk(double x_, double y_) {x = (float) x_, y = (float) y_; }
        explicit Trunk(float x_, float y_) {x = x_, y = y_;}
        ~Trunk() = default;
        void deliver() override { printf("Trunk::deliver()\n"); }
        friend std::ostream &operator<<(std::ostream &os, const Trunk &o) {return os << "x:" << o.x << "y:" << o.y;}
        static std::unique_ptr<Trunk> create(float x_, float y_) {return std::make_unique<Trunk>(x_, y_);
        }
    };

    class Ship : public Transport {
        float x, y;

    public:
        explicit Ship(double x_, double y_) {x = (float) x_, y = (float) y_; }
        explicit Ship(float x_, float y_) {x = x_, y = y_;}
        ~Ship() = default;
        void deliver() override { printf("Ship::deliver()\n"); }
        friend std::ostream &operator<<(std::ostream &os, const Ship &o) {return os << "x:" << o.x << "y:" << o.y;}
        static std::unique_ptr<Ship> create(float r_, float theta_) {return std::make_unique<Ship>(r_ * cos(theta_), r_ * sin(theta_));
        }
    };

} // namespace hicc::dp::factory::inner

void test_factory_inner() {
    using namespace hicc::dp::factory::inner;

    auto p1 = create_transport<Trunk>(3.1, 4.2);
    std::cout << *p1.get() << '\n';
    p1->deliver();

    auto p2 = create_transport<Ship>(3.1, 4.2);
    std::cout << *p2.get() << '\n';
    p2->deliver();

    auto p3 = Ship::create(3.1, 4.2);
    std::cout << *p3.get() << '\n';
    p3->deliver();}

依照古典的表述,工厂办法模式倡议应用非凡的 工厂 办法代替对于对象构造函数的间接调用(即应用 new运算符)。不必放心,对象仍将通过 new运算符创立,只是该运算符改在工厂办法中调用罢了。工厂办法返回的对象通常被称作“产品”。

然而在古代 C++ 中,回绝 new delete 的显式呈现(甚至也回绝 指针的呈现),所以下面的表述也须要微微调整一下。可能会像这样:

static std::unique_ptr<Ship> create_ship_2(float r_, float theta_) {return std::make_unique<Ship>(r_ * cos(theta_), r_ * sin(theta_));
}
static std::unique_ptr<Trunk> create_trunk_2(float x_, float y_) {return std::make_unique<Trunk>(x_, y_);
}

应用时也要微调,但也能够简直没变动(因为智能指针的封装能力):

    auto p3 = f.create_ship_2(3.1, 4.2);
    std::cout << *p3.get() << '\n';
    p3->deliver();

如此。

评讲

工厂办法模式的特点在于将创立一个子类对象的代码集中放到一个独立的工厂类中来进行治理,对于每个子类来说通常会有一个专门的办法绝对应,这也是 Factory Method 一名的由来。

工厂办法模式的劣势很显著,劣处也不少。

其劣势在于集中的创立点,易于保护,若有设计调整或者需要变更都能够很容易地甚至是不用调整业务代码。调用工厂办法的代码(以下均称为 业务 代码)无需理解不同子类返回理论对象之间的差异。业务代码将所有产品视为形象的 Point。业务代码晓得所有 Point 对象都提供 at 办法,然而并不关怀其具体实现形式。

其劣势在于较为生硬,新增产品的话将会带来比拟糟的结果。一般来讲,总是会有若干反复的 create() 办法和新产品相配套,这使得类库版本迭代成为不大不小的问题,又或者会导致用户无奈自行添加产品实现类。

改良

在 Modern C++ 中借助于模板变参(C++17 or later)和完满转发有可能可能削减创立办法,将其糅合为一个繁多的函数:

class classical_factory_method {
  public:
  template<typename T, typename... Args>
  static std::unique_ptr<T> create(Args &&...args) {return std::make_unique<T>(args...);
  }
};

应用时像这样:

    auto p4 = f.create<Ship>(3.1, 4.2);
    std::cout << *p4.get() << '\n';
    p4->deliver();

这会使得编码上很简练,对于增加一个新产品的状况也很敌对,简直无需对 factroy 做任何批改。

稍后咱们还将提供一个更加改善的 factory 模板类,能够解决这些问题。

Inner

将工厂办法模式的实现稍微调整一下,把创立新实例的办法扩散到每个产品类当中,能够形成 Inner 形式的 Factory Method。个别地可能仅仅将其视为 Factory Method 的变体。

namespace hicc::dp::factory::inner {

    class Transport {
    public:
        virtual ~Transport() {}
        virtual void deliver() = 0;};

    class Trunk : public Transport {
        float x, y;

    public:
        explicit Trunk(float x_, float y_) {x = x_, y = y_;}
        ~Trunk() = default;
        void deliver() override { printf("Trunk::deliver()\n"); }
        friend std::ostream &operator<<(std::ostream &os, const Trunk &o) {return os << "x:" << o.x << "y:" << o.y;}
        static std::unique_ptr<Trunk> create(float x_, float y_) {return std::make_unique<Trunk>(x_, y_);
        }
    };

    class Ship : public Transport {
        float x, y;

    public:
        explicit Ship(float x_, float y_) {x = x_, y = y_;}
        ~Ship() = default;
        void deliver() override { printf("Ship::deliver()\n"); }
        friend std::ostream &operator<<(std::ostream &os, const Ship &o) {return os << "x:" << o.x << "y:" << o.y;}
        static std::unique_ptr<Ship> create(float r_, float theta_) {return std::make_unique<Ship>(r_ * cos(theta_), r_ * sin(theta_));
        }
    };

} // namespace hicc::dp::factory::inner

有时候,没有集中的 factory class 兴许是失当的。这就是为什么咱们要强调 inner factory method pattern 的起因。

改良

为了利用一个集中的 factory class 的长处(集中统一的入口能够有利于代码保护、业务层级的保护——例如埋点),即便是 Inner FM Pattern 也能够提供一个 helper class/function 来做具体的调用,咱们能够思考 Modern C++ 的 SFINAE 个性。

在元编程的世界里,既然所有 product 对象总是会实现 create(以及 clone 办法),那么咱们能够很容易地利用 SFINAE 技术(以及 C++17 的参数包开展,模板变参,折叠表达式)来结构它的实例:

template<typename T, typename... Args>
inline std::unique_ptr<T> create_transport(Args &&...args) {return T::create(args...);
}

而且甚至无需什么高超的编码技巧(只管学习技巧时会有肯定的苦楚性,但代码写进去后看起来是无需什么技巧的,所有都显得平平无奇,合乎直觉,少数人都能了解,对于 reviewers 也十分敌对)。

应用时会是这样子:

void test_factory_inner() {
    using namespace hicc::dp::factory::inner;
    
    auto p1 = create_transport<Trunk>(3.1, 4.2);
    std::cout << *p1.get() << '\n';
    p1->deliver();

    auto p2 = create_transport<Ship>(3.1, 4.2);
    std::cout << *p2.get() << '\n';
    p2->deliver();}

当然也能够这样子:

    auto p3 = Ship::create(3.1, 4.2);
    std::cout << *p3.get() << '\n';
    p3->deliver();

看起来也还行。

评讲

综合来看 Factory Method 的几种状态,不仅仅是 Inner 形式的,也包含正统的 Factory Method 模式。

它们的共性在于抽出了一个显著的 creator 作为办法,这个办法被放在何处权且不管,但这么做的目标首先是为了解除创建者和具体产品之间的严密耦合,于是咱们能够采纳多种手段来做到更好的拆散。此外,因为咱们往往会将产品创立代码集中搁置在某个地位,例如 factory class 中,所以代码更容易保护,这也是繁多职责准则的指标。转而钻研业务代码,咱们会发现在必要的改良后新增产品类型甚至能够简直不影响到业务代码的变更,所以这一方面 Factory Method 也是有劣势的。

至于毛病,在通过了前文提及的几种 Modern C++ 根底上的改良之后,基本上没有毛病了,或者,子类可能太多太拆散,导致类继承体系有时候收缩到治理艰难,也算是一种毛病了。

Abstract Factory

因为这个命名的起因,咱们可能经常会将它和 C++ 的 Abstract class、Virtual Method,Polymorphic object 互相混同起来。

但实际上两者之间并无必然的因果关系。

好的例子难寻,所以上面咱们借用 Refactoring Guru 的范例 来做简略讲解,咱们举荐你在本文根底下来浏览 Refactoring Guru 的 罕用设计模式有哪些?

假如你正在开发一款家具商店模拟器。你的代码中包含一些类,用于示意:

  1. 一系列相干产品,例如 椅子 Chair沙发 Sofa咖啡桌 Coffee­Table
  2. 系列产品的不同变体。例如,你能够应用 古代 Modern维多利亚 Victorian装璜风艺术 Art­Deco 等格调生成 椅子 沙发 咖啡桌

所以,Abstract Factory 的特点就在于工厂能够以某种特定的格调对立地创立所有产品,而不是仅仅创立了产品就完事。这样的对于多种维度进行管制的能力,才是 Abstract Factory 的独有之处。

换而言之,另外思考,在 UI 控件上引入多种格调(Metro,Fluent,Apple,Material Design 等等),以及引入多种主题(红色,红色,暗色等等),这样的创立工厂,才是形象工厂。

除了多维度之外,进一步地推动这个了解,实际上椅子和沙发是齐全不同的两种产品,只不过它们都有“家具”这一共性,所以“木工工厂”这家形象工厂才会一起办了。也因而咱们须要留神到形象工厂的另一特色在于它可能创立多种系列的产品,椅子系列,桌子系列等等。

如此一来,形象工厂将能够创立:Point, Line, Arc, Ellipse, Circle, Rect, Rounded Rect 等等一系列的 Shapes,而且能够管制它们的填充色和边框线等等等等。

评讲

因为示例代码将会显著地宏大,所以没有示例代码。但想必经由前文表述后,你可能轻易透彻地了解什么是 AF,它和 FM 有何区别。

事实上,形象工厂和工厂办法模式有肯定的继承关系。将工厂办法进一步拓展就能够失去形象工厂。所以它们两者的界线也没有那么清晰。并且在大多数架构设计中,一开始还是简简单单的 inner,随后就会重构为一个具体的 factory class,从而造成 FM 模式,紧接着引入更多对象,持续重构对象以进行分组分类,抽取共性,引入主题,于是就开始演变为形象工厂模式。

小小结

和 generator/builder 模式不同,工厂模式总是一次性地立刻返回对象新实例,而前者更关怀怎么去分步骤地、循序渐进地结构出一个简单对象。

业务代码面对 factory class 就能够了,再加上产品体系中的各种接口抽象类。这就是所谓的,我无需晓得有哪些产品,我只需晓得提供什么原料怎么操作就能生产出符合要求的产品。

factory 模板类

咱们当然不会止步于前。

如果仅仅只是对几种古老的 Patterns 做出 Modern C++ 的改进型的话,到此本文就能够为止了。不过我还不想满足于此,因为这样的 Posts 应该曾经有不少了。所以说人就是会不满足的。

那么当初,不去回顾 factory pattern 的若干特点、长处、毛病,咱们将思路清空,从新来做一番设计。

咱们想要做一个通用的 Factory Pattern 的工厂模板类,目标是省去 create method 的编写,因为它经常是 boring 的反复的,此外咱们也不想总是要编写一个 factory class 的骨架,而是想要在有一组 products 类之后就能主动有一个配套工厂。

实现

首先来讲,一系列的 products 类实际上能够被用作模板的变参;

其次而言从一个类型名上、在编译期咱们曾经有一个工具可能获得类名(hicc::debug::type_name<T>())。

这样,咱们将可能结构一个 tuple,将类名和 creator 打包后堆放到 tuple 中。

之所以应用 tuple,是因为咱们想利用 C++17 std::tuple 的形参包开展语法。

如果不借助于 std::tuple,那么咱们须要这样的片段:

template<typename product_base, typename... products>
struct factory {
  template<typename T>
  struct clz_name_t {std::string_view id = debug::type_name<T>();
    T data;
  };
  auto named_products = {clz_name_t<products>...};
};

但这会引发一系列的编码问题。

或者兴许你认为应用 std::map<std::string, std::function<T()> > 是一种好想法?

无妨事,如果真的有趣味,你能够试试做相应的改写。

而当这么一个 tuple 在手之后,咱们能够依据传入的类名标识字符串来搜寻对应的对象的实例结构器,而后实现对象的实例化。

所以咱们能够有这样一个实现计划:

namespace hicc::util::factory {

  template<typename product_base, typename... products>
  class factory final {
    public:
    CLAZZ_NON_COPYABLE(factory);
    template<typename T>
    struct clz_name_t {std::string_view id = debug::type_name<T>();
      T data;
    };
    using named_products = std::tuple<clz_name_t<products>...>;

    template<typename... Args>
    static std::unique_ptr<product_base> create_unique_ptr(const std::string_view &id, Args &&...args) {std::unique_ptr<product_base> result{};

      std::apply([](auto &&...it) {((static_check<decltype(it.data)>()), ...);
      },
                 named_products{});

      std::apply([&](auto &&...it) {((it.id == id ? result = std::make_unique<decltype(it.data)>(args...) : result), ...);
      },
                 named_products{});
      return result;
    }
    template<typename... Args>
    static product_base *create(const std::string_view &id, Args &&...args) {return create_unique_ptr(id, args...).release();}

    private:
    template<typename product>
    static void static_check() {static_assert(std::is_base_of<product_base, product>::value, "all products must inherit from product_base");
    }
  }; // class factory

} // namespace hicc::util::factory

这个类的原始动机来自于 C++ Template to implement the Factory Pattern – Code Review Stack Exchange,但打消了原有的移植性问题,改善了构造函数的变参问题,所以当初是一个实在可用的版本,在 cmdr-cxx 中它也 是开箱即用的(cmdr::util::factory<...>)。

为了想要做成充分利用 C++17 新个性的代码,咱们尝试过多种计划。然而目前来讲,应用一个 T 的编译期固化的实例,并用 tuple 打包,是最简洁的。欢送在这里做多番尝试并探讨。

这么做的后果就是下面给出的实现代码,它的弱点在于不得不遍历 named_products{} 数组,这往往是一个蠢笨的伎俩,but 代码形态难看啊。此外对每个产品都会提前结构一个外部实例 T data,因为没有其余的无效伎俩来抽出 decltype(it.data),故而这个伎俩是被迫的,它的害处在于节约内存,升高启动速度,但运行时应用倒是没有副作用。

总的来看,设想你的产品类不应该会超出 500 个的吧,那么这些节约大略没什么不能够承受的。

应用

应用它也和平常的工厂模式的用法有一点点的不同,你须要在具现化 factory 模板类时指定产品的接口类以及所有产品类。

namespace fct = hicc::util::factory;
using shape_factory = fct::factory<tmp1::Point, tmp1::Point2D, tmp1::Point3D>;

这里会有一个强制要求你的全副产品类必须有对立的根类(即 product_base 抽象类),而后 factory class 能力通过这个根类的多态向你返回新产品的实例。

一个理论的例子能够是这样子:

namespace tmp1 {
    struct Point {virtual ~Point() = default;
        // virtual Point *clone() const = 0;
        virtual const char *name() const = 0;};

    struct Point2D : Point {Point2D() = default;
        virtual ~Point2D() = default;
        static std::unique_ptr<Point2D> create_unique() { return std::make_unique<Point2D>(); }
        static Point2D *create() { return new Point2D(); }
        // Point *clone() const override { return new Point2D(*this); }
        const char *name() const override { return hicc::debug::type_name<std::decay_t<decltype(*this)>>().data(); }
    };

    struct Point3D : Point {// Point3D() = default;
        virtual ~Point3D() = default;
        static std::unique_ptr<Point3D> create_unique() { return std::make_unique<Point3D>(); }
        static Point3D *create() { return new Point3D(); }
        // Point *clone() const override { return new Point3D(*this); }
        const char *name() const override { return hicc::debug::type_name<std::decay_t<decltype(*this)>>().data(); }
    };
} // namespace tmp1

void test_factory() {
    namespace fct = hicc::util::factory;
    using shape_factory = fct::factory<tmp1::Point, tmp1::Point2D, tmp1::Point3D>;
  
    auto *ptr = shape_factory::create("tmp1::Point2D");
    hicc_print("shape_factory: Point2D = %p, %s", ptr, ptr->name());
    ptr = shape_factory::create("tmp1::Point3D");
    hicc_print("shape_factory: Point3D = %p, %s", ptr, ptr->name());

    std::unique_ptr<tmp1::Point> smt = std::make_unique<tmp1::Point3D>();
    hicc_print("name = %s", smt->name()); // ok
}

长处

以上咱们认为是工厂模式的一个最优解,因为大量的琐事被去除或覆盖了,当初的新代码量曾经算是充沛少的状态了。

你可能留神到了这个 factory 模板类的 create 办法须要业务代码提供类名作为创立标识。这是特意设计的。因为咱们须要一个运行期变量而不是编译期的开展。设想一下组态软件的需要,你能够在一个下拉框当选茄子或者黄瓜,而后在绘图区域绘制一个元件,这时候你须要的就是一个运行期变量标识。

即便你并不想要这样的将来的拓展性,它也并不影响到你的业务代码。

不过你也齐全能够本人写一套类模板开展的,甚至偏特化的。

背景常识

想要进一步理解完满转发等等常识,能够看看 C++ 中的原位构造函数及完满转发 – 写咱们本人的 variant 包装类 和 C++ 中的原位构造函数 (2),但也应该去看 cppreferences 信息。

虚析构函数

留神虚析构函数的重要用处在于通过基类指针能够平安的 delete 多态实例,这是一个强制性的要求。如果你没有实现虚析构函数,那么 delete base 时可能无奈正确地开释一个多态对象。所以在绝大多数派生类体系中肯定要在基类申明虚析构函数,尔后,实践上编译器会为所有派生类生成相应的析构函数多态。

然而我从不在这些状况上来挑战编译器能力,而是所有派生类都显式地写出析构函数代码。除了防止潜在的移植性问题之外,显式的虚析构函数有利于升高代码阅读者的心智累赘,这是你应该做的。

一旦 base 申明了虚析构函数,那么派生类的析构函数不用带有 virtual 或者 override 关键字,这是主动的。

参见下面“应用”一节中的范例。

智能指针与多态

  • 在须要多态能力时,应该应用基类的指针,援用参考将不能执行多态操作

    Point* ptr = new Point3D();
    ptr->name();   // ok
    (*ptr).name(); // mostly bad
  • 基类指针的智能指针包装也能正确地多态:

    std::unique_ptr<Point> ptr = std::make_unique<Point3D>();
    smt->name(); // ok

    请留神细节。

  • 从派生类智能指针,通过 move 操作或者调用 release() 的形式能够转移裸指针到基类智能指针中。否则,应用下面的结构并立刻降级的形式(利用的是 std::unique_ptr<T,…> 的挪动构造函数,实际上隐含了 move 语义)。

闲聊指针和智能指针

然而那是弱者的借口而已。如果你连指针都用不好,反而津津有味地跟我谈 unique 还是 shared,那恐怕一个先天不足的评估是少不了了。

咱们都说会而不必,按需择用,那是高手熟能生巧的体现。不过如果是无奈会而不必,转而抉择不那么烧脑的伎俩,这就很难让人不去狐疑这是否只不过是酸葡萄了。

经验过 C98 前的苦楚和毒打的人往往有很好的避免指针失控的手法,这也是他们能力的体现。而事实上古代 C++ 也无奈避免指针问题,在 C++/C 这样的零碎级编程语言中议论指针你只有一个抉择,面对它,干掉它。

或者能够这么说,Modern C++ 提供了很多新的手法来帮忙咱们将指针误用可能覆盖起来,其实就是为了让菜鸡能够去做填空题。

但你别甘于做菜鸡。

所以

所以,在咱们的 factory 模板类中提供了 create(...)create_unique_ptr(...) 两个输入接口,无论你是哪一种格调的爱好者,指针派也好,智能指针派也好,或者中立派也好,能够按需取用而无需不爽。

小结

以上,个人观点,你轻易看看就好。

更欠缺的源码(打消潜在 warnings 的)参阅 hicc-cxx 源代码 factory.cc。

正文完
 0