Строители в Java по сравнению с С++?
В Google Protocol Buffer API для Java они используют эти красивые Builders, которые создают объект (см. здесь):
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("[email protected]")
.addPhone(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build();
Но соответствующий С++ API не использует такие Builders (см. здесь)
Предполагается, что С++ и Java API выполняют одно и то же, поэтому мне интересно, почему они также не использовали сборщики на С++. Существуют ли языковые причины этого, т.е. Это не идиоматично, или он нахмурился на С++? Или, возможно, только личное предпочтение человека, написавшего версию протоколов протокола С++?
Ответы
Ответ 1
Правильный способ реализовать что-то подобное в С++ будет использовать сеттеры, которые возвращают ссылку на * this.
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"));
Если требуется отдельный класс строителя, это также можно сделать. Такие строители должны быть выделены
в стеке, конечно.
Ответ 2
Я бы пошел с "не идиоматическим", хотя я видел примеры таких стилей свободного интерфейса в коде на С++.
Возможно, это связано с тем, что существует множество способов решения одной и той же основной проблемы. Обычно проблема, решаемая здесь, - это именованные аргументы (или, скорее, их отсутствие). Возможно, более С++-подобное решение этой проблемы может быть Библиотека параметров Boost.
Ответ 3
Ваше утверждение о том, что "С++ и Java API должны делать одно и то же", необоснованно. Они не документированы, чтобы делать то же самое. Каждый язык вывода может создавать другую интерпретацию структуры, описанной в файле .proto. Преимущество этого в том, что то, что вы получаете на каждом языке, является идиоматичным для этого языка. Это сводит к минимуму ощущение, что вы, скажем, "пишите Java на С++". Это определенно было бы тем, как я буду чувствовать, если для каждого класса сообщений есть отдельный класс строителя.
Для целочисленного поля foo
вывод С++ из protoc будет включать метод void set_foo(int32 value)
в классе для данного сообщения.
Выход Java вместо этого генерирует два класса. Один непосредственно представляет сообщение, но имеет только поля для поля. Другой класс - класс строителя и имеет только сеттеры для поля.
Выход Python по-прежнему отличается. Созданный класс будет включать поле, с которым вы можете напрямую манипулировать. Я ожидаю, что плагины для C, Haskell и Ruby также сильно отличаются. Пока они могут представлять структуру, которая может быть переведена на эквивалентные биты на проводе, они выполняют свою работу. Помните, что это "буферы протокола", а не "буферы API".
Источник для плагина С++ предоставляется с распределением протоков. Если вы хотите изменить тип возврата для функции set_foo
, вы можете это сделать. Обычно я избегаю ответов, на которые ссылаюсь: "Это с открытым исходным кодом, поэтому любой может его изменить", потому что обычно не рекомендуется рекомендовать, чтобы кто-то учил совершенно новый проект достаточно хорошо, чтобы внести серьезные изменения только для решения проблемы. Однако я не ожидаю, что в этом случае это будет очень сложно. Самая сложная часть - найти раздел кода, который генерирует сеттеры для полей. Как только вы обнаружите, что внесение изменений вам нужно, вероятно, будет простым. Измените тип возвращаемого значения и добавьте оператор return *this
в конец сгенерированного кода. Затем вы должны написать код в стиле, указанном в Hrnt answer.
Ответ 4
Чтобы следить за моим комментарием...
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";
Ответ 5
Разница частично идиоматична, но также является результатом более оптимизированной библиотеки С++.
Одна вещь, которую вы не заметили в своем вопросе, состоит в том, что классы Java, выпущенные protoc, неизменяемы и, следовательно, должны иметь конструкторы с (потенциально) очень длинными списками аргументов и без методов setter. Непрерывный шаблон обычно используется на Java, чтобы избежать сложностей, связанных с многопоточными (за счет производительности), и шаблон строителя используется, чтобы избежать боли прищуривания при больших вызовах конструктора и необходимости иметь все значения, доступные в одном и том же в коде.
Классы С++, выпущенные protoc, не являются неизменяемыми и предназначены для повторного использования объектов несколькими приемами сообщений (см. раздел "Советы по оптимизации" на С++ Basics Страница); они тем сложнее и опаснее использовать, но более эффективны.
Конечно, эти две реализации могли быть написаны в одном стиле, но разработчики, похоже, чувствовали, что простота использования важнее для Java, и производительность была более важной для С++, возможно, отражая шаблоны использования для эти языки в Google.
Ответ 6
В С++ вам нужно явно управлять памятью, что, вероятно, сделало бы идиому более болезненной - либо build()
должен вызвать деструктор для строителя, либо вы должны сохранить его, чтобы удалить его после создания Person
объект.
Для меня это немного страшно.