2013-04-26 13 views
7

Zastanawiam się nad dobrymi praktykami w C++, a ja stanąłem przed problemem zrobienia gettera/setera dla członka klasy.Powrócić przez odwołanie lub utworzyć typowy setter/getter?

Dlaczego więc po prostu nie zwracamy elementu przez odniesienie, w ten sposób mogę zmodyfikować lub uzyskać dostęp do jego wartości, aby go przeczytać? W szczególności jest to mój kod:

class Chest : public GameObject 
{ 
public: 
    Chest(); 
    ~Chest(); 

    static int& num_chests(); 

private: 
    static int num_chests_; 
}; 

Czy to jest zła praktyka? Czy powinienem ich użyć zamiast tego?

+6

Jeśli getter nie robi nic, a seter nic nie robi, to oba są błędne. Wpisz 'num_chests_' jako publiczny. W przeciwnym razie wymagany jest drugi. Niewiarygodnie rzadkie jest zwracanie członka przez odniesienie. –

+2

Możesz też użyć opcji 3 i ustawić 'num_chest' jako" publiczny "element danych, zwłaszcza jeśli twój seter nie musi wykonywać żadnych testów sprawdzających. – Praetorian

+3

Nie sądzę, że jest to szczególnie złe ćwiczenie. Jest to popularny język w C++ do używania/zwracania odnośników. –

Odpowiedz

10

Jeśli nie czujesz się do tego zbyt mocno, użyj funkcji członka gettera i setera.

Powodem, dla którego int& num_chests() lub publiczne pole jest złe, jest powiązanie kodu klienta, który używa wartości num_chests z faktem, że w rzeczywistości jest to pole (wewnętrzny szczegół implementacji).

Załóżmy, że później zdecydowałeś, że będziesz mieć prywatne pole z klasy std::vector<Chest> chests. Wtedy nie chciałbyś mieć pola int num_chests - jest strasznie niepotrzebny. Chciałbyś mieć int num_chests() { return chests.size(); }.

Jeśli korzystasz z publicznego pola, teraz cały kod klienta musi korzystać z tej funkcji zamiast poprzedniego dostępu do pola - każde użycie wartości num_chests musi zostać zaktualizowane, ponieważ interfejs się zmienił.

Jeśli używasz funkcji, która zwraca referencję, masz teraz problem, ponieważ chests.size() jest zwrotem według wartości - nie możesz z kolei zwrócić tego przez odniesienie.

Zawsze hermetyzuj swoje dane. Wymaga tylko minimalnej ilości kodu.

W odpowiedzi na komentarze, że powinniśmy po prostu użyć pól publicznych:

Pamiętaj, że tylko zaletą korzystania z pól publicznych (inne niż odległej możliwości jakiegoś mikro optymalizacji) jest to, że don” t muszą napisać kod na płycie głównej. "Mój nauczyciel nienawidził kiedy używał publicznych pól (i był taki irytujący)" jest bardzo słabym argumentem za używaniem pól publicznych.

+0

Czy możesz powiedzieć, że klasa pary jest złym projektem? – Rayniery

+1

@Rayniery No. Projekt jest taki, że jeśli mam 'pair p', pola' p.first' i 'p.second' mają zmienną semantykę tak samo jak zmienna' p'. Interfejsem i semantyką * jest * to, że są zmiennymi. Typ 'pair ' (i 'tuple' również) służy jako lekki sposób wiązania zmiennych (lub wartości) razem - na pewno nie jako narzędzie do enkapsulacji danych! –

2

W niemal każdym przypadku, gdy myślisz, że musisz stworzyć setera AND getter (czyli w tym samym czasie), twój projekt jest nieprawidłowy.

Zastanów się, jaki cel ma num_chests? Nie możesz iść nigdzie, nie wiedząc, co to jest.

Według twojego kodu, zgaduję, że zawiera on liczbę skrzyń na poziomie. W takim przypadku nie chcesz dostarczać setera dla tej wartości dla wszystkich. Chcesz, aby wartość była równa liczbie skrzyń w grze, a dostarczając setera, każdy może unieważnić ten niezmiennik.

Zamiast tego możesz oferować TYLKO getter, a możesz kontrolować jego wartość w klasie.

class Chest : public GameObject 
{ 
public: 
    Chest() { ++num_chests_; } 
    ~Chest() { --num_chests_; } 

    static int num_chests() { return num_chests_; } 

private: 
    static int num_chests_; 
}; 

int Chest::num_chests_ = 0; 

Więcej wyjaśnień na temat tego, dlaczego podmioty pobierające i ustawiające są złymi decyzjami w mojej opinii. Jeśli podasz setter i getter, masz tylko złudzenie kontroli nad zmienną. Rozważ std::complex.Dlaczego

std::complex<double> my_complex(1.0, 5.0); 
my_complex.real(my_complex.real()+1); 

lepiej nad

