Il problema dell'ellisse circolare può essere risolto invertendo la relazione?


13

Avere l' CircleestensioneEllipse rompe il Principio della Sottostazione di Liskov , perché modifica una postcondizione: vale a dire, puoi impostare X e Y in modo indipendente per disegnare un'ellisse, ma X deve sempre essere uguale a Y per i cerchi.

Ma il problema qui non è causato dal fatto che Circle sia il sottotipo di un Ellisse? Non potremmo invertire la relazione?

Quindi, Circle è il supertipo: ha un solo metodo setRadius.

Quindi, Ellipse estende Circle aggiungendo setXe setY. Chiamare setRadiussu Ellipse imposterebbe sia X che Y - il che significa che la postcondizione su setRadius è mantenuta, ma ora puoi impostare X e Y in modo indipendente attraverso un'interfaccia estesa.


1
Hai prima cercato Wikipedia ( en.wikipedia.org/wiki/Circle-ellipse_problem )?
Doc Brown,

1
sì, l'ho anche collegato alla mia domanda ...
HorusKol

6
E questo punto esatto è trattato in quell'articolo, quindi non sono chiaro cosa stai chiedendo?
Philip Kendall,

6
"Alcuni autori hanno suggerito di invertire la relazione tra cerchio ed ellisse, in base al fatto che un'ellisse è un cerchio con capacità aggiuntive. Sfortunatamente, le ellissi non soddisfano molti degli invarianti dei cerchi; se Circle ha un raggio di metodo, Ellipse ora avrà per fornirlo anche ".
Philip Kendall,

3
Quella che ho scoperto essere la spiegazione più chiara del perché questo problema ha delle premesse sbagliate si trova in fondo all'articolo di Wikipedia: en.wikipedia.org/wiki/… . A seconda della situazione, c'è diversi design pulito, ma dipende di cosa avete bisogno di queste due classi di fare , di non essere .
Arthur Havlicek,

Risposte:


37

Ma il problema qui non è causato dal fatto che Circle sia il sottotipo di un Ellisse? Non potremmo invertire la relazione?

Il problema con questo (e il problema quadrato / rettangolo) sta assumendo falsamente una relazione in un dominio (geometria) trattenuta in un altro (comportamento)

Un cerchio e un'ellisse sono correlati se li stai osservando attraverso il prisma della teoria geometrica. Ma questo non è l'unico dominio che puoi guardare.

Il design orientato agli oggetti si occupa del comportamento .

La caratteristica che definisce un oggetto è il comportamento di cui l'oggetto è responsabile. E nel dominio del comportamento, un cerchio e un'ellisse hanno un comportamento così diverso che probabilmente è meglio non pensarli affatto come correlati. In questo dominio, un'ellisse e un cerchio non hanno relazioni significative.

La lezione qui è quella di scegliere il dominio che ha più senso per OOD, non provare a provare una relazione semplicemente perché esiste in un dominio diverso.

L'esempio più comune di questo errore nel mondo reale è quello di presumere che gli oggetti siano correlati (o anche della stessa classe) perché hanno dati simili anche se il loro comportamento è molto diverso. Questo è un problema comune quando si inizia a costruire oggetti "dati prima" definendo dove vanno i dati. È possibile finire con una classe correlata tramite dati con un comportamento completamente diverso. Ad esempio, sia la busta paga che gli oggetti dipendente potrebbero avere un attributo "stipendio lordo", ma un dipendente non è un tipo di busta paga e una busta paga non è un tipo di dipendente.


