设计模式:构造器模式

构造器模式主要关注复杂对象的创建过程。复杂对象就是指难以通过单个构造函数构造的对象。其本身可能由多个对象构成,且可能设计不太明显的逻辑。因此需要专门的组件创建

相关代码已经上传至gitee:设计模式: 本期代码主要是介绍编程中相关的设计模式及其简单地代码示例(以C++语言为主)

目录

预想方案

简单构造器

流式构造器

调用构造器

Groovy风格构造器

组合构造器

参数化构造器

构造器的继承

总结


预想方案

如果我们要创建一个网页组件,首先输出一个无序列表

cpp 复制代码
void test()
{
    std::string words[]={"Hello", "World", "C++20", "Programming", "Language"};
    std::ostringstream oss;
    oss<<"<ul>";
    for(const auto& word : words)
    {
        oss<<"<li>"<<word<<"</li>";
    }
    oss<<"</ul>";
    printf("%s", oss.str().c_str());
}

我们可以通过面向对象的方式写一个HTML组件类

cpp 复制代码
struct HtmlElement
{
    std::string tag;
    std::string content;
    std::vector<HtmlElement> children;
    HtmlElement()=default;
    HtmlElement(const std::string& t, const std::string& c):tag(t), content(c){}
    std::string str(int indent=0) const
    {}
};

以这种方式我们可以用更合理的方式创建它

cpp 复制代码
void test()
{
    std::string words[]={"Hello", "World", "C++20", "Programming", "Language"};
    HtmlElement ul("ul", "");
    for(const auto& word : words)
    {
        ul.elements.emplace_back("li", word);
    }
    printf("%s", ul.str().c_str());
}

这种方式也不错,它为我们创建了一个更可控的面向对象驱动的项目列表形式。但是这种创建方式并不方便

简单构造器

构造器模式尝试将对象构造过程封装到单独的类中,代码如下:

cpp 复制代码
struct HtmlBuilder
{
    HtmlElement root;
    HtmlBuilder(std::string root_tag)
    {
        root.tag=root_tag;
    }
    void add_child(std::string child_tag, std::string child_content)
    {
        root.elements.emplace_back(child_tag, child_content);
    }
    std::string str() const
    {
        return root.str();
    }
};

这样我们就实现了一个简单的构造器。 HtmlBuilder专门用来创建组件,add_child方法用于添加子元素

流式构造器

如果我们将add_child定义修改如下:

cpp 复制代码
HtmlBuilder& add_child(std::string child_tag, std::string child_content)
{
    root.elements.emplace_back(child_tag, child_content);
    return *this;
}

这样通过add_child()的方法返回值修改为类本身的引用,可以实现链式调用,这就是流式接口

我们可以实现两种版本,这取决于你喜欢->还是.进行调用(不可以同时实现)

cpp 复制代码
struct HtmlBuilder
{
    HtmlElement root;
    HtmlBuilder(std::string root_tag)
    {
        root.tag=root_tag;
    }
    //链式调用版本1 - 返回引用(默认使用)
    HtmlBuilder& add_child(std::string child_tag, std::string child_content)
    {
        root.elements.emplace_back(child_tag, child_content);
        return *this;
    }
    
    //链式调用版本2 - 返回指针(需要显式指定)
    HtmlBuilder* add_child_ptr(std::string child_tag, std::string child_content)
    {
        root.elements.emplace_back(child_tag, child_content);
        return this;
    }
    std::string str() const
    {
        return root.str();
    }
};
//链式调用版本1
void Test1()
{    
    HtmlBuilder builder("ul");
    builder.add_child("li", "Hello").add_child("li", "World");
    std::cout<<builder.str()<<std::endl;
    
}
//链式调用版本2
void Test2()
{
    
    HtmlBuilder* builder2=new HtmlBuilder("ul");
    builder2->add_child_ptr("li", "Hello")->add_child_ptr("li", "World");
    std::cout<<builder2->str()<<std::endl;
    delete builder2;
}

调用构造器