std::complex<double> my_complex(1.0, 5.0); 
my_complex.real()++; 

odpowiedź: kiedy std::complex został zaprojektowany, nie było żadnych odniesień w C++. Ponadto, Java nie ma odniesień w stylu C++, więc muszą napisać kod standardowego elementu wszędzie. Teraz GCC zwraca const odniesienia tu jako przedłużenie i C++ 11 pozwala

reinterpret_cast<double (&)[2]>(non_const_non_volatile_complex_variable)[0] 

i

reinterpret_cast<double (&)[2]>(non_const_non_volatile_complex_variable)[1] 

jako prawidłowy sposób dostępu do rzeczywistych i urojonych części z std::complex<value>.

+0

Podoba mi się twoja odpowiedź. Motywujesz do myślenia o celu członka, zamiast mówić, aby zawsze robić to, czy tamto. Myślę, że jest to jeden z głównych problemów związanych z projektowaniem oprogramowania: jesteśmy tak przyzwyczajeni do "przepisów", które każą nam robić jedną rzecz, zamiast motywować nas do myślenia o szczegółach projektu i domeny pod ręką. – Rayniery

+0

"Za każdym razem, gdy myślisz, że musisz stworzyć setera AND getter (czyli obie jednocześnie), twój projekt jest nieprawidłowy." - Może być trochę za mocny, by powiedzieć, że jest "niewłaściwy" ... Z pewnością jest powód, aby podwójnie sprawdzić swój projekt, ale istnieją uzasadnione przypadki, w których można uzyskać zdobywcę i setera. –

+0

@ TimothyShields Oczywiście, nie ma srebrnej kulki w programowaniu, ale przez większość czasu jest to błąd projektowy. – milleniumbug

2

Celem interfejsu jest nie być najprostszych do programu, ale najprostsze do użytku i przedłużyć.

Jeśli nie dostarczysz metod ustawiających i pobierających, ustawiasz się na późniejsze bóle głowy. Na przykład:

  • co się stanie, jeśli chcesz wysłać powiadomienie, gdy ktoś zmieni wartość num_chests?
  • Co się stanie, jeśli trzeba sprawdzić poprawność niż num_chests nie może być ujemny?
  • co się stanie, jeśli trzeba uruchomić program w środowisku wielowątkowym i trzeba zablokować odczyty, dopóki zapisy nie będą gotowe?

Jak widać, interfejs, który jest przezroczysty dla użytkownika, jest również prostszy w ochronie przed błędami użytkownika, a także przedłuża się w przyszłości; ta przewaga ma bardzo niewielki (jeśli w ogóle) dodatkowy koszt.

Z drugiej strony, czasami chcesz zwrócić referencję lub wskaźnik do elementu wewnętrznego. Na przykład klasy kontenerów w bibliotece standardowej często oferują metodę data(), która pobiera wskaźnik do podstawowego kontenera (oba w odmianach const i innych niż const).

Więc nie jest to trudna zasada, ale chciałbym powiedzieć, że powracające niewiążące odniesienia do prywatnych członków pokonują cel programowania OO.

0

Dążę do niskiego sprzężenia i wysokiej spójności; Unikam getterów i seterów.

Jeśli masz moduły pobierające i ustawiające, inny obiekt będzie musiał wiedzieć, jak je wywoływać. To sprzężenie.

Spróbuj oddzielić przedmioty od siebie, ale spój je. Przez spójność rozumiem, że działają dobrze z resztą systemu.

Jaki jest twój system? Dlaczego masz pobierające i ustawiające? Ponieważ chcesz kontrolować i wyświetlać te obiekty. Są one modelem i masz kontrolerów i widoki na nich.

Łatwo wpaść w pułapkę posiadania sprzężenia pomiędzy kontrolą/widokiem a modelem.

Aby uniknąć sprzężenia, pozwól, aby model utworzył kontrolkę i zaktualizuj widok. Wtedy nie będzie musiał mieć żadnych pobierających lub ustawiających.

np.

struct Instrumenter { 
    virtual void addRange(int& value, int min, int max) = 0; 
}; 

struct Renderer { 
    virtual void render(std::string& description, int value) = 0; 
}; 

struct GameObject { 
    virtual void instrument(Instrumenter& instrumenter) = 0; 
    virtual void display(Renderer& renderer) = 0; 
}; 

struct Chest : GameObject { 
    virtual void instrument(Instrumenter& instrumenter) { 
     intrumenter.addRange(count, 0, 10); 
    } 
    virtual void display(Renderer& renderer) { 
     renderer.render("Chest count", count); 
    } 
private: 
    int count; 
}; 

Wtedy można go używać tak:

int main() { 
    vector<shared_ptr<GameObject>> gameObjects; 
    MyControlInstrumenter instrumenter; 
    // ... 
    for(auto& gameObject: gameObjects) { 
     gameObject->instrument(instrumenter); 
    } 
    // etc. 
} 
Powiązane problemy