wtorek, 27 października 2009

Re: Pola i akcesory wewnątrz klas

Do napisania tego posta sprowokował mnie Xion, swoją notką. Kiedyś sam o tym troszkę rozmyślałem, i doszedłem do wniosku, że zwykle wewnątrz klasy lepiej jest się odwołać bezpośrednio do składowych. Są jednak pewne wyjątki. I tutaj historia z życia wzięta, ale rozpisywać się nie będę, bo jak wiadomo jeden pseudokod znaczy więcej niż tysiąc słów :P
class Scene {

// trzy wskazniki na glowne macierze 
const Matrix4x4* modelMatrix;
const Matrix4x4* viewMatrix;
const Matrix4x4* projMatrix;

// od groma wyliczonych z nich macierzy 
Matrix4x4 modelViewMatrix, modelViewProjMatrix, invModel, (...), invModelViewProjMatrix;

bool /* w oryginale tu jest bitset */ modelChanged, (...), invModelViewProjChanged; 

public:

// tylko trzy metody set 
void setModelMatrix(const Matrix4x4* m) const;
void setViewMatrix(const Matrix4x4* m) const;
void setProjMatrix(const Matrix4x4* m) const;

// od groma metod get
const Matrix4x4& getModelMatrix() const; 
(...) 
const Matrix4x4& getInvModelViewProj() const;

};
Do tego dodajmy klasę ShaderManager, która pobiera sobie te macierze (i nie tylko, ale nie będę mieszać tutaj za bardzo, bo do puenty nie dojdę nigdy). I teraz - jak słusznie zauważył Xion użycie metod set na zewnątrz klasy Scene nie budzi (nie powinno!) wątpliwości. W tym przypadku jest to również konieczne wewnątrz. Przyczyna? Ktoś kto uważnie przeczytał pseudokod pewnie się domyśla, że liczenie (bądź co bądź kosztowne, szczególnie w przypadku inverse) macierzy jest odłożone do pierwszego get. Natomiast set aktualizuje odpowiednie wartości "changed". W przypadku nieużywania set trzeba te wartości zmienić ręcznie, co powoduje generalny syf w kodzie.

Pojawia się tutaj pytanie - co jeśli ktoś używa naszej klasy i o tym nie wie, i napisze gdzieś po prostu modelMatrix = identity4x4. Albo (o zgrozo :P) my sami mamy już tak rozdmuchaną klasę, że o tym zapominamy. Znalezienie błędu potem może być dosyć kosztowne. Moje rozwiązanie (które stosuję) polega na wywaleniu całej logiki związanej z macierzami do klasy SceneMatrix, gdzie macierze są w prywatnym obszarze a metody w publicznym, i dziedziczenie po niej. Zaleta jest taka, że w zależności od potrzeb możemy dziedziczyć publicznie lub prywatnie i w ten sposób udostępniać lub chować metody get / set na zewnątrz. Przy takim podejściu nie ma mowy o "zapomnieniu", że trzeba użyć set zamiast operatora=. Wadą jest to, że zwykle prowadzi to do wielodziedziczenia, ale moim zdaniem nie jest to ani błąd ani problem w takim przypadku.

2 komentarze:

  1. Uogólniając ten przykład, można by powiedzieć, że jeśli zmiana wartości pola A powoduje skutki uboczne widoczne, to powinniśmy używać akcesorów. Rzecz w tym, że nie wszystkie takie skutki uboczne są pożądane. Oto przykład - można go potraktować jako kontrprzykład to tego twojego :)

    class Object
    {
    private:
    Object* parent;
    list<Object*> children;
    public:
    explicit Object(Object* p = NULL) : parent(p) { }

    Object* Parent() {return parent;}
    void SetParent(Object*);

    void AddChild(Object*);
    void RemoveChild(Object*);
    };

    Jak widać, jest to drzewo obiektów z odwołaniami w obie strony (do potomków i rodziców). Mamy tez trzy samodzielne funkcje zmieniające relacje rodzic-potomek dla dwóch obiektów.
    Tutaj Parent() i SetParent() są też akcesorami pola parent.

    Gdzie jest haczyk? Metoda AddChild robi oczywiście dwie rzeczy: dodaje parametr do kolekcji this->children, a następnie - no właśnie - zmienia mu rodzica. Powinna więc użyć do tego SetParent(), prawda?... Otóż nie - SetParent() jest przecież też funkcją publiczną, więc musi poprawnie ustawiać rodzica także wtedy, gdy jest wywoływana z zewnątrz. Dlatego tak naprawdę ustawienie obiektowi rodzica oznacza usunięcie go z kolekcji dzieci jednego (RemoveChild) oraz dodanie do kolekcji dzieci drugiego (AddChild, a jakże).

    Używając akcesorów w obu metodach wpadniemy w nieskończoną rekurencję, której rozwiązaniem jest bezpośrednie sięgnięcie do pól parent lub children.

    OdpowiedzUsuń
  2. No więc właśnie. Czyli generalnie wniosek jest prosty - zawsze trzeba wiedzieć jak klasa będzie używana, po co, do czego i w ogóle, i tak zaprojektować, żeby to używanie było jak najłatwiejsze, a co za tym idzie, powodowało jak najmniej błędów. W przypadku który podałem ja, błędo-genne jest nie używanie akcesorów, w przypadku Twoim - niewłaściwe używanie :) Po prostu trzeba myśleć, nie ma przepisu na wszystko :P

    OdpowiedzUsuń