我们已经设计出来了构造器,那么我们该如何告诉使用者如何调用构造器呢?

一种方法是使用HtmlBuilder

cpp 复制代码
struct HtmlElement
{
    std::string tag;
    std::string content;
    std::vector<HtmlElement> elements;
    const size_t indent_size=2;
    
    std::string str(int indent=0) const
    {}
    static std::unique_ptr<HtmlBuilder> build(std::string root_tag)
    {
        return std::make_unique<HtmlBuilder>(root_tag);
    }
    protected:
        HtmlElement()=default;
        HtmlElement(const std::string& t, const std::string& c):tag(t), content(c){}
};

这里我们隐藏了HtmlElement所有的构造函数,无法外部构造。同时创造了一个工厂方法

cpp 复制代码
/*
    构造器的调用
*/ 
#include<string>
#include<sstream>
#include<cstdio>
#include<iostream>
#include<sstream>
#include<vector>
#include<string>
#include<memory>
struct HtmlBuilder
{
    HtmlElement root;
    
    // 改进构造函数:使用初始化列表
    operator HtmlElement() const{ return root; }
    
    // 链式调用版本1 - 返回引用(优化参数传递)
    HtmlBuilder& add_child(const std::string& child_tag, const std::string& child_content)
    {
        root.elements.emplace_back(child_tag, child_content);
        return *this;
    }
    
    // 链式调用版本2 - 返回指针(优化参数传递)
    HtmlBuilder* add_child_ptr(const std::string& child_tag, const std::string& child_content)
    {
        root.elements.emplace_back(child_tag, child_content);
        return this;
    }
    std::string str() const
    {
        return root.str();
    }
};
struct HtmlElement
{
    std::string tag;
    std::string content;
    std::vector<HtmlElement> elements;
    const size_t indent_size=2;
    
    std::string str(int indent=0) const
    {}
    static std::unique_ptr<HtmlBuilder> build(std::string root_tag)
    {
        return std::make_unique<HtmlBuilder>(root_tag);
    }
    protected:
        HtmlElement()=default;
        HtmlElement(const std::string& t, const std::string& c):tag(t), content(c){}
};


void test()
{
    HtmlElement builder=HtmlElement::build("ul")->add_child("li", "Hello").add_child("li", "World");
    std::cout<<builder.str()<<std::endl;
}

Groovy风格构造器

Groovy、kotlin和其他编程语言都试图通过支持易于处理的语法结构来展示他们在构建DSL(一种小型编程语言)。在C++中我们可以用普通的类来构建于HTML兼容的DSL

cpp 复制代码
struct Tag
{
    std::string name;
    std::string text;
    std::vector<Tag> children;
    std::vector<std::pair<std::string, std::string>> attributes;
    friend std::ostream& operator<<(std::ostream& os, const Tag& tag)
    {}
    protected:
        Tag(const std::string &name, const std::string &text): name(name), text(text) {}
        Tag(const std::string &name,const std::vector<Tag>&children): name(name), children(children) {}

};

接下来从Tag类派生出其他类,但是只能派生有效的HTML标签类

cpp 复制代码
struct P:Tag
{
    explicit P(const std::string &text): Tag("p", text) {}
    P(std::initializer_list<Tag> children): Tag("p", children) {}
};
struct Image:Tag
{
    explicit Image(const std::string &src): Tag("img", "") 
    {
        attributes.emplace_back(std::make_pair("src", src));  // 使用make_pair来创建键值对
    }
};

以上代码进一步约束了我们的API。接下来我们就来构造一个DSL

cpp 复制代码
void test()
{
    std::cout<<P{
        Image("https://example.com/image.jpg"),
    }<<std::endl;
}

组合构造器

接下来我们将使用多个构造器来构造单个对象。

比如说我们像记录一个人的某些信息

