2010-02-19 17 views
10

W Google Protocol Buffer API for Java, używają tych miłych Builders że utworzenie obiektu (patrz here):Konstruktorzy w języku Java kontra 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(); 

ale odpowiadający C++ API nie korzysta z takich wypełniaczy (patrz here)

C++ i Java API ma działać tak samo, więc zastanawiam się, dlaczego nie używali też budowniczych w C++. Czy są jakieś powody językowe, tzn. Nie jest to idiomatyczne, czy też jest źle napisane w C++? A może po prostu osobiste preferencje osoby, która napisała wersję buforów protokołów C++?

+2

Myślę, że to prawdopodobnie osobiste preferencje implementatora C++. Konstruktorzy nie są (przynajmniej w moim odczuciu) źle widziani w kodzie C++, i faktycznie używam ich wszędzie tam, gdzie obiekt może mieć) wiele parametrów lub (bardziej prawdopodobne) b) wiele opcjonalnych parametrów. – moswald

+0

Jedną z rzeczy, których nie zauważyłeś w swoim pytaniu, jest to, że klasa Osoba jest niezmienna. –

Odpowiedz

0

W C++ trzeba wyraźnie zarządzania pamięcią, który zapewne uczyniłoby idiom bardziej bolesne w użyciu - zarówno build() musi wywołać destruktor dla budowniczego, albo trzeba trzymać go dookoła, aby go usunąć po konstruowania Person obiekt. Albo jest dla mnie trochę przerażająca.

+6

Nie możesz tego obejść, zachowując wszystko na stosie? – cobbal

+4

lub używanie inteligentnych wskaźników (w pewnym sensie to samo) – philsquared

+6

Po prostu nie prawda - tymczasowe obiekty w C++ są trywialne. Są one niszczone na końcu pełnego wyrażenia, które jest po kompilacji. A dzięki szablonom tworzenie takich budowniczych byłoby banalne, ponieważ można utworzyć ogólny - nie wymaga specjalizacji. To znaczy. 'Person = Builder(). (& Person :: id, 1234). (& Person :: Name, "John Doe"); ' – MSalters

6

Właściwy sposób na zaimplementowanie czegoś takiego w C++ będzie używał setterów, które zwracają odwołanie do * this.

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

Klasa może być stosowany tak, zakładając, podobnie zdefiniowane PhoneNumber:

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

Jeśli oddzielna klasa budowniczym jest poszukiwany, to można to zrobić też. Tacy budowniczowie powinni oczywiście zostać przydzieleni w stosie.

+1

Należy zauważyć, że wymaga to skonstruowanej domyślnie "osoby". Jeśli każda "Osoba" potrzebuje "id", nie może istnieć taki ctor. Konstruktor może rozwiązać problem, zbierając argumenty przed utworzeniem obiektu. – MSalters

+0

@MSalters Rzeczywiście, w tych przypadkach powinieneś używać tego samego idiomu z klasą budowniczego (i funkcją składową .build(), która zwraca obiekt Person może sprawdzić ważność obiektu przed budową). – hrnt

+0

W twojej odpowiedzi brakuje jednego głównego punktu, o którym OP zapomniało wspomnieć: kod Java używa tutaj wzorca budowniczego, ponieważ klasa Person jest zdefiniowana jako niezmienna i dlatego nie ma żadnych metod ustawiających. –

4

Poszedłbym z "nie idiomatycznym", chociaż widziałem przykłady takich płynnie interfejsów stylów w kodzie C++.

Możliwe, że istnieje wiele sposobów rozwiązania tego samego problemu. Zwykle rozwiązywany problem dotyczy nazwanych argumentów (a raczej ich braku). Prawdopodobnie rozwiązaniem tego problemu może byćC++ - Boost's Parameter library.

1

Twoje twierdzenie, że "C++ i Java API mają działać tak samo" są bezpodstawne. Nie są udokumentowane, aby robić to samo. Każdy język wyjściowy może tworzyć inną interpretację struktury opisanej w pliku .proto. Zaletą tego jest to, że to, co dostajesz w każdym języku, jest idiomatic dla tego języka. Minimalizuje uczucie, że jesteś, powiedzmy, "pisanie Javy w C++". Byłoby to z pewnością odczuwane, gdyby istniała osobna klasa Builder dla każdej klasy wiadomości.

dla pola całkowitej foo, C++ wyjściowy protoc będzie obejmować sposób void set_foo(int32 value) w klasy dla danego komunikatu.

Wyjście Java zamiast tego generuje dwie klasy. Jeden bezpośrednio reprezentuje komunikat, ale ma tylko przyrostki dla pola. Druga klasa to klasa budowniczych i ma tylko seterów dla pola.

Dane wyjściowe w języku Python są inne. Generowana klasa będzie zawierała pole, które możesz bezpośrednio manipulować. Spodziewam się, że wtyczki dla C, Haskell i Ruby również są całkiem inne. Dopóki wszystkie one mogą reprezentować strukturę, która może być przetłumaczona na równoważne bity na drucie, wykonują swoją pracę.Pamiętaj, że są to "bufory protokołów", a nie "bufory API".

Źródło wtyczki C++ jest dostarczane z dystrybucją protokołu. Jeśli chcesz zmienić typ zwrotu dla funkcji set_foo, możesz to zrobić. Zwykle staram się unikać odpowiedzi, które polegają na: "Jest to oprogramowanie open source, więc każdy może je modyfikować", ponieważ zwykle nie zaleca się, aby ktoś nauczył się zupełnie nowego projektu na tyle, aby wprowadzić istotne zmiany tylko w celu rozwiązania problemu. Nie sądzę jednak, by w tym przypadku było to bardzo trudne. Najtrudniejszą częścią byłoby znalezienie sekcji kodu, która generuje setery dla pól. Kiedy już to zauważysz, zmiana, której będziesz potrzebować, będzie najprawdopodobniej prosta. Zmień typ zwracania i dodaj instrukcję return *this na końcu generowanego kodu. Powinieneś wtedy móc napisać kod w stylu podanym w Hrnt's answer.

1

Aby śledzić na mój komentarz ...

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

ten kończy się uzyskiwanie skompilowany do mniej więcej w tym samym asemblerze jako odpowiednik kodu:

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

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

Różnica jest częściowo idiomatyczne, ale jest również wynik, w którym biblioteka C++ jest bardziej zoptymalizowana.

Jedną z rzeczy, o której nie wspomniałeś w swoim pytaniu jest to, że klasy Java emitowane przez protoc są niezmienne i dlatego muszą mieć konstruktory z (potencjalnie) bardzo długimi listami argumentów i bez metod ustawiania. Niezmienny wzór jest często używany w języku Java, aby uniknąć złożoności związanej z wielowątkowością (kosztem wydajności), a wzorzec budowniczego jest używany w celu uniknięcia bólu mrużącego przy dużych wywołaniach konstruktora i konieczności posiadania wszystkich dostępnych wartości w tym samym czasie. wskaż w kodzie.

Klasy C++ emitowane przez protoc nie są niezmienne i są zaprojektowane w taki sposób, że obiekty mogą być ponownie wykorzystane przez wiele przyjęć (zobacz sekcję "Porady dotyczące optymalizacji" na stronie C++ Basics Page); są zatem trudniejsze i bardziej niebezpieczne w użyciu, ale bardziej wydajne.

Z pewnością dwie implementacje mogły być napisane w tym samym stylu, ale twórcy zdawali się czuć, że łatwość użycia była ważniejsza dla Javy, a wydajność była ważniejsza dla C++, być może odzwierciedlająca wzorce użytkowania dla tych języków w Google.