piątek, 23 lipca 2010

Problem kwadratu i prostokąta

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

2 komentarze:

  1. Ciekawy blog ;)
    wszystkie wpisy, które czytam mi się podobają :)

    OdpowiedzUsuń
  2. Dzięki ;) Staram się, ale ostatnio coraz mniej czasu ;)

    OdpowiedzUsuń