Cirkel -ellipse problem - Circle–ellipse problem

Den cirkel-ellipse problem i softwareudvikling (også kaldet kvadrat-rektangel problem ) illustrerer flere faldgruber, der kan opstå, når du bruger undertype polymorfi i objekt modellering . Problemerne opstår oftest ved brug af objektorienteret programmering (OOP). Per definition er dette problem en overtrædelse af Liskov -substitutionsprincippet , et af SOLID -principperne.

Problemet vedrører hvilken undertypning eller arveforhold , der skal eksistere mellem klasser, der repræsenterer cirkler og ellipser (eller på samme måde firkanter og rektangler ). Mere generelt illustrerer problemet de vanskeligheder, der kan opstå, når en basisklasse indeholder metoder, der muterer et objekt på en måde, der kan ugyldiggøre en (stærkere) invariant fundet i en afledt klasse, hvilket får Liskov -substitutionsprincippet til at blive overtrådt.

Eksistensen af ​​cirkel-ellipseproblemet bruges undertiden til at kritisere objektorienteret programmering. Det kan også betyde, at hierarkiske taksonomier er svære at gøre universelle, hvilket indebærer, at situationelle klassificeringssystemer kan være mere praktiske.

Beskrivelse

Det er et centralt grundlag for objektorienteret analyse og design, at undertype polymorfisme , som implementeres i de fleste objektorienterede sprog via arv , skal bruges til at modellere objekttyper, der er undergrupper af hinanden; dette kaldes almindeligvis is-a- forholdet. I det foreliggende eksempel er sættet med cirkler en delmængde af ellipsesættet; cirkler kan defineres som ellipser, hvis større og mindre akser har samme længde. Således vil kode skrevet i et objektorienteret sprog, der modellerer former, ofte vælge at gøre klasse Circle til en underklasse af klasse Ellipse , dvs. arve fra den.

En underklasse skal understøtte al adfærd, der understøttes af superklassen; underklasser skal implementere alle mutatormetoder, der er defineret i en basisklasse. I det foreliggende tilfælde ændrer metoden Ellipse.stretchX længden af ​​en af ​​dens akser på plads. Hvis Circle arver fra Ellipse , skal den også have en metode stretchX , men resultatet af denne metode ville være at ændre en cirkel til noget, der ikke længere er en cirkel. Den Cirkel klasse kan ikke samtidig tilfredsstille sin egen invariant og de adfærdsmæssige krav i Ellipse.stretchX metoden.

Et relateret problem med denne arv opstår, når man overvejer implementeringen. En ellipse kræver, at der beskrives flere tilstande end en cirkel, fordi førstnævnte har brug for attributter for at specificere længden og rotationen af ​​de store og mindre akser, hvorimod en cirkel kun har brug for en radius. Det kan være muligt at undgå dette, hvis sproget (f.eks. Eiffel ) laver konstante værdier for en klasse, fungerer uden argumenter og datamedlemmer kan udskiftes.

Nogle forfattere har foreslået at vende forholdet mellem cirkel og ellipse med den begrundelse, at en ellipse er en cirkel med flere evner. Desværre formår ellipser ikke at tilfredsstille mange af kredsens invarianter; hvis Circle har en metode radius , Ellipse skal nu give det, også.

Mulige løsninger

Man kan løse problemet ved at:

  • ændre modellen
  • ved hjælp af et andet sprog (eller en eksisterende eller specialskrevet udvidelse af et eksisterende sprog)
  • ved hjælp af et andet paradigme

Præcis hvilken mulighed der er passende, vil afhænge af, hvem der skrev Circle, og hvem der skrev Ellipse . Hvis den samme forfatter designer dem begge fra bunden, vil forfatteren kunne definere grænsefladen til at håndtere denne situation. Hvis Ellipse -objektet allerede var skrevet og ikke kan ændres, er mulighederne mere begrænsede.

Skift model

Returnér succes eller fiasko værdi

