目录
笔者写了一些小玩具玩玩了,对C++项目设计与程序设计颇有感触。最近打算重新温习一下设计模式,从而将自己的经验系统化
笔者在这篇博客主要阐述的是构造器!
流水构造器
这里,我想继续使用《C++20设计模式》这本书中使用到的例子,那就是尝试以构造器的方式构造一个较为简单的DSL。
我们都知道,HTML是一个标签语言,我们可以看到像这样的标签语言:
<ul>
<li>Milk</li>
<li>Cheese</li>
</ul>
很好,我们设计的时候,可以立马想到树的结构,甚至是一个多叉树。那么单位就是一个完整的标签结构体。诸位可看:
struct HTMLElements
{
// 两个构造函数
HTMLElements() = default;
HTMLElements(const std::string name, const std::string text):
name(name), text(text){}
std::string createString(int indent = 0, bool is_tree = true) const; // Implements Omiited
// public
std::string name, text;
std::vector<HTMLElements> elements;
}
这就是为什么说我们构建的DSL相当简单了,它不支持向内插入标签树。当然这是为了节约精力注重设计模式而作的。
我们下面就可以开始设计构造器了,其实很简单,将构造的构成托付给它就行了。这就是构造器模式和后面的工厂模式的优点:分离构造解耦合逻辑。从而让我们可以专心的构造对象!
struct HTMLBuilder
{
HTMLBuilder(std::string root_name){root.name = root_name;}
HTMLBuilder& addChild(const std::string name, const std::string text){
root.elements.emplace_back(name, text);
return *this;
}
std::string createString(){
return root.createString();
}
private:
HTMLElements root;
};
可以看到,我们的构造器实际上就是调用了目标管理的对象。我们需要注意到这个地方
HTMLBuilder& addChild(const std::string name, const std::string text){
root.elements.emplace_back(name, text);
return *this;
}
如你所见,我们在添加节点的时候返回了构造器本身,欸,这样我们就可以做流水线式的调用了(至少不用一次又一次的写构造器的名称)
builder.addChild("li", "hello_world").addChild("li", "hello, world");
这个地方的addChild可以一直添加(笑)
当然,这样看构造器模式好像意义并不大。但是考虑这样的场景:
-
我们不希望客户程序员手动调用默认的或者是如何的构造函数
-
我们希望在构造的时候就做一些事情,但是不希望外界知道,实现想要藏起来
这个时候构造器的好处就上来了。当然,在这种情况下记得把构造函数放到私有函数里去,同时只声明构造器为友元类。这样外界就不会直接调用了。
具体的代码实现本人放到了自己的Github仓库中,地址在最后给出。
组合构造器
组合构造器!这个东西很有意思。我们这一次采取的是:注册一个人的信息。
一个人的信息很复杂,仔细想象,他有自己的私人信息,他的工作信息等等。我们难道要写一个带有成千上万的参数构造函数来表达一个人的信息吗?未也!这实在太不方便维护了。
所以,我们这样考虑:使用组合构造器的模式,将一个人的信息构造分给多个人来进行维护。举个例子:我会将一个人的私有信息放到私有信息构造类,而将工作信息放到工作信息构造类进行注册。
下面我们就要开始设计一个------嗯,相当复杂的组合流水式构造器。为了实现在一个类中返回各个构造器子类,我们思考一下,不如将代码这样书写:
----PersonBuilderBase
|
|---- PersonLivingBuilder: 注册私人信息
|
|---- PersonAddressBuilder: 注册工作信息
然后,我们将这个父类PersonBuilderBase设计为可以这样返回字类的接口:
PersonAddressBuilder work();
PersonLivingBuilder live();
protected:
PersonBuilderBase(Person& p):who(p){}
Person& who;
以及我们看到,这个地方为了保证我们操作的同一性,我们将引用而不是值存储进入类,这样我们后续调用何种接口都将会操作的是用一个对象。
为此,我们就需要将PersonLivingBuilder和PersonAddressBuilder设计为PersonBuilderBase的子类,他们将需要共享我们的操作对象Person:
struct PersonLivingBuilder : public PersonBuilderBase
{
explicit PersonLivingBuilder(Person& p): PersonBuilderBase(p){}
PersonLivingBuilder& live_in(const std::string living_at);
PersonLivingBuilder& with_id(const std::string id_num);
PersonLivingBuilder& with_name(const std::string name);
};
struct PersonAddressBuilder : PersonBuilderBase
{
explicit PersonAddressBuilder(Person& p):PersonBuilderBase(p){}
PersonAddressBuilder& work_as(const std::string work_professions);
PersonAddressBuilder& work_in(const std::string work_at);
PersonAddressBuilder& with_work_number(const std::string work_number);
};
其中,Person的信息如下所示:
struct Person
{
std::string work_space, work_number, work_professions;
std::string live_space, id_card, name, gender;
std::string introduce_self() const{
std::ostringstream oss;
oss << name << "'s id is " << id_card << ", who lives in " << live_space;
oss << ". he works as a " << work_professions <<
"with work number " << work_number << " at " << work_space;
return oss.str();
}
};
Ok,非常好,现在我们就差起手构造了。起手怎么办呢?或者说,我们这些构造器的应用的实际Hook从哪里来呢?这实际上就涉及到到底是谁拥有对象的所属权?构造器吗?还是将对象释放到自由空间由程序员管理呢?
这个问题仁者见仁,智者见智。笔者认为这需要依靠场景进行考虑。举个例子:如果我们外界不希望对象由我们托管,只是使用这个对象来完成一些工作,然后由什么Manager自动的进行回收,那么,我们就可以将这个Manager赋予构造器或者更进一步的------工厂的模式。托管这个对象的生命周期!
如果我们需要这个对象的所属权,那么我们全应当将对象使用std::move
的方式将对象从构造器中转移出来,这样我们自己来管理这个对象(或者说,回避不必要的拷贝)
因此,我们起手使用一个子类(实际上就是资源子类)来管理构造时期的资源。
class PersonBuilder : public PersonBuilderBase
{
Person creating_person;
public:
PersonBuilder():PersonBuilderBase(creating_person){}
};
如你所见,在对象构造期,我们将Person放到PersonBuilderBase的子类PersonBuilder上,他是我们的构造器前端。在这里我们锁定使用到的Person类的资源。
当然,我们的构造可能到任意步骤就会结束,因此,我们需要将返回被操作资源的接口放到Base类上,保证所有的构造器都可以直接将资源释放出来。
当然,一种方式就是:
Person PersonBuilderBase::finish_build() const{
return p;
}
emm,好像不太雅观。在写了一大长串的构造器之后喊一嗓子finish_build似乎不太符合我的胃口(真的,虽然他像是程序的终止符一样,表达自己的构造结束。但是总是有像我一样的人会忘记释放对象所属权,至少导致返回的类型和接受类型不符合,要么惊讶的发现返回了一个自己没见过的构造器,要么在编译器间炸出一个错误------哎!这样写吧
operator PersonBuilderBase::Person(){
return std::move(who);
}
最后,让我们小小的超前一点,放到一个工厂函数里:
struct PersonCreator
{
static PersonBuilder create(){
return PersonBuilder();
}
};
现在,我们就可以流水一般的构造对象!
Person p = PersonCreator::create()
.live().with_name("Charliechen").with_id("114514").live_in("South Eight")
.work().work_as("Student").work_in("Everywhere").with_work_number("1919810");
std::cout << p.introduce_self();
啊哈,写完这个句子,你就构造好了这个对象!帅!