Java和C++中的构建器有什么区别?

14

在Google的Protocol Buffer Java API中,它们使用了这些方便的Builder来创建一个对象(请参见此处):

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没有使用这样的构建器(请参见这里)。

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


2
我认为这很可能是C++实现者的个人偏好。在我的经验中,C++代码中并不会对构建器表示反感,事实上,在对象可能具有a)许多参数或(更有可能)b)许多可选参数的情况下,我会在各个地方使用它们。 - moswald
你在问题中没有注意到的一件事是,Person类是不可变的。 - BD at Rivenhill
6个回答

8
在C++中实现这样的功能的正确方法是使用返回*this引用的setter。
class Person {
  std::string name;
public:
  Person &setName(string const &s) { name = s; return *this; }
  Person &addPhone(PhoneNumber const &n);
};

假设已经定义了类似PhoneNumber的类,那么可以像这样使用该类:

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

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

1
请注意,这需要一个默认构造的 Person。如果每个 Person 都需要一个 id,则不能存在这样的构造函数。通过在创建对象之前收集参数,可以使用构建器来解决此问题。 - MSalters
@MSalters 在这些情况下,您应该使用与构建器类相同的习语(并且.build()成员函数在构建之前可以检查对象的有效性并返回Person对象)。 - hrnt
你的回答缺少一个重要的点,即 OP 忘记提到:Java 代码在这里使用了构建器模式,因为 Person 类被定义为不可变的,因此没有 setter 方法。 - BD at Rivenhill
1
这完全违背了模式的初衷:我们不想要有 setters,这就是为什么在 Java 中我们使用静态构建器类的原因。 - Rob
@Rob:C++的好处在于,你可以通过声明const来使变量p成为不可变类,这有效地删除了该特定对象的所有setter方法。然而,我可能仍然会选择一个显式的构建器类,以便有一个单一的函数,可以检查某个参数组合的有效性。 - MikeMB
你理解 const 的语义了吗?我同意你的结论。相当着迷于构建器。 - Rob

4
我会选择“不习惯使用”,尽管我在C++代码中看到过这种流畅接口风格的例子。
可能是因为有许多方法可以解决相同的基本问题。通常,在这里解决的问题是命名参数(或者说它们缺失)。一个更符合C ++特点的解决该问题的方法可能是 Boost's Parameter library

2
这段话的意思是:差异在一定程度上是惯用语的不同,但也是由于 C++ 库更加优化。在您的问题中,有一件事情您没有注意到,即 protoc 生成的 Java 类是不可变的,因此必须具有(可能)非常长的参数列表和没有 setter 方法的构造函数。在 Java 中,不可变模式通常用于避免与多线程相关的复杂性(以性能为代价),而建造者模式则用于避免在大型构造函数调用时眯眼和需要在代码中同时使用所有值的痛苦。protoc 生成的 C++ 类不是不可变的,设计为对象可以在多个消息接收中重复使用(请参见“C++ Basics Page”中的“优化提示”部分);它们因此更难以使用,但更高效。当然,两种实现方式本来可以用相同的风格编写,但开发人员似乎认为易用性对于 Java 更重要,而对于 C++,性能更重要,这或许反映了 Google 中这些语言的使用模式。

1

关于我的评论后续...

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";

1
您声称“C++和Java API应该做同样的事情”的说法是没有根据的。它们的文档不同,每种输出语言可以创建不同的结构解释。这样做的好处是每种语言都可以习惯地使用相应的表达方式。减少了一种感觉,就是你在用C++编写Java代码。如果每个消息类都有一个单独的Builder类,那肯定会让我有这样的感觉。
对于一个整数字段foo,从protoc生成的C++代码将包括一个方法void set_foo(int32 value),位于给定消息类中。
Java输出会生成两个类。一个直接表示消息,但仅具有字段的getter。另一个类是Builder类,只有字段的setter。
Python的输出也不同。生成的类将包括一个可以直接操作的字段。我预计,C、Haskell和Ruby的插件也是非常不同的。只要它们都能够表示一个可以转换为等效比特的结构,它们就完成了它们的工作。请记住,这些是“协议缓冲区”,而不是“API缓冲区”。

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


0
在C++中,您必须明确管理内存,这可能会使惯用法更难使用 - 要么build()必须调用构建器的析构函数,要么您必须将其保留以便在构造Person对象后删除它。对我来说,两者都有点可怕。

6
你能否通过将所有内容放在栈上来解决这个问题? - cobbal
4
或者使用智能指针(从某种意义上来说,这是一样的)。 - philsquared
6
在C++中临时对象是轻量级的,这个说法不正确。它们会在完整表达式结束后进行销毁,这在编译期就已经确定了。使用模板可以很容易地创建这样的构建器,因为你可以创建一个通用的构建器,而不需要特化。例如,Person = Builder().(&Person::id, 1234).(&Person::Name, "John Doe"); - MSalters
@MSalters,那个语法很丑,但我喜欢你关于临时对象的观点。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接