I have written a talk about the Factory mode C++17, and then I wrote a talk about the Singleton mode C++17 by the way. It seems to have to be a whole lot, which is very troublesome for lazy people. I don't know if I plan to write a complete personal understanding of GoF and new implementations. Take your time to read it, and just do it.
Review the builder pattern, and deal with the problem of how to implement the builder template class when building a class library.
Prologue
In fact, as far as I am concerned, the real use of the builder pattern is actually in the Java development experience. The same is true for streaming interfaces.
The Builder mode is used to construct an object step by step, see the picture:
FROM: HERE
Although many times we only care about how to manipulate the object after it is new, sometimes in some scenes we only care about how to new the object. This is the Builder.
Builder Pattern
theory
Builder pattern is one of Creational Patterns . In talking about the Factory mode , we have already introduced the creation mode, so this article will not repeat it.
The intention of the builder pattern is to allow you to build complex objects step by step, which allows you to use the same (similar) creation diamanté to produce objects of different types and forms.
For the Builder mode, an important sign, although this is not a requirement, it is often established by convention, that is, it ends with a .build()
call. E.g:
auto shape = Builder()
.choose(Shape.Rect) // choose a factory
.setColor(COLOR.RED)
.setBorderWidth(1)
.setFill(COLOR.GRAY)
.build();
canva.place(shape, Position.Default);
The Builder mode does not have to use a streaming interface.
Instead, in many cases we need to negotiate a choice with the interacting object and set this decision in the Builder. Until all the negotiations are completed, the final product instance is constructed using builder.build().
Just like the imagination given in the sample code, we can also combine the Builder and Factory modes (and Proxy mode or others), and let a fundamental Builder call the concreted FactoryBuilder to build a variety of products. Since this often requires a larger length of code to show its appearance, it will not be expanded anymore.
C++ implementation
Note that the following examples are all long.
basic
The following is a standard, basic builder pattern example. This case demonstrates the typical implementation method of builder pattern through the step-by-step construction of the four components of email.
namespace hicc::dp::builder::basic {
class email_builder;
class email {
public:
~email() {}
friend class email_builder; // the builder can access email's privates
static email_builder builder();
std::string to_string() const {
std::stringstream ss;
ss << " from: " << _from
<< "\n to: " << _to
<< "\nsubject: " << _subject
<< "\n body: " << _body;
return ss.str();
}
explicit email(std::string const &from, std::string const &to, std::string const &subject, std::string const &body)
: _from(from)
, _to(to)
, _subject(subject)
, _body(body) {}
email(email &&o) {
_from = o._from, _to = o._to, _subject = o._subject, _body = o._body;
}
email clone(email &&o) {
email n{o._from, o._to, o._subject, o._body};
return n;
}
private:
email() = default; // restrict construction to builder
std::string _from{}, _to{}, _subject{}, _body{};
};
class email_builder {
public:
email_builder &from(const std::string &from) {
_email->_from = from;
return *this;
}
email_builder &to(const std::string &to) {
_email->_to = to;
return *this;
}
email_builder &subject(const std::string &subject) {
_email->_subject = subject;
return *this;
}
email_builder &body(const std::string &body) {
_email->_body = body;
return *this;
}
operator std::unique_ptr<email> &&() {
return std::move(_email); // notice the move
}
auto build() {
return std::move(_email); // not a best solution since concise is our primary intent
}
email_builder()
: _email(std::make_unique<email>("", "", "", "")) {}
private:
std::unique_ptr<email> _email;
};
inline email_builder email::builder() { return email_builder(); }
inline std::ostream &operator<<(std::ostream &os, const email &email) {
os << email.to_string();
return os;
}
} // namespace hicc::dp::builder::basic
void test_builder_basic() {
using namespace hicc::dp::builder::basic;
// @formatter:off
auto mail = email::builder()
.from("me@mail.com")
.to("you@mail.com")
.subject("About Design Patterns")
.body("There is a plan to write a book about cxx17 design patterns. It's good?")
.build();
std::cout << *mail.get() << '\n';
// @formatter:on
}
And its test code part also presents a typical streaming call style.
The sample code provides a rigid method of coding structure, that is, the model class model class instance object is obtained by builder.build() in the last step. Sometimes stereotypes are the best choice. Indeed, we will see later that a design pattern can be implemented in various ways. However, maintaining the similarity of the coding structure will help users to obtain the interface usage method without additional documentation when visiting the interface API, especially when visiting the available interfaces through the namespace level.
Therefore, the code can explain everything by itself, and this is the correct way for you to escape comments.
Extra tips
In response to the Modern C++ style, the sample code uses unique_ptr to help manage the samples. Why not use shared_ptr? Because shared_ptr is relatively heavier, it needs to manage a set of additional reference counting mechanism, so use unique_ptr directly and only consider using shared_ptr when necessary (for example, when it needs to be hosted in multiple containers).
Then use the fixed paradigm above, but what I need is shared_ptr. What should I do? Can I convert unique_ptr to shared_ptr semantics?
This is not a problem. Move semantics allows u to s directly:
std::unique_ptr<std::string> unique = std::make_unique<std::string>("test");
std::shared_ptr<std::string> shared = std::move(unique);
even:
std::shared_ptr<std::string> shared = std::make_unique<std::string>("test");
So you can decide whether to make an explicit return type declaration during build():
auto obj = builder.build(); // 得到 unique_ptr<T>
std::shared_ptr<T> o = builder.build(); // 隐含一个移动操作
Embedded
In the previous example, the method of separating two independent classes is adopted, which makes the class structure and dependencies clearer, but it may be slightly polluted because there will be an extra existence of the product builder class in the namespace. And a namespace named models should not have other things that are not Models-helpers or utilities-exist. Therefore, especially in metaprogramming, it is more inclined to embed the builder class directly in the product class:
namespace hicc::dp::builder::embed {
class email {
public:
class builder_impl {
public:
builder_impl &from(const std::string &from) {
_email._from = from;
return *this;
}
// ...
auto build() {
return _email;
}
private:
std::unique_ptr<email> _email;
};
static builder_impl builder(){
return builder_impl{};
}
public:
//...
private:
email() = default; // restrict construction to builder
std::string _from, _to, _subject, _body;
};
} // namespace hicc::dp::builder::embed
void test_builder_embed() {
using namespace hicc::dp::builder::embed;
// @formatter:off
auto mail = email::builder()
.from("me@mail.com")
.to("you@mail.com")
.subject("About Design Patterns")
.body("There is a plan to write a book about cxx17 design patterns. It's good?")
.build();
std::cout << mail << '\n';
// @formatter:on
}
There is almost no need for users to revise.
Its additional advantage is that there is no need for additional declarations of forward references, and no need for declarations of friend class, which can save a lot of brain power.
complicated
However, the builder pattern does not have to have a build() method to do it, nor does it have to use a stream interface. The following case often appears in the corresponding tutor, but we have modified it.
First, give the product category part:
namespace hicc::dp::builder::complex {
namespace basis {
class wheel {
public:
int size;
};
class engine {
public:
int horsepower;
};
class body {
public:
std::string shape;
};
class car {
public:
wheel *wheels[4];
engine *engine;
body *body;
void specifications() {
std::cout << "body:" << body->shape << std::endl;
std::cout << "engine horsepower:" << engine->horsepower << std::endl;
std::cout << "tire size:" << wheels[0]->size << "'" << std::endl;
}
};
} // namespace basis
} // namespace hicc::dp::builder::complex
It has nothing to say.
But its builder will be more complicated, because it is decided that there are two prefabricated builders (Jeep and Nissan) to make Cars of different specifications. So we need an abstract class builder class and a build template class director. In fact, you don't need to separate the template classes, and it is possible to make full use of polymorphism:
namespace hicc::dp::builder::complex {
class builder {
public:
virtual basis::wheel *get_wheel() = 0;
virtual basis::engine *get_engine() = 0;
virtual basis::body *get_body() = 0;
};
class director {
public:
void set_builder(builder *b) { _builder = b; }
basis::car *get_car() {
basis::car *car = new basis::car();
car->body = _builder->get_body();
car->engine = _builder->get_engine();
car->wheels[0] = _builder->get_wheel();
car->wheels[1] = _builder->get_wheel();
car->wheels[2] = _builder->get_wheel();
car->wheels[3] = _builder->get_wheel();
return car;
}
private:
builder *_builder;
};
} // namespace hicc::dp::builder::complex
The model class determines the standard model for building Car.
If you do use the get_car()
in the abstract builder class and make it virtual (which is not necessary), then this practice actually also references the Template Method Pattern.
Template Method Pattern defines an algorithm framework in the super class, allowing subclasses to rewrite specific steps of the algorithm without modifying the structure.
Next, the concrete realization of two builder classes:
namespace hicc::dp::builder::complex {
class jeep_builder : public builder {
public:
basis::wheel *get_wheel() {
basis::wheel *wheel = new basis::wheel();
wheel->size = 22;
return wheel;
}
basis::engine *get_engine() {
basis::engine *engine = new basis::engine();
engine->horsepower = 400;
return engine;
}
basis::body *get_body() {
basis::body *body = new basis::body();
body->shape = "SUV";
return body;
}
};
class nissan_builder : public builder {
public:
basis::wheel *get_wheel() {
basis::wheel *wheel = new basis::wheel();
wheel->size = 16;
return wheel;
}
basis::engine *get_engine() {
basis::engine *engine = new basis::engine();
engine->horsepower = 85;
return engine;
}
basis::body *get_body() {
basis::body *body = new basis::body();
body->shape = "hatchback";
return body;
}
};
} // namespace hicc::dp::builder::complex
And, its test code:
void test_builder_complex() {
using namespace hicc::dp::builder::complex;
basis::car *car; // Final product
/* A director who controls the process */
director d;
/* Concrete builders */
jeep_builder jb;
nissan_builder nb;
/* Build a Jeep */
std::cout << "Jeep" << std::endl;
d.set_builder(&jb); // using JeepBuilder instance
car = d.get_car();
car->specifications();
std::cout << std::endl;
/* Build a Nissan */
std::cout << "Nissan" << std::endl;
d.set_builder(&nb); // using NissanBuilder instance
car = d.get_car();
car->specifications();
}
Note that Car is composed of many parts, and each part may also have very complicated construction steps.
optimization
Of course, this example is just an example. In the real world, the implementation of this example can extract jeep_builder and nissan_builder out of a common base class:
class managed_builder : public builder {
public:
basis::wheel *get_wheel() {
basis::wheel *wheel = new basis::wheel();
wheel->size = wheel_size;
return wheel;
}
basis::engine *get_engine() {
basis::engine *engine = new basis::engine();
engine->horsepower = engine_horsepower;
return engine;
}
basis::body *get_body() {
basis::body *body = new basis::body();
body->shape = body_shape;
return body;
}
managed_builder(int ws, int hp, const char *s = "SUV")
: wheel_size(ws), engine_horsepower(hp), body_shape(s) {}
int wheel_size;
int engine_horsepower;
std::string_view body_shape;
};
Not only is it conducive to eliminating duplicate code fragments, but it is also more able to cope with future expansion, in case you want BMW.
Further generalization
In fact, you can also use template classes:
template<int wheel_size, int engine_horsepower, char const *const body_shape>
class generic_builder : public builder {
public:
basis::wheel *get_wheel() {
basis::wheel *wheel = new basis::wheel();
wheel->size = wheel_size;
return wheel;
}
basis::engine *get_engine() {
basis::engine *engine = new basis::engine();
engine->horsepower = engine_horsepower;
return engine;
}
basis::body *get_body() {
basis::body *body = new basis::body();
body->shape = body_shape;
return body;
}
};
constexpr const char suv_str[] = {"SUV"};
constexpr const char hatchback_str[] = {"hatchback"};
class jeep_builder : public generic_builder<22, 400, suv_str> {
public:
jeep_builder()
: generic_builder<22, 400, suv_str>() {}
};
class nissan_builder : public generic_builder<16, 85, hatchback_str> {
public:
nissan_builder()
: generic_builder<16, 85, hatchback_str>() {}
};
constexpr const char suv_str[]
is used here, which allows us to try to pass the literal value of the string directly in the template parameter, so the above code is completely templated.
If you are already using C++20, then std::basic_fixed_string
can give you the ability to pass string literals directly:
template<int wheel_size, int engine_horsepower, char const *const body_shape>
class generic_builder : public builder {
// ...
};
class jeep_builder : public generic_builder<22, 400, "SUV"> {
public:
};
class nissan_builder : public generic_builder<16, 85, "hatchback"> {
public:
};
If you are interested in the complete source code, you can check the source code related to .
Builder Pattern in Metaprogramming
We just talked about the job of genericizing a builder in advance, but that was just a little bit of initial refactoring. When the Builder Pattern needs to be used in the template class system, the situation has changed a little, especially when the common code of the builder is extracted into a single base class, we need the intervention of CRTP technology.
CRTP
CRTP is a C++ idiom, which was born much earlier than C++11. In the Visual C++ era, ATL, WTL and a small amount of MFC all used this technology on a large scale, and so did the later ProfUIS.
Simply put, the purpose of CRTP is to achieve compile-time polymorphic binding. The implementation method is to pass the derived class name to the template parameter of the base class, so the base class can use the static_cast<derived_t>(*this*)
syntax to obtain the "polymorphic" of the derived class. "Operational ability:
template <typename derived_t>
class base{
public:
void do_sth(){
static_cast<derived_t>(*this*)->show();
}
void show(){hicc_debug("base::show");}
};
template <typename T>
class derived: public base<derived> {
public:
T t{};
void show(){
hicc_debug("t: %s", hicc::to_string(t).c_str());
}
};
Inheritable builder pattern
After understanding CRTP technology, here is only a schematic fragment:
namespace hicc::dp::builder::meta {
class builder_base {
public:
builder_base &set_a() {
return (*this);
}
builder_base& on_set_b(){
return (*this);
}
};
template<typename derived_t, typename T>
class builder : public builder_base {
public:
derived_t &set_a() {
return *static_cast<derived_t *>(this);
}
derived_t &set_b() {
return *static_cast<derived_t *>(this);
}
std::unique_ptr<T> t{}; // the temporary object for builder constructing...
// ... more
};
template<typename T>
class jeep_builder : public builder<jeep_builder<T>, T> {
public:
jeep_builder &set_a() {
return *this;
}
};
} // namespace hicc::dp::builder::meta
void test_builder_meta() {
using namespace hicc::dp::builder::meta;
jeep_builder<int> b{};
b.set_a();
}
In the code, return *static_case<derived_t*>(this)
can guarantee that the derived_t&
reference is always returned, which can ensure that the chain call jeep_builder().set_a() initiated from the derived class can correctly call the overloaded version of the derived class (also an overwrite, wipe Divide version), so it can still correctly (simulate) polymorphism without using virtual function.
Epilogue
A few features depend on the syntax support above cxx17, but they are not necessary.
:end:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。