Tillad, at objekterne returnerer en "succes" eller "fiasko" -værdi for hver modifikator eller hæver en undtagelse for fejl. Dette gøres normalt i tilfælde af fil I/O, men kan også være nyttigt her. Nu fungerer Ellipse.stretchX og returnerer "true", mens Circle.stretchX ganske enkelt returnerer "false". Dette er generelt god praksis, men kan kræve, at den oprindelige forfatter af Ellipse forudså et sådant problem og definerede mutatorerne som at returnere en værdi. Det kræver også klientkoden at teste returværdien til understøttelse af stretchfunktionen, hvilket i virkeligheden er som at teste, om det refererede objekt enten er en cirkel eller en ellipse. En anden måde at se på dette er, at det er som at indsætte kontrakten, at kontrakten måske eller ikke opfyldes afhængigt af objektet, der implementerer grænsefladen. Til sidst er det kun en smart måde at omgå Liskov-begrænsningen ved på forhånd at angive, at stillingsbetingelsen muligvis ikke er gyldig.

Alternativt kunne Circle.stretchX kaste en undtagelse (men afhængigt af sproget kan dette også kræve, at den originale forfatter af Ellipse erklærer, at den kan kaste en undtagelse).

Returner den nye værdi af X

Dette er en lignende løsning til ovenstående, men er lidt mere kraftfuld. Ellipse.stretchX returnerer nu den nye værdi af sin X -dimension. Nu kan Circle.stretchX ganske enkelt returnere sin nuværende radius. Alle ændringer skal foretages gennem Circle.stretch , som bevarer cirklens invariant.

Tillad en svagere kontrakt på Ellipse

Hvis grænsefladekontrakten for Ellipse kun angiver, at "stretchX ændrer X -aksen", og ikke angiver "og intet andet vil ændre sig", kunne Circle simpelthen tvinge X- og Y -dimensionerne til at være de samme. Circle.stretchX og Circle.stretchY ændrer både X- og Y -størrelsen.

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

Konverter cirklen til en ellipse

Hvis Circle.stretchX kaldes, ændrer Circle sig til en Ellipse . For eksempel i Common Lisp kan dette gøres via metoden CHANGE-CLASS . Dette kan imidlertid være farligt, hvis en anden funktion forventer, at det er en cirkel . Nogle sprog udelukker denne type ændringer, og andre pålægger Ellipse -klassen begrænsninger for at være en acceptabel erstatning for Circle . For sprog, der tillader implicit konvertering som C ++ , er dette muligvis kun en delvis løsning, der løser problemet ved opkald-til-kopi, men ikke ved opkald-ved-reference.

Gør alle instanser konstante

Man kan ændre modellen, så forekomster af klasserne repræsenterer konstante værdier (dvs. de er uforanderlige ). Dette er den implementering, der bruges i rent funktionel programmering.

I dette tilfælde skal metoder som stretchX ændres for at give en ny instans frem for at ændre den instans, de handler på. Det betyder, at det ikke længere er et problem at definere Circle.stretchX , og arven afspejler det matematiske forhold mellem cirkler og ellipser.

En ulempe er, at ændring af værdien af ​​en forekomst derefter kræver en tildeling , som er ubelejlig og tilbøjelig til programmeringsfejl, f.eks.

Bane (planet [i]): = bane (planet [i]). StretchX

En anden ulempe er, at en sådan opgave konceptuelt indebærer en midlertidig værdi, som kan reducere ydelsen og være vanskelig at optimere.

Faktor ud modifikatorer

Man kan definere en ny klasse MutableEllipse , og sætte modifikatorerne fra Ellipse i den. Den Circle kun arver forespørgsler fra Ellipse .

Dette har en ulempe ved at indføre en ekstra klasse, hvor det eneste, der ønskes, er at angive, at Circle ikke arver modifikatorer fra Ellipse .

Sæt forudsætninger for modifikatorer

Man kan angive, at Ellipse.stretchX kun er tilladt i tilfælde, der tilfredsstiller Ellipse.stretchable , og vil ellers kaste en undtagelse . Dette kræver foregribelse af problemet, når Ellipse er defineret.

Faktorér fælles funktionalitet ind i en abstrakt basisklasse

