问题 Java与C ++的构建者?


在Google的 协议缓冲区 API for Java,他们使用这些很好的构建器来创建一个对象(参见 这里):

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 ++中不受欢迎?或者可能只是编写C ++版协议缓冲区的人的个人偏好?


4668
2018-02-19 07:40


起源

我认为这可能是C ++实现者的个人偏好。构建者不是(根据我的经验,至少)在C ++代码中不赞成,事实上,我在对象可能具有a)许多参数或(更可能)b)许多可选参数的地方使用它们。 - moswald
你在问题中没有注意到的一件事是Person类是不可变的。 - BD at Rivenhill


答案:


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

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


6
2018-02-19 07:58



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


虽然我已经在C ++代码中看到了这种流畅的界面风格的例子,但我会选择“非惯用语”。

这可能是因为有很多方法可以解决同样的潜在问题。通常,这里要解决的问题是命名参​​数(或者更确切地说是缺少参数)。可以说更多 C ++ - 像 解决这个问题的方法可能是 Boost的参数库


4
2018-02-19 08:18





您声称“C ++和Java API应该做同样的事情”是没有根据的。他们没有记录做同样的事情。每种输出语言都可以创建.proto文件中描述的结构的不同解释。这样做的好处是你在每种语言中得到的都是惯用的 对那种语言。它最大限度地减少了你用“用C ++编写Java”的感觉。那绝对是怎么回事 ID 感觉每个消息类是否有单独的构建器类。

对于整数字段 foo,C ++输出 protoc 将包括一种方法 void set_foo(int32 value) 在给定消息的类中。

而是生成Java输出  类。一个直接表示消息,但只有该字段的getter。另一个类是构建器类,只有该字段的setter。

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

C ++插件的源代码随附 protoc 分配。如果要更改的返回类型 set_foo 功能,欢迎你这样做。我通常会避免相应的回答,“它是开源的,所以任何人都可以对其进行修改”,因为建议有人学习一个全新的项目,以便为解决问题做出重大改变通常不会有帮助。但是,我不认为在这种情况下会非常困难。最难的部分是找到为字段生成setter的代码部分。一旦找到它,进行所需的更改可能会很简单。更改返回类型,然后添加一个 return *this语句到生成代码的结尾。然后,您应该能够以给定的样式编写代码 赫恩的回答


1
2018-02-26 16:17





跟进我的评论......

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
2018-02-26 16:46





差异部分是惯用的,但也是C ++库更加优化的结果。

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

protoc发出的C ++类不是不可变的,其设计使得对象可以在多个消息接收上重用(请参阅“优化提示”部分) C ++基础知识页面);因此它们使用起来更难,更危险,但效率更高。

当然,两个实现可以用相同的样式编写,但是开发人员似乎觉得易用性对于Java来说更重要,性能对于C ++来说更重要,可能反映了这些语言的使用模式。谷歌。


1
2018-02-04 17:54





在C ++中,你必须明确地管理内存,这可能会使得习惯用法更加痛苦 - 或者 build() 必须为构建器调用析构函数,否则你必须保留它以在构造之后删除它 Person 目的。 要么对我有点害怕。


0
2018-02-19 07:47



难道你不能通过保持堆栈中的一切来解决这个问题吗? - cobbal
或使用智能指针(在某种程度上相同的东西) - philsquared
事实并非如此 - C ++中的临时对象是微不足道的。它们在完整表达式结束时被销毁,这是在构建之后。使用模板,创建这样的构建器将是微不足道的,因为您可以创建一个通用的 - 不需要专业化。 IE浏览器。 Person = Builder(). (&Person::id, 1234).(&Person::Name, "John Doe"); - MSalters
@MSalters男孩,语法很难看,但我喜欢你关于临时对象的观点。 - Rob