Kiedy na studiach miałem pierwszy wykład o dziedziczeniu w obiektowych językach programowania (akurat na przykładzie C++), to zapamiętałem następujący przykład - prostokąt dziedziczy po kwadracie.
Przykład ten wyglądał pewnie mniej więcej tak:
class Square
{
protected:
double x;
};
class Rectangle : public Square
{
protected:
double y;
};
Od razu widać tu bezsens - przecież kwadrat jest szczególnym przypadkiem prostokąta, a nie na odwrót. Każdy kwadrat jest prostokątem, ale nie każdy prostokąt jest kwadratem. Czyli zgodnie z zasadami matematyki i logiki to kwadrat powinien dziedziczyć po prostokącie.
I tu zaczynają się problemy.
Wynikają one z faktu, iż metody wirtualne znajdujące się w klasie nadrzędnej mogą zmodyfikować stan obiektu tak, że warunek narzucony na klasę podrzędną nie zostanie zachowany. W przypadku kwadratów i prostokątów operacją taką jest modyfikacja długości boków.
Rozwiązaniem tego problem jest zaprojektowanie klas kwadratu i prostokąta tak, aby zachowywały się jak stałe. Eliminuje to wszystkie metody modyfikujące stan obiektu (mutatory) - każda taka metoda zwraca nową instancję klasy Prostokąt i nie ma problemy. Natomiast jeżeli chcemy dopuścić mutatory, to czas pójść na kompromisy.
Jednym z rozwiązań może być modyfikacja obu wymiarów kiedy mamy do czynienia z kwadratem. W C++ wyglądałoby to tak:
class Rectangle {
public:
Rectangle(double x, double y) : _x(x), _y(y) { }
virtual double getX() { return _x; }
virtual double getY() { return _y; }
virtual void setX(double x) { _x = x; }
virtual void setY(double y) { _y = y; }
protected:
double _x;
double _y;
};
class Square : public Rectangle {
public:
Square(double x) : Rectangle(x, x) { }
virtual void setX(double x) { _x = _y = x; }
virtual void setY(double y) { _x = _y = y; }
};
Problem z tym rozwiązaniem jest taki, że ktoś pracując ze wskaźnikiem na prostokąt r może się zdziwić, jeżeli jest to kwadrat:
r->getX(); // tu mamy np. 2
r->setY(3); // zmieniamy Y na 3
r->getX(); // ups, X == 3 !
Innym rozwiązaniem jest zwracanie wartości logicznej przez metody modyfikujące - false jest zwracany jeżeli taka modyfikacja nie jest dozwolona. W linku na końcu posta jest jeszcze parę podobnych rozwiązań-obejść.
Ale gdzie tak naprawdę tkwi problem? W trzech literkach: LSP. Zasada Podstawienia Liskov (Liskov Substitution Principle) to zasada programowania obiektowego autorstwa pani Barbary Liskov:
Jeżeli q(x) jest własnością wszystkich obiektów x typu T, to q(y) powinno być własnością wszystkich obiektów y typu S, gdzie S jest podtypem T.
A tak trochę bardziej praktycznie:
Funkcje które używają wskaźników lub referencji do klas bazowych, muszą być w stanie używać również obiektów klas dziedziczących po klasach bazowych, bez dokładnej znajomości tych obiektów.
W problemie kwadrat-prostokąt własnością q jest "zmiana x nie wymusza zmiany y". Dla każdego prostokąta jest to prawda, natomiast nie jest to prawda dla kwadratów. Czyli problem tkwi w samym fakcie, iż kwadrat jest podtypem prostokąta.
Więcej:
http://en.wikipedia.org/wiki/Circle-ellipse_problem
http://en.wikipedia.org/wiki/Liskov_substitution_principle
piątek, 23 lipca 2010
Subskrybuj:
Komentarze do posta (Atom)
Ciekawy blog ;)
OdpowiedzUsuńwszystkie wpisy, które czytam mi się podobają :)
Dzięki ;) Staram się, ale ostatnio coraz mniej czasu ;)
OdpowiedzUsuń