Kör-ellipszis probléma

A Wikipédiából, a szabad enciklopédiából

A kör-ellipszis probléma, más néven négyzet-téglalap probléma az objektummodellezés altípusos polimorfizmusának több hibalehetőségét is mutatja. Leggyakrabban az objektumorientált programozásban lehet vele találkozni. Definíciója alapján a probléma megsérti a Liskov-féle helyettesítési elvet, ami a SOLID elvek egyike.

A probléma azt veti fel, hogy milyen öröklődési vagy altípus kapcsolat lehet kört és ellipszist, vagy négyzetet és téglalapot reprezentáló objektumok között. A probléma egy általánosabb kérdést vet fel: az ősosztály egyik metódusa úgy változtatja meg az objektumot, hogy azzal megsérti a leszármazott osztály erősebb invariánsát, megsértve a Liskov-féle helyettesítési elvet.

A problémát néha arra használják, hogy kritizálják az objektumorientált programozást. Le lehet vonni azt a következtetést, hogy a hierarchikus taxonómiákat nehéz általános elvvé tenni, a helyzethez illő osztályozási rendszerek praktikusabbak lehetnének.

Leírás[szerkesztés]

Az objektumorientált tervezés és elemzés egyik központi lehetősége az öröklődés, ami altípusos polimorfizmust hoz létre. Ez azt jelenti, hogy a gyermek osztály példányai példányai a szülő osztálynak is. A matematikában a körök speciális ellipszisek, amelyeknek kis- és nagytengelyük ugyanolyan hosszú. Egy objektumorientált nyelven írt programrészlet, ami alakzatokkal foglalkozik, követheti ezt a modellt. Így a Circle (kör) osztály az Ellipse (ellipszis) osztályból fog származni.

Egy alosztálynak a szülő összes metódusát meg kell valósítania, így a megváltoztató metódusokat is. A példában az ellipszisnek van olyan metódusa, ami csak az egyik irányban nyújtja meg az alakzatot. Ha a kör osztály az ellipszisből származik, akkor neki is meg kell ezt valósítania. Ennek azonban az az eredménye, hogy a kör már nem lesz kör, hanem valami más. Megváltoztathatatlan objektumok esetén ez a probléma nem merül fel.

Hasonló probléma adódik az állapotokból is. Egy ellipszisnek több állapota lehet, mint egy körnek. A körnek csak sugár kell, az ellipszisnek két tengely, és a nagytengely által az X tengellyel bezárt szög. Ez elkerülhető, ha a konstansok és a paraméter nélküli függvények felcserélhetők, ahogy az az Eiffelben is van.

Egyes szerzők szerint jobb, ha elszakadunk a matematikai modelltől, és megfordítjuk az öröklést, mondván, hogy az ellipszis több lehetőséggel bíró kör. Azonban az ellipszisek szintén nem felelnek meg a kör összes invariánsának. Ha a körnek van radius (sugár) metódusa vagy adattagja, akkor ezt az ellipszisnek is biztosítania kell.

Megoldási lehetőségek[szerkesztés]

A probléma megoldható:

  • a modell megváltoztatásával
  • a nyelv megváltoztatásával: egy bővítés használatával vagy a nyelv lecserélésével
  • egy másik paradigmára való áttéréssel

A lehetőségek attól függnek, hogy minden kódrészlet megváltoztatható-e. Ha az Ellipse osztály előre adott, és nem lehet vagy szabad megváltoztatni, akkor a lehetőségek beszűkülnek.

A modell megváltoztatása[szerkesztés]

Sikeresség visszaadása[szerkesztés]

A módosító metódusok visszaadhatnak egy sikeres vagy sikertelen értéket. Ellipszis esetén az Ellipse.stretchX (megnyújtás X irányban) sikeres, true értéket ad vissza és elvégzi a feladatot, a Circle.stretchX pedig nem változtatja meg a kört, és false értéket ad vissza.

Ez a megoldás azt igényli, hogy vagy mi írjuk mindkét osztályt, vagy az Ellipse szerzője előre látta a problémát, és követte azt a mintát, hogy a megváltoztató metódusok logikai értéket adjanak vissza. A kliensnek továbbá vizsgálnia kell ezeket a visszatérési értékeket, amivel tulajdonképpen teszteli, hogy ellipszisről vagy körről van-e szó. Ez azt is jelenti, hogy a szerződést lehet, hogy nem teljesíti az objektum, attól függően, hogy mi a pontos típusa.