Opret en abstrakt basisklasse kaldet EllipseOrCircle og sæt metoder, der fungerer med både Circle s og Ellipse s i denne klasse. Funktioner, der kan håndtere begge typer objekter, vil forvente en EllipseOrCircle , og funktioner, der bruger Ellipse - eller Circle -specifikke krav, vil bruge de efterkommende klasser. Imidlertid er Circle ikke længere en Ellipse -underklasse, hvilket fører til situationen "a Circle is not a slags Ellipse " beskrevet ovenfor.

Slip alle arverelationer

Dette løser problemet med et slagtilfælde. Enhver fælles operation, der ønskes for både en cirkel og en ellipse, kan abstraheres ud til en fælles grænseflade, som hver klasse implementerer, eller til mixins .

Man kan også tilvejebringe konverteringsmetoder som Circle.asEllipse , som returnerer et omskifteligt Ellipse -objekt initialiseret ved hjælp af cirkelens radius. Fra det tidspunkt er det et separat objekt og kan muteres separat fra den originale cirkel uden problem. Metoder, der konverterer den anden vej, behøver ikke forpligte sig til én strategi. For eksempel kan der være både Ellipse.minimalEnclosingCircle og Ellipse.maximalEnclosedCircle og enhver anden ønsket strategi.

Kombiner klasse Cirkel til klasse Ellipse

Brug derefter en ellipse, uanset hvor en cirkel blev brugt før.

En cirkel kan allerede repræsenteres af en ellipse. Der er ingen grund til at have klasse Circle, medmindre den har brug for nogle cirkelspecifikke metoder, der ikke kan anvendes på en ellipse, eller medmindre programmereren ønsker at drage fordel af konceptuelle og/eller præstationsfordele ved cirkelens enklere model.

Omvendt arv

Majorinc foreslog en model, der deler metoder på modifikatorer, selektorer og generelle metoder. Kun selektorer kan automatisk arves fra superklasse, mens modifikatorer bør arves fra underklasse til superklasse. I almindelighed skal metoderne eksplicit arves. Modellen kan efterlignes på sprog med flere arv ved hjælp af abstrakte klasser .

Skift programmeringssprog

Dette problem har enkle løsninger i et tilstrækkeligt kraftfuldt OO -programmeringssystem. I det væsentlige er cirkel -ellipseproblemet et af synkronisering af to repræsentationer af typen: de facto -typen baseret på objektets egenskaber og den formelle type, der er forbundet med objektet af objektsystemet. Hvis disse to oplysninger, som i sidste ende kun er bits i maskinen, holdes synkroniseret, så de siger det samme, er alt i orden. Det er klart, at en cirkel ikke kan tilfredsstille de invarianter, der kræves af den, mens dens ellipsemetoder tillader mutation af parametre. Imidlertid eksisterer muligheden for, at når en cirkel ikke kan møde cirkelvarianterne, kan dens type opdateres, så den bliver en ellipse. Hvis en cirkel, der er blevet en de facto ellipse, ikke ændrer type, så er dens type et stykke information, der nu er forældet, hvilket afspejler objektets historie (hvordan det engang blev konstrueret) og ikke dets nuværende virkelighed ( hvad den siden er muteret til).

Mange objektsystemer i populær brug er baseret på et design, der tager det for givet, at et objekt bærer den samme type i hele sin levetid, fra konstruktion til færdiggørelse. Dette er ikke en begrænsning af OOP, men kun bestemte implementeringer.

Følgende eksempel bruger Common Lisp Object System (CLOS), hvor objekter kan ændre klasse uden at miste deres identitet. Alle variabler eller andre lagringssteder, der indeholder en reference til et objekt, gemmer fortsat en reference til det samme objekt, efter at det ændrer klasse.

Cirkel- og ellipsemodellerne er bevidst forenklet for at undgå distraherende detaljer, som ikke er relevante for cirklen -ellipseproblemet. En ellipse har to halvakser kaldet h-akse og v-akse i koden. Som en ellipse arver en cirkel disse og har også en radiusegenskab , hvilken værdi er lig med aksernes værdi (som naturligvis skal være ens).