cpp 复制代码
struct Person
{
    std::string name,address,code,city;
    std::string company ,position;
    int annual_income;
    Person()=default;
};
class PersonBuilderBase
{
    protected:
        Person& person;
        PersonBuilderBase(Person &p):person(p){}
    public:
        operator Person() const
        {
            return std::move(person);
        }
        PersonAddressBuilder lives() const;
        PersonJobBuilder works() const;
};
class PersonAddressBuilder: public PersonBuilderBase
{
    using self=PersonAddressBuilder;
    public:
        explicit PersonAddressBuilder(Person &p):PersonBuilderBase(p){}
        self& at(std::string street_address)
        {
            person.address=street_address;
            return *this;
        }
        self& in(std::string city)
        {
            person.city=city;
            return *this;
        }
        self& with(std::string code)
        {
            person.code=code;
            return *this;
        }
};
class PersonJobBuilder: public PersonBuilderBase
{
    using self=PersonJobBuilder;
    public:
        explicit PersonJobBuilder(Person &p):PersonBuilderBase(p){}
        self& at(std::string company)
        {
            person.company=company;
            return *this;
        }
        self& as_a(std::string position)
        {
            person.position=position;
            return *this;
        }
        self& earning(int annual_income)
        {
            person.annual_income=annual_income;
            return *this;
        }
};

Person有两方面的信息------地址信息和职业信息

这比之前定义构造器复杂多了,接下来我们将逐个讨论 PersonBuilderBase 的各个成员:

  • person :这是对即将创建的对象的引用。也许看起来很奇怪,但这对创建 person 对象的子构造器却很有意义!注意一个关键点,在这个类中并没有实际存储 Person 的成员!这个类存储的是 Person 的引用,而不是实际构建的 Person 对象。

  • 以 Person 的引用作为参数的构造函数被声明为 protected(受保护的),因此只有其派生类(PersonAddressBuilder 和 PersonJobBuilder)可以使用它。

  • Operator Person() 是我们之前使用过的一个技术,这个函数假设 Person 提供了一个正确定义的移动构造函数

  • lives() 和 works() 是两个返回构造器的函数:它们分别完成对地址信息和工作信息的构建。

那么我们先缺少的就是实际要构造的对象了

为什么我们要定义两种构造函数?我们来看一下其中一个构造器的实现

cpp 复制代码
class PersonAddressBuilder: public PersonBuilderBase
{
    using self=PersonAddressBuilder;
    public:
        explicit PersonAddressBuilder(Person &p):PersonBuilderBase(p){}
        self& at(std::string street_address)
        {
            person.address=street_address;
            return *this;
        }
        self& in(std::string city)
        {
            person.city=city;
            return *this;
        }
        self& with(std::string code)
        {
            person.code=code;
            return *this;
        }
};

可以看到,PersonAddressBuilder 提供了创建 person 地址的流式接口。

注意,PersonAddressBuilder 实际上继承自 PersonBuilderBase(这意味着它也有 lives()works() 函数)并且将 Person 的引用传入 PersonBuilderBase 的构造函数。PersonAddressBuilder 并没有继承自 PersonBuilder------ 如果它这么做了,我们将会创建很多 Person 实例,但老实说,我们只需要一个就够了。

可以想见,PersonJobBuilder 会以同样的方式来实现。PersonAddressBuilderPersonJobBuilder 以及 PersonBuilder,均声明为 Person 类的友元类(friend),因此它们可以访问 Person 的私有成员。

现在,是见证如何构建 Person 的时候了!以下示例代码展示了上述构造器是如何工作的:

cpp 复制代码
Person p = Person::create()
  .lives().at("123 London Road")
          .with_postcode("SW1 1GB")
          .in("London")
  .works().at("PragmaSoft")
          .as_a("Consultant")
          .earning(10e6);

我们发现使用 create() 函数得到了一个构造器,然后使用 lives() 函数获得了一个 PersonAddressBuilder。一旦我们完成地址信息的初始化,仅通过调用 works() 就可以使用 PersonJobBuilder 了。

当完成构建过程后,我们可以像之前一样得到构建的 Person 对象。注意,一旦构建完成,构造器就没有用了,因为我们调用 move() 函数来移动 Person

参数化构造器