Egy másik lehetőség, hogy ekkor a kör kivételt dob. Azonban egyes programnyelvekben, mint például a Java ez is az Ellipse szerzőjének közreműködését igényli; hiszen nem feltétlenül célszerű ezt a kivételt egy másik oda nem illő osztályból származtatni.

Az új érték visszaadása[szerkesztés]

Most az Ellipse.stretchX az X tengely megváltoztatott értékét adja vissza. A Circle.stretchX visszatérési értéke a kör sugara. A módosításokhoz a Circle.stretch metódust kell hívni.

Gyengébb szerződés[szerkesztés]

Ha az Ellipse interfésze azt írja, hogy: A stretchX módosítja az X tengelyt, és nem teszi hozzá, hogy semmi más nem változik, akkor a Circle kikényszerítheti, hogy az X és az Y tengely ugyanaz maradjon. Ekkor Circle.stretchX és Circle.stretchY mindkét tengelyt megváltoztatja:

Circle::stretchX(x) {xSize = ySize = x;}
Circle::stretchY(y) {xSize = ySize = y;}

Konverzió[szerkesztés]

A Circle.stretchX meghívásával a Circle objektumból Ellipse objektum lesz. Például Common Lispben ez megtehető a CHANGE-CLASS metódussal.

Ez a változás veszélyes lehet, ha a kliens azt várja el, hogy a metódusok megőrzik az objektum típusát. Vannak nyelvek, amelyek tiltják ezt a váltást, mások korlátozásokat vezetnek be, hogy az Ellipse elfogadható legyen a Circle-t váró metódusok számára. Implicit típuskonverziót támogató nyelvekben, mint a C++ ez csak részleges megoldás, mivel referencián keresztül nem oldja meg a problémát.

Megváltoztathatatlan objektumok[szerkesztés]

A modell alakítható úgy is, hogy megváltoztathatatlan objektumokat használjon. A tisztán funkcionális programozásban minden objektum megváltoztathatatlan.

Ekkor az olyan metódusok, mint a stretchX nem azt a példányt módosítja, amire meghívták, hanem egy új példányt ad vissza. Ekkor nincs probléma a Circle.stretchX megvalósításával, így az öröklődés tükrözheti a matematikai kapcsolatot.

Ennek az a hátránya, hogy a példányok nem megváltoztathatók, csak helyettesíthetők, így a kliens kódban explicit kell értéket adni. Ez kevésbé megszokott, és hibát okozhat, például Orbit(planet[i]) := Orbit(planet[i]).stretchX

Egy másik hátrány, hogy az értékadás egy ideiglenes objektumot hoz létre, ami rontja az optimalizáció lehetőséget és lassítja a programot.

A módosítók kifaktorálása[szerkesztés]

A kifaktorálás azt jelenti, hogy az Ellipse osztályból áthelyezzük a módosító metódusokat a MutableEllipse osztályba; a Circle osztály pedig csak az Ellipse osztályból örököl.

Ennek az a hátránya, hogy extra osztályt vezet be, és a Circle nem örökli az Ellipse módosító metódusait.

Előfeltételek a módosítóknak[szerkesztés]

Az Ellipse.stretchX nem hajtható végre minden példányokon, csak azokon, amelyekre teljesül az Ellipse.stretchable, különben kivétel keletkezik. Az Ellipse készítőjének ehhez már előre látnia kell a problémát, vagy azt is a Circle írójának kell készítenie.

Kifaktorálás közös ősbe[szerkesztés]

Ez azt jelenti, hogy létrehoznak egy absztrakt őst, ami előírja az összes olyan műveletet, amit a Circle és az Ellipse is tartalmaz. A kliens EllipseOrCircle objektumokkal találkozik.

Ezek után a Circle nem lesz Ellipse, így nem felel meg a matematikai modellnek.

Az öröklési kapcsolat megszüntetése[szerkesztés]

Az öröklési kapcsolat megszüntetése megoldja a problémát. A közös tulajdonságok kiemelése interfészbe, mixinbe vagy absztrakt ősbe visszavezet az előző ponthoz. A Circle osztálynak adnak egy Circle.asEllipse metódust, ami a Circle alapján inicializűál egy ellipszis objektumot. Ezzel aztán bármit lehet csinálni, az eredeti kör nem változik. Visszafelé több metódus is lehetséges, mint Ellipse.minimalEnclosingCircle és Ellipse.maximalEnclosedCircle, vagy ami a kliensnek kell.