(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 (= (radius c) new-value)
    (change-class c 'ellipse)))

(defmethod (setf v-axis) :after ((new-value real) (c circle))
  (unless (= (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.
;;;
;;;
(defmethod initialize-instance :after ((e ellipse) &key h-axis v-axis)
  (if (= h-axis v-axis)
    (change-class e 'circle)))

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

(defmethod (setf v-axis) :after ((new-value real) (e ellipse))
  (unless (typep e 'circle)
    (if (= (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 (= (h-axis old-e) (v-axis old-e))
    (error "ellipse ~s can't change into a circle because it's not one!"
           old-e)))

Denne kode kan demonstreres med en interaktiv session ved hjælp af CLISP -implementeringen af ​​Common Lisp.

$ 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]>

Udfordre problemets præmis

Selv om det ved første øjekast kan virke indlysende, at en cirkel er en ellipse, skal du overveje følgende analoge kode.

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

Nu er en fange naturligvis en person. Så logisk set kan der oprettes en underklasse:

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

Også selvfølgelig, dette fører til problemer, da en fange er ikke fri til at flytte en vilkårlig afstand i alle retninger, men kontrakten af Person klassen hedder, at en person kan.

Således klassen Person kunne bedre hedde FreePerson . Hvis det var tilfældet, er ideen om, at klasse Fange forlænger FreePerson , klart forkert.

I analogi er en Cirkel altså ikke en Ellipse, fordi den mangler de samme frihedsgrader som en Ellipse.

Ved at anvende bedre navngivning kunne en cirkel i stedet hedde OneDiameterFigure og en ellipse kunne hedde TwoDiameterFigure . Med sådanne navne er det nu mere indlysende, at TwoDiameterFigure skal udvide OneDiameterFigure , da det tilføjer en anden egenskab til det; hvorimod OneDiameterFigure har en egenskab med en enkelt diameter, har TwoDiameterFigure to sådanne egenskaber (dvs. en større og en mindre akselængde ).

Dette tyder kraftigt på, at arv aldrig bør bruges, når underklassen begrænser den frihed, der er implicit i basisklassen, men kun bør bruges, når underklassen tilføjer ekstra detaljer til konceptet repræsenteret af basisklassen som i 'Monkey' er -Et dyr'.

Det er imidlertid endnu en gang en forkert forudsætning, at en fange ikke kan bevæge sig en vilkårlig afstand i nogen retning, og en person kan. Ethvert objekt, der bevæger sig i en hvilken som helst retning, kan støde på forhindringer. Den rigtige måde at modellere dette problem på er at have en WalkAttemptResult walkToDirection (int meter, Direction direction) kontrakt. Nu, når du implementerer walkToDirection for underklassen Prisoner, kan du kontrollere grænserne og returnere korrekte gangresultater.

Referencer

eksterne links

  • https://web.archive.org/web/20150409211739/http://www.parashift.com/c++-faq-lite/proper-inheritance.html#faq-21.6 Et populært C ++- ofte stillet websted af Marshall Cline . Angiver og forklarer problemet.
  • Konstruktiv dekonstruktion af undertyping af Alistair Cockburn på sit eget websted. Teknisk/matematisk diskussion af typning og underskrivning, med applikationer til dette problem.
  • Henney, Kevlin (2003-04-15). "Fra mekanisme til metode: Total Ellipse" . Dr. Dobb .
  • http://orafaq.com/usenet/comp.databases.theory/2001/10/01/0001.htm Begyndelsen på en lang tråd (følg måske svaret: links) på Oracle FAQ om problemstillinger. Henviser til skrifter fra CJ Date. Nogle fordomme over for Smalltalk .
  • LiskovSubstitutionPrincipWikiWikiWeb
  • Undertypning, underklassificering og problemer med OOP , et essay, der diskuterer et relateret problem: skal sæt arve fra poser?
  • Subtyping by Constraints in Object-Oriented Databases , et essay, der diskuterer en udvidet version af cirkel-ellipseproblemet i miljøet i objektorienterede databaser.