强制用户用构造器而不是直接构造对象的唯一方法是让构造函数不可访问,但是某些情况下我们希望用户一开始就与构造器打交道,甚至隐藏他们构建的对象

这里所说的内部是之不想让用户直接与这个类交流

接下来我们来实现这个构造器,并且只能通过MailService来构造

cpp 复制代码
/*
    参数化构建器
*/
#include <string>
#include <iostream>



class MailServer
{   
    class Email
    {
        public:
            std::string from, to, subject, body;
            Email()=default;
    };
    
    public:
        
        class EmailBuilder
        {
            Email& email;
            public:
                explicit EmailBuilder(Email& email):email(email){}
                
                EmailBuilder& from(const std::string& sender)
                {
                    email.from = sender;
                    return *this;
                }
                
                EmailBuilder& to(const std::string& recipient)
                {
                    email.to = recipient;
                    return *this;
                }
                
                EmailBuilder& subject(const std::string& subj)
                {
                    email.subject = subj;
                    return *this;
                }
                
                EmailBuilder& body(const std::string& content)
                {
                    email.body = content;
                    return *this;
                }
        };
        
        // 修改send函数以接受构建器lambda
        template<typename F>
        void send(F&& builder_func)
        {
            Email email;
            EmailBuilder builder(email);
            builder_func(builder);  // 调用构建器函数配置邮件
            send_impl(email);       // 发送配置好的邮件
        }
        
    private:
        void send_impl(const Email& email)
        {
            // 实际的邮件发送逻辑
            std::cout << "Sending email:" << std::endl;
            std::cout << "From: " << email.from << std::endl;
            std::cout << "To: " << email.to << std::endl;
            std::cout << "Subject: " << email.subject << std::endl;
            std::cout << "Body: " << email.body << std::endl;
        }
};

可以看到,用户使用的 send_email() 方法的入口并不是一组参数或预先打包好的对象,而是携带了一个函数。这个函数以 EmailBuilder 的引用为参数,然后通过 EmailBuilder 构建邮件的主体内容。一旦构建工作完成,我们就可以使用 MailService 的内部机制来生成一个完全初始化的 Email

可以看到,这里有一个巧妙的技巧:EmailBuilder 通过其构造函数的参数来获得 Email 的引用,而不是在其内部存储 Email 的引用。这么做的原因是,通过这种方式,EmailBuilder 也就不必在它的任何一个 API 中公开暴露 Email

我们可以这样使用API

cpp 复制代码
void test()
{
    MailServer ms;
    ms.send([&](auto& eb){
        eb.from("foo@bar.com")
            .to("bar@baz.com")
            .subject("hello")
            .body("Hello, how are you?");
    });
}

构造器的继承

流式构造器的继承性问题不仅影响流式构造器,也影响任意一个使用流式接口的类。

某个流式构造器可以继承另一个流式构造器吗?可以的,但是这并不方便。

我们以以下代码为例子:

cpp 复制代码
class Person
{
    public:
        std::string name, address, company, position, city, annual_income;
        friend std::ostream&operator<<(std::ostream& os, const Person& p)
        {
            os << "name: " << p.name << std::endl;
            os << "address: " << p.address << std::endl;    
            os << "city: " << p.city << std::endl;
            os << "company: " << p.company << std::endl;
            return os;
        }
};
class PersonBuilder
{
    protected:
        Person& person;
        PersonBuilder(Person &p):person(p){}
    public:
        [[nodiscard]] Person Build()const
        {
            return std::move(person);
        }
};
class PersonInfoBuilder: public PersonBuilder
{
    using self=PersonInfoBuilder;
    public:
        PersonInfoBuilder()=default;
        self& called(const std::string& name)
        {
            person.address=name;
            return *this;
        }
};

这种方法可行且没有问题。但是当我们需要从PersonInfoBuilder类派生出一个类的时候骂我们可能会这么写:

cpp 复制代码
class PersonJobBuilder: public PersonBuilder
{
    Person p;
    using self=PersonJobBuilder;
    public:
        PersonJobBuilder(): PersonBuilder(p) {}  // 传递p给基类构造函数
        self& work_as(const std::string& position)
        {
            person.position=position;
            return *this;
        }
};

