2010-02-19 5 views
10

In Protocol Buffer API di Google per Java, usano queste belle Costruttori che creano un oggetto (vedi here):Costruttori in Java contro C++?

Person john = 
    Person.newBuilder() 
    .setId(1234) 
    .setName("John Doe") 
    .setEmail("[email protected]") 
    .addPhone(
     Person.PhoneNumber.newBuilder() 
     .setNumber("555-4321") 
     .setType(Person.PhoneType.HOME)) 
    .build(); 

Ma il corrispondente API C++ non usa tali Costruttori (vedi here)

Il C++ e si suppone che le API Java stiano facendo la stessa cosa, quindi mi chiedo perché non abbiano usato i builder anche in C++. Ci sono ragioni linguistiche dietro a ciò, cioè non è idiomatico o è disapprovato in C++? O probabilmente solo la preferenza personale della persona che ha scritto la versione C++ di Protocol Buffers?

+2

Penso che sia probabile la preferenza personale dell'implementatore C++. I costruttori non sono (nella mia esperienza, almeno) disapprovati nel codice C++, e infatti li uso ovunque nel punto in cui un oggetto può avere a) molti parametri o (più probabilmente) b) molti parametri opzionali. – moswald

+0

una cosa che non hai notato nella tua domanda è che la classe Person è immutabile. –

risposta

0

In C++ è necessario gestire in modo esplicito la memoria, che probabilmente rendere il linguaggio più doloroso da usare - sia build() deve chiamare il distruttore per il costruttore, altrimenti si devono tenere in giro per eliminarlo dopo la costruzione del Person oggetto. O è un po 'spaventoso per me.

+6

Non potresti aggirare questo mantenendo tutto in pila? – cobbal

+4

o utilizzando puntatori intelligenti (che equivale alla stessa cosa, in un certo senso) – philsquared

+6

Non è vero - gli oggetti temporanei in C++ sono banali. Sono distrutti alla fine della piena espressione, che è dopo la costruzione. E con i modelli, la creazione di tali builder sarebbe banale, dato che è possibile crearne uno generico - non ha bisogno di specializzazione. Vale a dire. 'Persona = Builder(). (& Person :: id, 1234). (& Person :: Name, "John Doe"); ' – MSalters

6

Il modo corretto di implementare qualcosa del genere in C++ utilizza i setter che restituiscono un riferimento a * questo.

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

La classe potrebbe essere utilizzato in questo modo, assumendo PhoneNumber simile definita:

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

Se una classe separata costruttore è voluto, quindi che può essere fatto anche. Tali costruttori dovrebbero essere allocati nello stack , ovviamente.

+1

Si noti che questo richiede un 'Person' predefinito costruito. Se ogni 'Persona' ha bisogno di un 'id', non può esistere un tale ctor. Un costruttore può risolvere il problema raccogliendo gli argomenti prima di creare l'oggetto. – MSalters

+0

@MSalters In effetti, in questi casi è necessario utilizzare lo stesso idioma con la classe builder (e la funzione membro .build() che restituisce l'oggetto Persona potrebbe verificare la validità dell'oggetto prima della costruzione). – hrnt

+0

Alla tua risposta manca un punto importante che l'OP ha dimenticato di menzionare: il codice Java sta usando il modello di builder qui perché la classe Person è definita come immutabile e quindi non ha metodi setter. –

4

Vorrei andare con il "non idiomatico", anche se ho visto esempi di tali stili di interfaccia fluente in codice C++.

Può essere perché ci sono diversi modi per affrontare lo stesso problema di fondo. Di solito, il problema da risolvere qui è quello degli argomenti con nome (o piuttosto la loro mancanza). Una soluzione probabilmente più C++ - come a questo problema potrebbe essere Boost's Parameter library.

1

L'affermazione secondo cui "il C++ e l'API Java dovrebbero fare la stessa cosa" è infondata. Non sono documentati per fare le stesse cose. Ogni lingua di output può creare una diversa interpretazione della struttura descritta nel file .proto. Il vantaggio è che ciò che ottieni in ogni lingua è idiomatico per quella lingua. Riduce al minimo la sensazione che tu sia, per esempio, "scrivendo Java in C++". Sarebbe sicuramente come mi piacerebbe sentire se ci fosse una classe di builder separata per ogni classe di messaggio.

Per un campo intero foo, C++ uscita dal ProtoC includerà un metodo void set_foo(int32 value) nella classe di messaggio dato.

L'uscita Java genererà invece due classi. Uno rappresenta direttamente il messaggio, ma ha solo getter per il campo. L'altra classe è la classe del costruttore e ha solo setter per il campo.

L'output Python è diverso ancora. La classe generata includerà un campo che puoi manipolare direttamente. Mi aspetto che i plug-in per C, Haskell e Ruby siano anche molto diversi. Finché tutti possono rappresentare una struttura che può essere tradotta in bit equivalenti sul filo, hanno fatto il loro lavoro.Ricordare che questi sono "buffer di protocollo", non "buffer di API".

L'origine del plug-in C++ è fornita con la distribuzione protoc. Se si desidera modificare il tipo di ritorno per la funzione , è possibile farlo. Normalmente evito le risposte che equivalgono a "È open source, quindi chiunque può modificarlo" perché di solito non è utile raccomandare che qualcuno apprenda un progetto completamente nuovo abbastanza bene da apportare grandi cambiamenti solo per risolvere un problema. Tuttavia, non mi aspetto che sarebbe molto difficile in questo caso. La parte più difficile sarebbe trovare la sezione di codice che genera setter per i campi. Una volta che lo trovi, fare il cambiamento che ti serve sarà probabilmente semplice. Modificare il tipo di reso e aggiungere un'istruzione return *this alla fine del codice generato. Dovresti quindi essere in grado di scrivere codice nello stile dato in Hrnt's answer.

1

dare un seguito a mio commento ...

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

Questo finisce per essere compilato nel all'incirca lo stesso assemblatore come il codice equivalente:

struct Person 
{ 
    int id; 
    std::string name; 
}; 

Person p; 
p.id = 2; 
p.name = "Derek Jeter"; 
1

La differenza è in parte idiomatica, ma è anche il risultato della libreria C++ è stata ottimizzata più pesantemente.

Una cosa che non è riuscita a notare nella domanda è che le classi Java emesse da protoc sono immutabili e quindi devono avere costruttori con (potenzialmente) elenchi di argomenti molto lunghi e metodi senza setter. Il pattern immutabile viene utilizzato comunemente in Java per evitare la complessità legata al multithreading (a scapito delle prestazioni) e il pattern builder viene utilizzato per evitare il fastidio di strizzare gli occhi alle invocazioni del costruttore di grandi dimensioni e la necessità di avere tutti i valori disponibili allo stesso indicare il codice.

classi

Il C++ emessi dal ProtoC non sono immutabili e sono progettati in modo che gli oggetti possono essere riutilizzati su più ricezioni di messaggi (vedere la sezione "Suggerimenti per l'ottimizzazione" sul C++ Basics Page); sono quindi più difficili e più pericolosi da usare, ma più efficienti.

È certamente il caso che le due implementazioni avrebbero potuto essere scritte nello stesso stile, ma gli sviluppatori sembravano sentire che la facilità di utilizzo era più importante per Java e le prestazioni erano più importanti per C++, forse rispecchiando i modelli di utilizzo per queste lingue su Google.