This article is not suitable for beginners, you should already have an understanding of Factory mode, and you are not unfamiliar with the common features of C++17.
Factory Pattern
Review the factory pattern, and consider implementing a generic factory template class to achieve the goal of low code on the business side.
FROM: Refactoring Guru
theory
Factory pattern is one of Creational Patterns .
Creation mode
The so-called creation mode mainly includes these types:
- Abstract factory Abstract factory pattern. A group of object creation factories with the same theme are individually encapsulated, and multiple groups of different object factories have a unified abstract creation interface, and the abstract creation interface is an abstract factory.
- Builder builder mode. The purpose is to construct a complex object. It is necessary to set the various attributes it contains in order to make the construction process easy to manage. Generally, a chain call method is used, and after the attribute construction is completed, a starting gun (such as build()) will direct the complex object to be finally constructed as an instance.
- Factory method Classic factory method. Factory method pattern. Generally there is a static create() in order to create an instance of an object.
- Prototype prototype model. Create a new instance by copying an existing type, namely clone()
- the Singleton singleton. There is only one object instance globally.
The above is the classic division . However, almost thirty years have passed, and now there are more creative models:
- Generator pattern slightly different from Builder
- Delayed initialization mode. The lazyinit keyword in Kotlin is a kind of language support.
- Object pool mode. If the creation of objects is time-consuming or resource-consuming, then create a group of objects in advance at one time, fetch them when needed, and put them back in the pool after use.
- and many more.
Factory mode
When referring to the factory model in this article, it generally refers to Factory Method, Factory, Abstract Factory, etc. Taken together, the factory model refers to a certain programming paradigm in which a product is created with the help of a factory. The purpose is to let consumers (business-side code) not care about how the product is made (simply through Factory.create() to get it), and only need to directly care about how to use the product.
From another point of view, the factory model has such characteristics: I know that the factory can produce cleansing products, but it doesn’t matter if soap is soap or soap. If I want a bit of fragrance, the factory will make soap for me. I didn’t ask for it. Maybe the factory made me soap. In other words, the interface looks like that, but the factory will make it must conform to this interface convention, but it is not necessarily an instance of that class (usually determined by the creation parameters).
In programming practice, the factory pattern is always accompanied by a product root class, which is an interface class, which usually contains a series of abstract methods as business-side operation interfaces. For complex product families, several categories will continue to be derived on the basis of this interface class.
Factory Method
The most classic factory model is Factory Method , a first discussed GoF 16129df00a1856.
Take Point as an example,
namespace cmdr::dp::factory::classical {
class classical_factory_method;
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;
friend class classical_factory_method;
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; }
};
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;
friend class classical_factory_method;
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; }
};
class classical_factory_method {
public:
static Transport *create_ship(float r_, float theta_) {
return new Ship{r_ * cos(theta_), r_ * sin(theta_)};
}
static Transport *create_trunk(float x_, float y_) {
return new Trunk{x_, y_};
}
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_);
}
template<typename T, typename... Args>
static std::unique_ptr<T> create(Args &&...args) {
return std::make_unique<T>(args...);
}
};
} // namespace cmdr::dp::factory::classical
void test_factory_classical() {
using namespace cmdr::dp::factory::classical;
classical_factory_method f;
auto p1 = f.create_trunk(3.1f, 4.2f);
std::cout << p1 << '\n';
p1->deliver();
auto p2 = f.create_ship(3.1f, 4.2f);
std::cout << p2 << '\n';
p2->deliver();
auto p3 = f.create_ship_2(3.1f, 4.2f);
std::cout << p3.get() << '\n';
p3->deliver();
auto p4 = f.create<Ship>(3.1f, 4.2f);
std::cout << p4.get() << '\n';
p4->deliver();
}
According to the classical expression, the factory method model recommends using the special factory method instead of direct calls to the object constructor (that is, using the new
operator). Don't worry, the object will still be new
, but the operator is changed to be called in the factory method. The objects returned by factory methods are often called "products."
But in modern C++, the explicit appearance of new delete is rejected (even the naked pointer is also rejected), so the above statement also needs to be slightly adjusted. It might look like this:
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_);
}
It should be fine-tuned when used, but it can also be almost unchanged (because of the encapsulation ability of smart pointers):
auto p3 = f.create_ship_2(3.1, 4.2);
std::cout << *p3.get() << '\n';
p3->deliver();
in this way.
Comment
The characteristic of the factory method pattern is that the code for creating a subclass object is concentrated in a separate factory class for management. For each subclass, there is usually a special method corresponding to it, which is also a Factory Method. origin.
The advantages of the factory approach model are obvious, but there are also many disadvantages.
Its advantage lies in the centralized creation point, easy to maintain, if there are design adjustments or requirements changes, it is easy or even unnecessary to adjust the business code. The code for calling the factory method (hereinafter referred to as business code) does not need to understand the difference between the actual objects returned by different subclasses. The business code treats all products as abstract Point
. The business code knows that all Point objects provide the at
method, but it does not care about its specific implementation.
The disadvantage is that it is more rigid, and new products will bring worse consequences. Generally speaking, there will always be a number of duplicate create() methods to match new products, which makes the iteration of the class library version a small problem, or it may cause users to be unable to add product implementation classes by themselves.
Improve
In Modern C++, with the help of template variable parameters (C++17 or later) and perfect forwarding, it is possible to reduce the creation method and combine it into a single function:
class classical_factory_method {
public:
template<typename T, typename... Args>
static std::unique_ptr<T> create(Args &&...args) {
return std::make_unique<T>(args...);
}
};
When used like this:
auto p4 = f.create<Ship>(3.1, 4.2);
std::cout << *p4.get() << '\n';
p4->deliver();
This will make the coding very concise, and it is also very friendly to the situation of adding a new product, almost no need to make any changes to factroy.
Later we will provide a more improved factory template class that can solve these problems.
Inner
Slightly adjust the implementation of the factory method pattern, and distribute the method of creating new instances to each product class to form a Factory Method in the Inner way. Generally, it may only be regarded as a variant of 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
Sometimes, no centralized factory class may be appropriate. This is why we want to emphasize the inner factory method pattern.
Improve
In order to take advantage of the advantages of a centralized factory class (a centralized and unified entry can be beneficial to code maintenance and business-level maintenance-such as buried points), even the Inner FM Pattern can provide a helper class/function to make specific calls. We can consider the SFINAE feature of Modern C++.
In the world of meta-programming, since all product objects will always implement create (and clone method), then we can easily use SFINAE technology (and C++17 parameter pack expansion, template variable parameter, folding expression) To construct an instance of it:
template<typename T, typename... Args>
inline std::unique_ptr<T> create_transport(Args &&...args) {
return T::create(args...);
}
And even no superb coding skills are needed (although there is a certain pain when learning skills, but after the code is written, it seems that no skills are required, everything seems plain, intuitive, and most people can understand. The reviewers are also very friendly).
It will look like this when used:
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();
}
Of course it can also be like this:
auto p3 = Ship::create(3.1, 4.2);
std::cout << *p3.get() << '\n';
p3->deliver();
It looks okay.
Comment
Take a comprehensive look at several forms of Factory Method, not only the Inner method, but also the orthodox Factory Method mode.
Their commonality lies in the extraction of an obvious creator as a method, regardless of where this method is placed, but the purpose of doing so is first to release the tight coupling between the creator and the specific product, so we can use a variety of methods To achieve better separation. In addition, because we tend to place the product creation code in a centralized location, such as a factory class, the code is easier to maintain, which is also the goal of the single responsibility principle. Turning to study the business code, we will find that after the necessary improvements, the new product type can hardly affect the change of the business code, so the Factory Method is also advantageous in this respect.
As for the shortcomings, after the improvements on the basis of the several Modern C++ mentioned above, there are basically no shortcomings, or there may be too many subclasses and separation, which causes the class inheritance system to sometimes swell to management difficulties, which can be regarded as one. Kind of shortcomings.
Abstract Factory
Because of this naming, we may often confuse it with C++'s Abstract class, Virtual Method, and Polymorphic object.
But in fact, there is no necessary causal relationship between the two.
Good examples are hard to find, so below we will use Refactoring Guru's example for a brief explanation. We recommend that you read Refactoring Guru's common design patterns based on this article?
Suppose you are developing a furniture store simulator. Your code includes some classes to represent:
- A series of related products, such as
Chair,
Sofa and
CoffeeTable.
- Different variants of the series. For example, you can use
Modern,
Victoria,
Art Deco and other styles to generate
chair,
sofa and
coffee table.
Therefore, the feature of Abstract Factory is that the factory can create all products uniformly in a certain style, instead of just creating products. This ability to control multiple dimensions is unique to Abstract Factory.
In other words, consider the introduction of multiple styles (Metro, Fluent, Apple, Material Design, etc.) on UI controls, and the introduction of multiple themes (red, white, dark, etc.). This kind of creation factory, It is an abstract factory.
In addition to multi-dimensionality, to further promote this understanding, in fact, chairs and sofas are two completely different products, but they all have the commonality of "furniture", so the abstract factory of "woodworking factory" will be built together. NS. Therefore, we need to note that another feature of the abstract factory is that it may create a variety of series of products, chair series, table series, and so on.
In this way, the abstract factory will be able to create a series of Shapes: Point, Line, Arc, Ellipse, Circle, Rect, Rounded Rect, etc., and can control their fill colors and border lines, etc.
Comment
Since the sample code will be obviously huge, there is no sample code. But presumably after the above description, you can easily and thoroughly understand what AF is and what is the difference between it and FM.
In fact, there is a certain inheritance relationship between the abstract factory and the factory method pattern. The factory method can be further expanded to get an abstract factory. So the boundary between the two is not so clear. And in most architecture designs, it is a simple inner at the beginning, and then it will be reconstructed into a specific factory class to form an FM mode, and then more objects will be introduced, and the objects will continue to be reconstructed for grouping and classification. Extracting commonalities and introducing themes, it began to evolve into an abstract factory model.
Summary
Unlike the generator/builder model, the factory model always returns a new instance of the object at once, while the former is more concerned with how to construct a complex object step by step and step by step.
Business code can face the factory class, plus various interface abstract classes in the product system. This is so-called. I don’t need to know what products are available, I only need to know what raw materials are provided and how to operate to produce products that meet the requirements.
factory template class
Of course we will not stop there.
If it's just an improved version of Modern C++ for several ancient Patterns, this article will stop here. But I don't want to be satisfied with this, because there should be a lot of such Posts. So people will be dissatisfied.
So now, instead of recalling some of the characteristics, advantages, and disadvantages of the factory pattern, we will clear our thinking and start a new design.
We want to make a generic Factory Pattern factory template class. The purpose is to save the writing of create method, because it is often boring repetitive. In addition, we don’t want to always write a skeleton of a factory class, but want After there is a set of products class, there will be a matching factory automatically.
accomplish
First of all, a series of products classes can actually be used as template variables;
Secondly, from a type name, we already have a tool that can get the class name ( hicc::debug::type_name<T>()
) during compilation.
In this way, we will be able to construct a tuple, pack the class name and creator, and stack them into the tuple.
The reason for using tuple is that we want to use the formal parameter package expansion syntax of C++17 std::tuple.
Without the help of std::tuple, then we need a fragment like this:
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>...}; };
But this will cause a series of coding problems.
Or maybe you think that using
std::map<std::string, std::function<T()> >
is a good idea?If you are really interested, you can try to rewrite it accordingly.
When such a tuple is in hand, we can search for the instance constructor of the corresponding object according to the passed-in class name identification string, and then complete the instantiation of the object.
So we can have such an implementation scheme:
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
The original motivation of this class comes from C++ Template to implement the Factory Pattern-Code Review Stack Exchange , but it eliminates the original portability problem and improves the variable parameter problem of the constructor, so it is now a real usable version. In cmdr-cxx it also will is used out of the box ( cmdr::util::factory<...>
).
In order to make code that takes full advantage of the new features of C++17, we have tried a variety of solutions. However, at present, it is the most concise to use a compile-time solidified instance of T and package it with tuple. Welcome to try and explore here.
The result of this is the implementation code given above. Its weakness is that it has to traverse the named_products{}
array. This is often an awkward method, but the code shape looks good. In addition, an internal instance T data
constructed in advance for each product. Because there is no other effective means to extract decltype(it.data)
, this method is forced. Its disadvantage is that it wastes memory and reduces the startup speed, but it has no side effects when used at runtime.
In general, imagine that your product categories should not exceed 500, so there is probably nothing unacceptable about these wastes.
use
Using it is also slightly different from the usual factory pattern usage. You need to specify the product interface class and all product classes when realizing the factory template class.
namespace fct = hicc::util::factory;
using shape_factory = fct::factory<tmp1::Point, tmp1::Point2D, tmp1::Point3D>;
There will be a mandatory requirement for all your product classes to have a unified root class (ie the product_base abstract class), and then the factory class can return new product instances to you through the polymorphism of this root class.
A practical example can look like this:
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
}
advantage
We consider the above to be an optimal solution of the factory model, because a large number of trivial matters have been removed or covered up, and the amount of new code is now sufficiently small.
You may have noticed that the create method of this factory template class requires the business code to provide the class name as the creation identifier. This is specially designed. Because we need a run-time variable rather than a compile-time expansion. Imagine the needs of configuration software. You can select eggplant or cucumber in a drop-down box, and then draw a component in the drawing area. At this time, all you need is a runtime variable identifier.
Even if you don't want this kind of future scalability, it does not affect your business code.
But you can also write a set of class template expansions, or even partial specializations.
background knowledge
To learn more about perfect forwarding and so on, you can look at the in-situ constructor and perfect forwarding in and . In-situ constructor in C++ (2) , but it should also Go to cppreferences information.
Virtual destructor
Note that the important use of virtual destructors is to safely delete polymorphic instances through base class pointers, which is a mandatory requirement. If you do not implement a virtual destructor, you may not be able to correctly release a polymorphic object when you delete the base. Therefore, in most derived class systems, virtual destructors must be declared in the base class. After that, in theory, the compiler will generate corresponding destructor polymorphisms for all derived classes.
However, I never challenge the ability of the compiler in these situations, but all derived classes explicitly write the destructor code. In addition to avoiding potential portability issues, an explicit virtual destructor helps reduce the mental burden of code readers, which is what you should do.
Once the base declares the virtual destructor, the destructor of the derived class does not have to carry the virtual or override keywords, which is automatic.
See the example in the "Usage" section above.
Smart pointers and polymorphism
When polymorphism is required, the pointer of the base class should be used, and the reference reference will not be able to perform polymorphic operations
Point* ptr = new Point3D(); ptr->name(); // ok (*ptr).name(); // mostly bad
The smart pointer wrapper of the base pointer can also be correctly polymorphic:
std::unique_ptr<Point> ptr = std::make_unique<Point3D>(); smt->name(); // ok
Please pay attention to details.
- From the derived smart pointer, the raw pointer can be transferred to the base smart pointer by move operation or call release(). Otherwise, use the above construction and immediately downgrade (using the move constructor of std::unique_ptr<T,...>, which actually implies move semantics).
Chat pointer and smart pointer
But that is just an excuse for the weak. If you don't even use pointers well, and instead talk to me about unique or shared, then I am afraid that a congenital inadequate evaluation is indispensable.
We all say that we will not use it, and we can choose to use it as needed. That is the performance of a master with ease. However, if you can't meet it and don't use it, and instead choose a less brain-burning method, it's difficult for people not to doubt whether this is just sour grapes.
People who have experienced the pain and beatings before C98 often have a good way to prevent the pointer from losing control, which is also the performance of their ability. In fact, modern C++ cannot prevent pointer problems. In a system-level programming language such as C++/C, you have only one choice when talking about pointers. Face it and get rid of it.
Perhaps it can be said that Modern C++ provides a lot of new methods to help us cover up the misuse of pointers. In fact, it is for the rookie to do fill-in-the-blank questions.
But don't be content with cooking chicken.
so
Therefore, in our factory template class, create(...)
and create_unique_ptr(...)
, are provided, no matter which style you are a fan of, pointer school, smart pointer school, or neutral school, you can do it on demand Take it without being upset.
summary
Above, personal point of view, just take a look at it.
For more complete source code (to eliminate potential warnings), refer to 16129df00a35b4 hicc-cxx source code factory.cc .
:end:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。