不幸的是,已经破坏了流式接口

cpp 复制代码
void test()
{
    PersonJobBuilder pb;
    auto person=pb.called("Loukou")//破坏了流畅接口,无法继续调用PersonInfoBuilder的函数
        .work_as("Engineer").company("Google")
    .Build();
}

为什么会这样呢?因为called返回的*this类型为PersonInfoBuilder&,但是PersonInfoBuilde没有work_as方法

我们可以以以下方式重新实现:

cpp 复制代码
template<typename T>
class PersonInfoBuilder: public PersonBuilder
{
    public:
        T&called(const std::string& name)
        {
            person.name=name;
            return static_cast<T&>(*this);
        }
};

这就是经典的CRTP

流式接口最大的问题是:调用基类的流式接口时,能否将基类内部的this指针转化欸正确类型并作为该流式接口的返回值。解决该问题唯一方式是使用贯穿整个继承层的模板参数

cpp 复制代码
template<typename T>
class PersonJobBuilder: public PersonInfoBuilder<PersonJobBuilder<T>>
{
    T& work_as(const std::string& position)
    {
        person.position=position;
        return static_cast<T&>(*this);
    }
};

此时PersonJobBuilder基类类型是PersonInfoBuilder<PersonJobBuilder<T>>,这样当继承来自PersonInfoBuilder时,PersonInfoBuilder的T参数设置为PersonJobBuilder,以保证所有流式接口返回正确的类型,而不是基类本身的类型

那么这些带模板参数的类,我们该如何实现构造器呢?我们可能需要这样写

cpp 复制代码
class MyBuilder: public PersonJobBuilder<MyBuilder>
{
public:
    MyBuilder& company(const std::string& company)
    {
        this->get_person().company = company;
        return *this;
    }
};

现在我们可以利用MyBuilder类将以上内容连在一起

cpp 复制代码
void test()
{
    MyBuilder pb;
    auto person=pb.called("Loukou")
        .work_as("Engineer").company("Google")
    .Build();
     std::cout << person;
}

总结

构造器模式特点:

  • 构造器模式可以通过流式接口调用链来实现复杂的构建过程。为了实现流式接口,构造器的函数需要返回 this*this
  • 为了强制用户使用构造器的 API,我们可以将目标对象的构造函数限制为不可访问,同时,定义一个 create() 接口返回构造器。
  • 通过定义适当的运算符,可以使构造器转换为对象本身。
  • 借助 C++ 新特性中的统一初始化语法,可以实现 Groovy 风格的构造器。这是一种很通用的方法,可创建各式各样的 DSL。
  • 单个构造器接口可以暴露多个子构造器接口。通过灵活地使用继承和流式接口,很容易将一个构造器变换为另一个构造器。

本期内容到这里就结束了。

封面图自取:

相关推荐
lly2024062 小时前
Swift 析构过程
开发语言
南境十里·墨染春水2 小时前
linux学习进展 进程
linux·运维·学习
mu_guang_2 小时前
计算机体系结构3-cache一致性和内存一致性的区别
java·开发语言·计算机体系结构
邪修king2 小时前
UE5 零基础入门第二弹:让你的几何体 “活” 起来 ——Actor 基础与蓝图交互入门
c++·ue5·交互
sp_fyf_20242 小时前
【大语言模型】 语言模型学习什么以及何时学习?隐式课程假说
人工智能·学习·语言模型
星辰即远方2 小时前
UI学习2
学习·ui
lingggggaaaa2 小时前
PHP模型开发篇&MVC层&动态调试未授权&脆弱鉴权&未引用&错误逻辑
开发语言·安全·web安全·网络安全·php·mvc·代码审计
星原望野2 小时前
java:volatile关键字的作用
java·开发语言·volatile
APIshop2 小时前
Java获取淘宝商品价格、图片与视频:淘宝开放平台API实战指南
开发语言·python