Separare le preoccupazioni del dominio (dell'applicazione) dalle capacità comportamentali e di responsabilità di OOD è un punto molto importante. Ad esempio, in un'applicazione di disegno, forse dovresti essere in grado di trasformare un cerchio in un quadrato, ma questo non è facilmente modellabile usando classi / oggetti nella maggior parte delle lingue (poiché gli oggetti di solito non possono cambiare classe). Quindi, il dominio dell'applicazione non si associa sempre alla gerarchia ereditaria di un determinato linguaggio OOP e non dovremmo cercare di forzarlo; in molti casi, la composizione è migliore.
Erik Eidt,

3
Questa risposta è di gran lunga la cosa migliore che ho visto sull'intero problema e su come il potenziale errore di progettazione possa sorgere in casi più generali. Grazie
HorusKol il

1
@ErikEidt Il problema di un comportamento che cambia oggetto può essere risolto in OOD attraverso la decomposizione. Ad esempio, se una forma morphable si trasforma in un cerchio, non è necessario cambiare la classe. Invece la classe prende un oggetto comportamento geometrico corrente che puoi scambiare con un altro comportamento quando ti trasformi. Quest'altra classe contiene le regole della forma geometrica attualmente in fase di modellizzazione e la classe forma morphable si differenzia da questa classe per comportamento geometrico. Se l'oggetto si trasforma in una classe diversa, cambia la classe di comportamento in qualcos'altro.
Cormac Mulhall,

2
@Cormac, giusto! Generalmente la definirei una forma di composizione, come ho già detto, anche se potresti identificare, più specificamente, un modello di strategia o qualcosa del genere. In sostanza, hai un'identità che non si trasforma e altre cose che possono essere modificate. Tutto sommato, una buona evidenziazione della differenza tra i concetti del dominio dell'applicazione e i dettagli dell'OOP di un determinato linguaggio e la necessità di mapparli tra loro (ovvero architettura, progettazione e programmazione).
Erik Eidt,

1
Ma un lavoro può essere uno stipendio.

8

I cerchi sono un caso speciale di ellissi, vale a dire che entrambi gli assi dell'ellissi sono uguali. È fondamentalmente falso nel dominio del problema (geometria) affermare che le ellissi potrebbero essere una sorta di cerchio. L'uso di questo modello difettoso violerebbe molte garanzie di un cerchio, ad esempio "tutti i punti del cerchio hanno la stessa distanza dal centro". Anche quella sarebbe una violazione del principio di sostituzione di Liskov. In che modo un'ellisse avrebbe un singolo raggio? (Non setRadius()ma soprattutto getRadius())

Mentre la modellazione dei cerchi come sottotipo di ellissi non è fondamentalmente sbagliata, è l'introduzione della mutabilità che rompe questo modello. Senza i metodi setX()e setY(), non vi è alcuna violazione di LSP. Se è necessario disporre di un oggetto con dimensioni diverse, la creazione di una nuova istanza è una soluzione migliore:

class Ellipse {
  final double x;
  final double y;
  ...
  Ellipse withX(double newX) {
    return new Ellipse(x: newX, y: y);
  }
}

1
ok - quindi, se ci fosse un'interfaccia comune tra Ellipsee Circle(come getArea) che sarebbe stata astratta in un tipo Shape- potrebbe Ellipsee Circlesottotipare separatamente da Shapee soddisfare LSP?
HorusKol

1
@HorusKol Sì. Due classi che ereditano un'interfaccia che entrambe implementano davvero correttamente vanno benissimo.
Ixrec,

7

Cormac ha una risposta davvero grandiosa, ma voglio solo approfondire un po 'il motivo della confusione in primo luogo.

L'ereditarietà nell'OO viene spesso insegnata usando metafore del mondo reale, come "mele e arance sono entrambe sottoclassi di frutta". Sfortunatamente questo porta all'errata convinzione che i tipi in OO dovrebbero essere modellati secondo alcune gerarchie tassonomiche esistenti indipendentemente dal programma.

Ma nella progettazione del software, i tipi dovrebbero essere modellati in base ai requisiti dell'applicazione. Le classificazioni in altri domini sono generalmente irrilevanti. In una vera applicazione con oggetti "Apple" e "Orange" - diciamo un sistema di gestione dell'inventario per un supermercato - probabilmente non saranno affatto classi distinte e categorie come "Fruit" saranno attributi piuttosto che supertipi.

Il problema dell'ellisse circolare è un'aringa rossa. In geometria un cerchio è una specializzazione di un'ellisse, ma le classi nel tuo esempio non sono figure geometriche. Fondamentalmente, le figure geometriche non sono mutabili. Tuttavia, possono essere trasformati , ma poi un cerchio può essere trasformato in ellissi. Quindi un modello in cui i cerchi possono cambiare raggio ma non passare a un'ellissi non corrisponde alla geometria. Un tale modello potrebbe avere senso in una particolare applicazione (ad esempio uno strumento di disegno) ma la classificazione geometrica è irrilevante per il modo in cui si progetta la gerarchia di classi.

Quindi Circle dovrebbe essere una sottoclasse di Ellipse o viceversa? Dipende totalmente dai requisiti della particolare applicazione che utilizza questi oggetti. Un'applicazione di disegno potrebbe avere diverse opzioni su come trattare cerchi ed ellissi:

  1. Tratta cerchi ed ellissi come tipi distinti di forme con UI diversa (es. Due maniglie di ridimensionamento su un'ellissi, una su un cerchio). Ciò significa che puoi avere un'ellisse che è geometricamente un cerchio ma non un cerchio dal punto di vista dell'applicazione.

  2. Tratta tutte le ellissi compresi i cerchi allo stesso modo, ma ha un'opzione per "bloccare" xey sullo stesso valore.

  3. Le ellissi sono solo cerchi in cui è stata applicata una trasformazione in scala.

Ogni possibile progetto porterà a diversi modelli di oggetti -

Nel primo caso, Circle e ellissi sarà pari livello classi

Nel secondo, non ci sarà affatto una classe Circle distinta

Nel terzo, non ci sarà una classe Ellipse distinta. Quindi il cosiddetto problema dell'ellisse circolare non entra nell'immagine in nessuno di questi.

Quindi, per rispondere alla domanda come posta: il cerchio dovrebbe estendere l'ellisse? La risposta è: dipende da cosa vuoi farci. Ma probabilmente no.


1
Un'ottima risposta!
Utsav T

6

Sin dall'inizio è un errore insistere sull'avere una classe "Ellisse" e una "Cerchia" in cui una è una sottoclasse dell'altra. Hai due scelte realistiche: una è avere classi separate. Possono avere una superclasse comune, per cose come il colore, se l'oggetto è pieno, la larghezza della linea per il disegno ecc.

L'altra è avere una sola classe chiamata "Ellipse". Se hai quella classe, è abbastanza facile usarla per rappresentare i cerchi (potrebbero esserci delle trappole a seconda dei dettagli di implementazione; un'ellisse avrà un angolo e il calcolo di quell'angolo non deve avere problemi per un'ellisse a forma di cerchio). Potresti anche avere metodi specializzati per le ellissi circolari, ma queste "ellissi circolari" sarebbero comunque oggetti "Ellisse" completi.


Potrebbe esserci un metodo IsCircle che verificherebbe se un particolare oggetto della classe Ellipse ha entrambi gli assi uguali. Hai anche sottolineato il problema dell'angolo. I cerchi non possono essere "ruotati".

3

Seguendo i punti LSP, una soluzione "adeguata" a questo problema è che sono arrivati ​​@HorusKol e @Ixrec - derivando entrambi i tipi da Shape. Ma dipende dal modello su cui stai lavorando, quindi dovresti sempre tornare a quello.

Quello che mi è stato insegnato è:

Se il sottotipo non può eseguire lo stesso comportamento del super-tipo, la relazione non è valida nella premessa IS-A: dovrebbe essere modificata.

  • Un sottotipo è un SUPERSET del super-tipo.
  • Un super-tipo è un SOTTOTETTO del sottotipo.

In inglese:

  • Un tipo derivato è un SUPERSET del tipo base.
  • Un tipo di base è un SUBSET del tipo derivato.

(Esempio:

  • Un'auto con scarico da cattivo ragazzo è ancora un'auto (secondo alcuni).
  • Un'auto senza motore, ruote, cremagliera dello sterzo, trasmissione e solo il guscio rimasto, non è un'auto, è solo un guscio.)

Ecco come funziona la classificazione (ad es. Nel mondo animale) e, soprattutto, in OO.

Usando questo come definizione di eredità e polimorfismo (che vengono sempre scritti insieme), se questo principio viene infranto, dovresti provare a ripensare i tipi che stai cercando di modellare.

Come menzionato da @HorusKul e @Ixrec, in matematica hai tipi chiaramente definiti. Ma in matematica, un cerchio è un'ellisse perché è un SUBSET di ellisse. Ma in OOP non è così che funziona l'eredità. Una classe dovrebbe ereditare solo se è un SUPERSET (un'estensione) di una classe esistente - il che significa che è comunque la classe di base in tutti i contesti.

Sulla base di ciò, penso che la soluzione dovrebbe essere leggermente riformulata.

Avere un tipo di base Shape, quindi RoundedShape (in effetti un cerchio ma ho usato un nome diverso qui DELIBERATAMENTE ...)

... poi Ellipse.

Quel modo:

  • RoundedShape è una forma.
  • Ellipse è una forma arrotondata.

(Questo ora ha senso per le persone nel linguaggio. Abbiamo già un concetto chiaramente definito di un "cerchio" nelle nostre menti, e ciò che stiamo cercando di fare qui generalizzando (aggregazione) rompe quel concetto.)


I nostri concetti chiaramente definiti non sempre funzionano nella pratica.

-1

Da una prospettiva OO l'ellisse estende il cerchio, si specializza su di esso aggiungendo alcune proprietà. Le proprietà esistenti del cerchio sono ancora contenute nell'ellisse, diventa solo più complessa e più specifica. Non vedo alcun problema con il comportamento in questo caso come fa Cormac, le forme non hanno alcun comportamento. L'unico problema è che in senso liguistico o matematico non è giusto dire "un'ellisse È un cerchio". Perché l'intero punto dell'esercizio che non è menzionato ma è comunque implicito, era classificare le forme geometriche. Questa potrebbe essere una buona ragione per considerare il cerchio e l'ellisse come pari, non collegarli per eredità e accettare che hanno solo alcune delle stesse proprietà e NON lasciare che la tua mente distorta OO si faccia strada con quell'osservazione.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.