【问题标题】:Builders in Java versus C++?Java 与 C++ 的构建器?
【发布时间】:2010-02-19 07:40:50
【问题描述】:

在 Google 的 Protocol Buffer Java API 中,他们使用这些漂亮的 Builder 来创建对象(请参阅 here):

Person john =
  Person.newBuilder()
    .setId(1234)
    .setName("John Doe")
    .setEmail("jdoe@example.com")
    .addPhone(
      Person.PhoneNumber.newBuilder()
        .setNumber("555-4321")
        .setType(Person.PhoneType.HOME))
    .build();

但是对应的 C++ API 并没有使用这样的 Builder(见here

C++ 和 Java API 应该做同样的事情,所以我想知道他们为什么不在 C++ 中使用构建器。这背后是否有语言原因,即它不是惯用的,或者它在 C++ 中不受欢迎?或者可能只是编写 C++ 版本 Protocol Buffers 的人的个人喜好?

【问题讨论】:

  • 我认为这可能是 C++ 实现者的个人偏好。构建器在 C++ 代码中(至少根据我的经验)并不被讨厌,事实上,我在对象可能具有 a)许多参数或(更有可能)b)许多可选参数的地方都使用它们。跨度>
  • 您在问题中没有注意到的一件事是 Person 类是不可变的。

标签: java c++ design-patterns builder


【解决方案1】:

在 C++ 中实现类似功能的正确方法是使用返回对 *this 的引用的 setter。

class Person {
  std::string name;
public:
  Person &setName(string const &s) { name = s; return *this; }
  Person &addPhone(PhoneNumber const &n);
};

假设类似定义的电话号码,该类可以这样使用:

Person p = Person()
  .setName("foo")
  .addPhone(PhoneNumber()
    .setNumber("123-4567"));

如果需要一个单独的构建器类,那么也可以这样做。应该分配这样的建设者 当然是在堆栈中。

【讨论】:

  • 请注意,这需要默认构造的Person。如果每个Person 都需要一个id,则可能不存在这样的ctor。构建器可以通过在创建对象之前收集参数来解决问题。
  • @MSalters 实际上,在这些情况下,您应该对构建器类使用相同的惯用语(并且返回 Person 对象的 .build() 成员函数可以在构造之前检查对象的有效性)。
  • 您的答案缺少 OP 忘记提及的一个要点:Java 代码在这里使用了构建器模式,因为 Person 类被定义为不可变的,因此没有设置方法。
  • 这违背了该模式的全部目的:我们不想拥有 setter,这就是我们在 Java 中创建静态构建器类的原因。
  • @Rob:关于 c++ 的好处在于 - 您可以通过将变量 p 声明为 const 来使变量 p 不可变,而不是创建不可变类,这有效地删除了该特定对象的所有 setter 方法。但是,我可能仍然会选择一个显式的构建器类,以便拥有一个可以检查某个参数组合的有效性的函数。
【解决方案2】:

我会选择“不习惯”,尽管我在 C++ 代码中看到过这种流畅界面样式的示例。

这可能是因为有多种方法可以解决相同的潜在问题。通常,这里要解决的问题是命名参​​数的问题(或者更确切地说是它们的缺乏)。 Boost's Parameter library.

【讨论】:

    【解决方案3】:

    这种差异部分是惯用的,但也是 C++ 库被更大量优化的结果。

    您在问题中没有注意到的一件事是 protoc 发出的 Java 类是不可变的,因此必须具有带有(可能)非常长的参数列表且没有 setter 方法的构造函数。不可变模式在 Java 中常用来避免与多线程相关的复杂性(以牺牲性能为代价),而构建器模式用于避免在大型构造函数调用时眯着眼睛看的痛苦,并且需要同时拥有所有值点在代码中。

    protoc 发出的 C++ 类不是不可变的,其设计目的是使对象可以在多个消息接收中重复使用(请参阅C++ Basics Page 上的“优化技巧”部分);因此,它们使用起来更难、更危险,但效率更高。

    当然,这两个实现可以用相同的风格编写,但开发人员似乎认为易用性对 Java 更重要,而性能对 C++ 更重要,也许反映了使用模式Google 的这些语言。

    【讨论】:

      【解决方案4】:

      您声称“C++ 和 Java API 应该做同样的事情”是没有根据的。他们没有记录做同样的事情。每种输出语言都可以对 .proto 文件中描述的结构创建不同的解释。这样做的好处是,您在每种语言中所获得的内容都是该语言的惯用。它最大程度地减少了您正在“用 C++ 编写 Java”的感觉。如果每个消息类都有一个单独的构建器类,那肯定是我的感受

      对于整数字段 fooprotoc 的 C++ 输出将在给定消息的类中包含方法 void set_foo(int32 value)

      Java 输出将改为生成 两个 类。一个直接代表消息,但只有该字段的吸气剂。另一个类是构建器类,只有字段的设置器。

      Python 输出仍然不同。生成的类将包含一个您可以直接操作的字段。我预计 C、Haskell 和 Ruby 的插件也有很大不同。只要它们都可以表示可以转换为线路上的等效位的结构,它们就完成了它们的工作。请记住,这些是“协议缓冲区”,而不是“API 缓冲区”。

      C++ 插件的源代码随protoc 分发提供。如果您想更改 set_foo 函数的返回类型,欢迎您这样做。我通常会避免类似“它是开源的,所以任何人都可以修改它”的回答,因为建议某人学习一个全新的项目以进行重大更改以解决问题通常没有帮助。但是,我不认为在这种情况下会很难。最难的部分是找到为字段生成设置器的代码部分。一旦发现这一点,进行所需的更改可能会很简单。更改返回类型,并在生成的代码末尾添加return *this 语句。然后您应该能够以Hrnt's answer 中给出的样式编写代码。

      【讨论】:

        【解决方案5】:

        跟进我的评论...

        struct Person
        {
           int id;
           std::string name;
        
           struct Builder
           {
              int id;
              std::string name;
              Builder &setId(int id_)
              {
                 id = id_;
                 return *this;
              }
              Builder &setName(std::string name_)
              {
                 name = name_;
                 return *this;
              }
           };
        
           static Builder build(/* insert mandatory values here */)
           {
              return Builder(/* and then use mandatory values here */)/* or here: .setId(val) */;
           }
        
           Person(const Builder &builder)
              : id(builder.id), name(builder.name)
           {
           }
        };
        
        void Foo()
        {
           Person p = Person::build().setId(2).setName("Derek Jeter");
        }
        

        这最终会被编译成与等效代码大致相同的汇编程序:

        struct Person
        {
           int id;
           std::string name;
        };
        
        Person p;
        p.id = 2;
        p.name = "Derek Jeter";
        

        【讨论】:

          【解决方案6】:

          在 C++ 中,您必须显式管理内存,这可能会使该成语使用起来更加痛苦——build() 必须为构建器调用析构函数,否则您必须保留它以在构造Person 对象。 要么对我来说有点吓人。

          【讨论】:

          • 你不能把所有东西都放在堆栈上来解决这个问题吗?
          • 或使用智能指针(在某种程度上相当于同一件事)
          • 不正确 - C++ 中的临时对象是微不足道的。它们在完整表达式结束时被销毁,也就是在构建之后。使用模板,创建这样的构建器将是微不足道的,因为您可以创建一个通用的 - 不需要专门化。 IE。 Person = Builder(). (&Person::id, 1234).(&Person::Name, "John Doe");
          • @MSalters 男孩,语法很丑,但我喜欢你关于临时对象的观点。
          猜你喜欢
          • 1970-01-01
          • 2011-02-08
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2014-02-11
          相关资源
          最近更新 更多