Az öröklés megfordítása[szerkesztés]

Majorinc modelljében a metódusok három típusra oszthatók: általános, lekérdező és megváltoztató metódusokra. Ezek öröklése különböző. Az általános metódusokat explicit kell örökölni, a lekérdezőket automatikusan. A módosítóknak fordított irányba kell öröklődnie, azaz gyerek osztályról szülő osztályra.

A modell absztrakt osztályok segítségével valósítható meg azokban a nyelvekben, amelyekben van többszörös öröklődés.[1]

A Circle osztály feladása[szerkesztés]

Ha a Circle osztálynak nincsenek speciális metódusai, amelyek nem alkalmazhatók ellipszisre, akkor a Circle osztály kiírható a rendszerből. Ehhez a Circle példányait Ellipse példányokként reprezentálják.

A nyelv megváltoztatása[szerkesztés]

Vannak olyan objektumorientált rendszerek, amelyek nem feltételezik, hogy az objektum típusa egész létezése során állandó, hanem meg lehet változtatni. Ha egy metódus megváltoztatja az objektumot úgy, hogy az már nem felel meg a régi típus invariánsainak, akkor az objektum régi típusa már csak történeti információ, hogy az objektum eredetileg minek készült.

A legtöbb típusrendszer feltételezi, hogy az objektumok típusa állandó. Ez nem az objektumorientáció sajátossága, hanem csak a megvalósításé. Ebben az állapot programtervezési minta segíthet, hogy az objektum úgy viselkedjen, mintha egy másik osztály példánya lenne.

Az alábbi példa a Common Lisp Object System (CLOS) működésén keresztül mutatja, hogy az objektum típusa megváltozhat anélkül, hogy elvesztené azonosságát. Az osztály megváltoztatása után is érvényes marad minden referencia, ami az objektumra hivatkozik.

A példában az ellipszis féltengelyei a h-axis és a v-axis. A körnek ezen kívül vagy egy radius tulajdonsága is, aminek értéke megegyezik a h-axis és a v-axis értékével.

(defclass ellipse ()
  ((h-axis :type real :accessor h-axis :initarg :h-axis)
   (v-axis :type real :accessor v-axis :initarg :v-axis)))

(defclass circle (ellipse)
  ((radius :type real :accessor radius :initarg :radius)))

;;;
;;; A circle has a radius, but also a h-axis and v-axis that
;;; it inherits from an ellipse. These must be kept in sync
;;; with the radius when the object is initialized and
;;; when those values change.
;;;
(defmethod initialize-instance ((c circle) &key radius)
  (setf (radius c) radius)) ;; via the setf method below

(defmethod (setf radius) :after ((new-value real) (c circle))
  (setf (slot-value c 'h-axis) new-value
        (slot-value c 'v-axis) new-value))

;;;
;;; After an assignment is made to the circle's
;;; h-axis or v-axis, a change of type is necessary,
;;; unless the new value is the same as the radius.
;;;
(defmethod (setf h-axis) :after ((new-value real) (c circle))
  (unless (eql (radius c) new-value)
    (change-class c 'ellipse)))

(defmethod (setf v-axis) :after ((new-value real) (c circle))
  (unless (eql (radius c) new-value)
    (change-class c 'ellipse)))

;;;
;;; Ellipse changes to a circle if accessors
;;; mutate it such that the axes are equal,
;;; or if an attempt is made to construct it that way.
;;;
;;; EQL equality is used, under which 0 /= 0.0.
;;;
;;; The SUBTYPEP checks are needed because these methods
;;; apply to circles too, which are ellipses!!!
;;;
(defmethod initialize-instance :after ((e ellipse) &key h-axis v-axis)
  (if (eql h-axis v-axis)
    (change-class e 'circle)))

(defmethod (setf h-axis) :after ((new-value real) (e ellipse))
  (unless (subtypep (class-of e) 'circle)
    (if (eql (h-axis e) (v-axis e))
      (change-class e 'circle))))

(defmethod (setf v-axis) :after ((new-value real) (e ellipse))
  (unless (subtypep (class-of e) 'circle)
    (if (eql (h-axis e) (v-axis e))
      (change-class e 'circle))))

;;;
;;; Method for an ellipse becoming a circle. In this metamorphosis,
;;; the object acquires a radius, which must be initialized.
;;; There is a "sanity check" here to signal an error if an attempt
;;; is made to convert an ellipse which axes are unequal
;;; with an explicit change-class call.
;;; The handling strategy here is to base the radius off the
;;; h-axis and signal an error.
;;; This doesn't prevent the class change; the damage is already done.
;;;
(defmethod update-instance-for-different-class :after ((old-e ellipse)
                                                       (new-c circle) &key)
  (setf (radius new-c) (h-axis old-e))
  (unless (eql (h-axis old-e) (v-axis old-e))
    (error "ellipse ~s can't change into a circle because it's not one!"
           old-e)))

A kód működése interaktívan bemutatható. Az alábbi a Common Lisp CLISP megvalósítását használja.

$ clisp -q -i circle-ellipse.lisp 
[1]> (make-instance 'ellipse :v-axis 3 :h-axis 3)
#<CIRCLE #x218AB566>
[2]> (make-instance 'ellipse :v-axis 3 :h-axis 4)
#<ELLIPSE #x218BF56E>
[3]> (defvar obj (make-instance 'ellipse :v-axis 3 :h-axis 4))
OBJ
[4]> (class-of obj)
#<STANDARD-CLASS ELLIPSE>
[5]> (radius obj)

*** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION RADIUS>
      with arguments (#<ELLIPSE #x2188C5F6>), no method is applicable.
The following restarts are available:
RETRY          :R1      try calling RADIUS again
RETURN         :R2      specify return values
ABORT          :R3      Abort main loop
Break 1 [6]> :a
[7]> (setf (v-axis obj) 4)
4
[8]> (radius obj)
4
[9]> (class-of obj)
#<STANDARD-CLASS CIRCLE>
[10]> (setf (radius obj) 9)
9
[11]> (v-axis obj)
9
[12]> (h-axis obj)
9
[13]> (setf (h-axis obj) 8)
8
[14]> (class-of obj)
#<STANDARD-CLASS ELLIPSE>
[15]> (radius obj)

*** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION RADIUS>
      with arguments (#<ELLIPSE #x2188C5F6>), no method is applicable.
The following restarts are available:
RETRY          :R1      try calling RADIUS again
RETURN         :R2      specify return values
ABORT          :R3      Abort main loop
Break 1 [16]> :a
[17]>

Előfeltevés megváltoztatása[szerkesztés]

Habár első pillantásra úgy látszik, hogy a kör ellipszis, ezt nem biztos, hogy így kell megvalósítani. Lássuk a következő, analóg példát:

class Person
{
  void walkNorth( int meters ) {...}
  void walkEast( int meters ) {...}
}

Egy rab (Prisoner) nyilvánvalóan személy (Person), így elkészülhet a gyermek osztály:

class Prisoner extends Person
{
  void walkNorth( int meters ) {...}
  void walkEast( int meters ) {...}
}

Ez azonban problémát okoz, mivel egy rab semmilyen irányban sem mozoghat szabadon, akármilyen távolságra, habár a Person osztály kiköti, hogy igen.

Így a Person osztályt úgy kellene nevezni, hogy FreePerson (szabad személy), ekkor nyilvánvaló, hogy nem származtatható le belőle a Prisoner.

Hasonlóan, a Circle nem Ellipse, mivel nem rendelkezik annak teljes szabadságával. Más elnevezéssel ez nyilvánvalóbb. A Circle elnevezése OneDiameterFigure (egy átmérőjű alakzat), az Ellipse elnevezése TwoDiameterFigure (két átmérőjű alakzat). Ebből látszik, hogy helyesebb, ha a TwoDiameterFigure örököl a OneDiameterFigure osztályból, mivel további tulajdonságot ad hozzá, egy másik tengelyt.

A probléma azt sugallja, hogy ha egy altípusnak speciális megkötöttségei vannak a fő típushoz képest, akkor ne használjuk az öröklést arra, hogy bevezessük ezt az altípust. Csak akkor használjunk öröklést, ha az altípus új tulajdonságot ad a típushoz.

Jegyzetek[szerkesztés]

  1. Kazimir Majorinc, Ellipse-Circle Dilemma and Inverse Inheritance, ITI 98, Proceedings of the 20th International Conference of Information Technology Interfaces, Pula, 1998

Források[szerkesztés]

Fordítás[szerkesztés]

Ez a szócikk részben vagy egészben a Circle-ellipse problem című angol Wikipédia-szócikk fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.