Perché Square ereditando da Rectangle può essere problematico se si sostituiscono i metodi SetWidth e SetHeight?

103

Se un quadrato è un tipo di rettangolo, allora perché un quadrato non può ereditare da un rettangolo? O perché è un cattivo design?

Ho sentito persone dire:

If you made Square derive from Rectangle, then a Square should be usable anywhere you expect a rectangle

Qual è il problema qui? E perché Square sarebbe utilizzabile ovunque ti aspetti un rettangolo? Sarebbe utilizzabile solo se creiamo l'oggetto Square e se sostituiamo i metodi SetWidth e SetHeight per Square del perché ci sarebbe qualche problema?

If you had SetWidth and SetHeight methods on your Rectangle base class and if your Rectangle reference pointed to a Square, then SetWidth and SetHeight don't make sense because setting one would change the other to match it. In this case Square fails the Liskov Substitution Test with Rectangle and the abstraction of having Square inherit from Rectangle is a bad one.

Qualcuno può spiegare gli argomenti di cui sopra? Ancora una volta, se superiamo i metodi SetWidth e SetHeight in Square, non risolverebbe questo problema?

Ho anche sentito / letto:

The real issue is that we are not modeling rectangles, but rather "reshapable rectangles" i.e., rectangles whose width or height can be modified after creation (and we still consider it to be the same object). If we look at the rectangle class in this way, it is clear that a square is not a "reshapable rectangle", because a square cannot be reshaped and still be a square (in general). Mathematically, we don't see the problem because mutability doesn't even make sense in a mathematical context

Qui credo che "ri-dimensionabile" sia il termine corretto. I rettangoli sono "ri-dimensionabili" e lo sono anche i quadrati. Mi manca qualcosa nell'argomento sopra? Un quadrato può essere ridimensionato come qualsiasi rettangolo.

    
posta user793468 07.05.2014 - 07:21
fonte

12 risposte

186

Fondamentalmente vogliamo che le cose si comportino in modo ragionevole.

Considera il seguente problema:

Mi viene assegnato un gruppo di rettangoli e voglio aumentare la loro area del 10%. Quindi, quello che faccio è impostare la lunghezza del rettangolo su 1,1 volte rispetto a prima.

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    rectangle.Length = rectangle.Length * 1.1;
  }
}

Ora, in questo caso, tutti i miei rettangoli ora hanno una lunghezza aumentata del 10%, che aumenterà la loro area del 10%. Sfortunatamente, qualcuno mi ha effettivamente passato una miscela di quadrati e rettangoli, e quando è stata cambiata la lunghezza del rettangolo, lo era anche la larghezza.

I miei test di unità passano perché ho scritto tutti i miei test di unità per utilizzare una raccolta di rettangoli. Ora ho introdotto un piccolo bug nella mia applicazione che può passare inosservato per mesi.

Peggio ancora, Jim dalla contabilità vede il mio metodo e scrive un altro codice che usa il fatto che se passa i quadrati al mio metodo, ottiene un bel 21% di aumento delle dimensioni. Jim è felice e nessuno è più saggio.

Jim viene promosso per un lavoro eccellente in una divisione diversa. Alfred si unisce alla compagnia da junior. Nella sua prima segnalazione di bug, Jill from Advertising ha riportato che il passaggio dei quadrati a questo metodo si traduce in un aumento del 21% e vuole che il bug sia corretto. Alfred vede che Squares e Rectangles sono usati ovunque nel codice e si rendono conto che rompere la catena ereditaria è impossibile. Inoltre, non ha accesso al codice sorgente di Accounting. Così Alfred risolve il bug in questo modo:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

Alfred è contento delle sue abilità di hacker super e Jill firma che il bug è stato corretto.

Il prossimo mese nessuno viene pagato perché la contabilità dipende dalla possibilità di passare i quadrati al metodo IncreaseRectangleSizeByTenPercent e ottenere un aumento dell'area del 21%. L'intera azienda entra in modalità "priorità 1 bugfix" per rintracciare la fonte del problema. Tracciano il problema alla correzione di Alfred. Sanno che devono tenere felici sia la contabilità che la pubblicità. Quindi risolvono il problema identificando l'utente con la chiamata al metodo in questo modo:

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles)
{
  IncreaseRectangleSizeByTenPercent(
    rectangles, 
    new User() { Department = Department.Accounting });
}

public void IncreaseRectangleSizeByTenPercent(IEnumerable<Rectangle> rectangles, User user)
{
  foreach(var rectangle in rectangles)
  {
    if (typeof(rectangle) == Rectangle || user.Department == Department.Accounting)
    {
      rectangle.Length = rectangle.Length * 1.1;
    }
    else if (typeof(rectangle) == Square)
    {
      rectangle.Length = rectangle.Length * 1.04880884817;
    }
  }
}

E così via e così via.

Questo aneddoto si basa su situazioni del mondo reale che affrontano quotidianamente i programmatori. Le violazioni del principio di sostituzione di Liskov possono introdurre bug molto sottili che vengono rilevati solo anni dopo che sono stati scritti, in quel momento la correzione della violazione interromperà un sacco di cose e non risolvendo farà arrabbiare il più grande cliente.

Esistono due modi realistici per risolvere questo problema.

Il primo modo è rendere il rettangolo immutabile. Se l'utente di Rectangle non può modificare le proprietà Length e Width, questo problema scompare. Se vuoi un rettangolo con una lunghezza e una larghezza diverse, ne crei uno nuovo. I quadrati possono ereditare felicemente dai rettangoli.

Il secondo modo è di rompere la catena ereditaria tra quadrati e rettangoli. Se un quadrato è definito come avente una sola proprietà SideLength e i rettangoli hanno una proprietà Length e Width e non c'è ereditarietà, è impossibile rompere accidentalmente le cose aspettandosi un rettangolo e ottenendo un quadrato. In termini C #, potresti seal della tua classe di rettangolo, che garantisce che tutti i rettangoli che ottieni siano in realtà rettangoli.

In questo caso, mi piace il modo in cui gli "oggetti immutabili" risolvono il problema. L'identità di un rettangolo è la sua lunghezza e larghezza. Ha senso che quando vuoi cambiare l'identità di un oggetto, ciò che vuoi veramente è un nuovo oggetto. Se perdi un vecchio cliente e guadagni un nuovo cliente, non cambi il campo Customer.Id dal vecchio cliente a quello nuovo, crei un nuovo Customer .

Le violazioni del principio di sostituzione di Liskov sono comuni nel mondo reale, soprattutto perché un sacco di codice è scritto da persone che sono incompetenti / sotto pressione / non si preoccupano / sbagliano. Può e causa problemi molto sgradevoli. Nella maggior parte dei casi, desideri preferire la composizione sull'ereditarietà .

    
risposta data 07.05.2014 - 09:19
fonte
29

Se tutti i tuoi oggetti sono immutabili, non ci sono problemi. Ogni quadrato è anche un rettangolo. Tutte le proprietà di un rettangolo sono anche proprietà di un quadrato.

Il problema inizia quando aggiungi la possibilità di modificare gli oggetti. O davvero - quando inizi a passare argomenti all'oggetto, non solo a leggere i getter di proprietà.

Ci sono modifiche che puoi apportare a un rettangolo che mantiene tutti gli invarianti della tua classe Rectangle, ma non tutti gli invarianti quadrati, come cambiare la larghezza o l'altezza. Suddamente il comportamento di un rettangolo non è solo le sue proprietà, ma anche le sue possibili modifiche. Non è solo ciò che ottieni dal rettangolo, è anche ciò che puoi mettere .

Se il tuo rettangolo ha un metodo setWidth che è documentato come modifica della larghezza e non modifica l'altezza, Square non può avere un metodo compatibile. Se cambi la larghezza e non l'altezza, il risultato non è più un quadrato valido. Se si è scelto di modificare sia la larghezza che l'altezza del quadrato quando si utilizza setWidth , non si sta implementando la specifica di setWidth di Rectangle. Non puoi vincere.

Quando guardi cosa puoi "inserire" in un rettangolo e in un quadrato, quali messaggi puoi inviare loro, probabilmente troverai che qualsiasi messaggio che puoi inviare validamente a un quadrato, puoi anche inviarlo a un rettangolo.

È una questione di co-varianza vs. contro-varianza.

Metodi di una sottoclasse appropriata, una in cui le istanze possono essere utilizzate in tutti i casi in cui è prevista la superclasse, richiede che ciascun metodo sia:

  • Restituisce solo i valori restituiti dalla superclasse, ovvero il tipo restituito deve essere un sottotipo del tipo di ritorno del metodo della superclasse. Il ritorno è in co-variante.
  • Accetta tutti i valori accettati dal supertipo, ovvero i tipi di argomento devono essere supertipi dei tipi di argomenti del metodo della superclasse. Gli argomenti sono contro-varianti.

Quindi, tornando a Rettangolo e Quadrato: Se Square può essere una sottoclasse di Rectangle dipende interamente da quali metodi hanno Rectangle.

Se Rettangolo ha setter individuali per larghezza e altezza, Square non creerà una buona sottoclasse.

Allo stesso modo, se fai in modo che alcuni metodi siano co-variant negli argomenti, come avere compareTo(Rectangle) su Rectangle e compareTo(Square) su Square, avrai un problema usando un quadrato come un rettangolo.

Se progetti Square e Rectangle per essere compatibili, probabilmente funzionerà, ma dovrebbero essere sviluppati insieme, o scommetto che non funzionerà.

    
risposta data 07.05.2014 - 13:19
fonte
16

Ci sono molte buone risposte qui; La risposta di Stephen in particolare fa un buon lavoro per illustrare perché le violazioni del principio di sostituzione portano a conflitti nel mondo reale tra squadre.

Ho pensato di poter parlare brevemente del problema specifico dei rettangoli e dei quadrati, piuttosto che usarlo come metafora per altre violazioni del LSP.

C'è un ulteriore problema con square-is-a-special-type-of-rectangle che raramente viene menzionato, e cioè: perché ci fermiamo con quadrati e rettangoli ? Se siamo disposti a dire che un quadrato è un tipo speciale di rettangolo, sicuramente dovremmo anche essere disposti a dire:

  • Un quadrato è un tipo speciale di rombo - è un rombo con angoli quadrati.
  • Un rombo è un tipo speciale di parallelogramma - è un parallelogramma con lati uguali.
  • Un rettangolo è un tipo speciale di parallelogramma - è un parallelogramma con angoli quadrati
  • Un rettangolo, un quadrato e un parallelogramma sono tutti un tipo speciale di trapezio - sono trapezi con due serie di lati paralleli
  • Tutti i suddetti sono tipi speciali di quadrilateri
  • Tutti i precedenti sono tipi speciali di forme planari
  • E così via; Potrei andare avanti per un po 'di tempo qui.

Che diavolo dovrebbero essere tutte le relazioni qui? I linguaggi basati sull'ereditarietà delle classi come C # o Java non sono stati progettati per rappresentare questi tipi di relazioni complesse con diversi tipi di vincoli. È meglio evitare semplicemente la domanda completamente non cercando di rappresentare tutte queste cose come classi con relazioni di sottotitoli.

    
risposta data 09.05.2014 - 18:44
fonte
13

Da una prospettiva matematica, un quadrato è un rettangolo. Se un matematico modifica il quadrato in modo che non aderisca più al contratto quadrato, si trasforma in un rettangolo.

Ma nella progettazione OO, questo è un problema. Un oggetto è quello che è, e questo include i comportamenti e lo stato. Se tengo un oggetto quadrato, ma qualcun altro lo modifica per essere un rettangolo, questo viola il contratto del quadrato senza colpa mia. Questo fa sì che accadano tutti i tipi di cose brutte.

Il fattore chiave qui è mutabilità . Una forma può cambiare una volta costruita?

  • Mutevole: se le forme possono cambiare una volta costruito, un quadrato non può avere una relazione is-a con il rettangolo. Il contratto di un rettangolo include il vincolo che i lati opposti devono essere della stessa lunghezza, ma i lati adiacenti non devono essere. Il quadrato deve avere quattro lati uguali. La modifica di un quadrato attraverso un'interfaccia rettangolo può violare il contratto quadro.

  • Immutabile: se le forme non possono cambiare una volta costruite, anche un oggetto quadrato deve sempre soddisfare il contratto del rettangolo. Un quadrato può avere una relazione-is con un rettangolo.

In entrambi i casi è possibile chiedere a un quadrato di produrre una nuova forma in base al suo stato con una o più modifiche. Ad esempio, si potrebbe dire "creare un nuovo rettangolo basato su questo quadrato, eccetto che i lati opposti A e C sono il doppio". Poiché viene costruito un nuovo oggetto, il quadrato originale continua ad aderire ai suoi contratti.

    
risposta data 07.05.2014 - 08:00
fonte
13

And why would Square be usable anywhere you expect a rectangle?

Perché questo è parte di ciò che significa essere un sottotipo (vedi anche: principio di sostituzione di Liskov). Puoi farlo, devi essere in grado di farlo:

Square s = new Square(5);
Rect r = s;
doSomethingWith(r); // written assuming a Rect, actually calls Square methods

In realtà lo fai sempre (a volte anche più implicitamente) quando usi OOP.

and if we over-ride the SetWidth and SetHeight methods for Square than why would there be any issue?

Perché non puoi ignorare sensibilmente quelli per Square . Perché un non può "quadrato essere ridimensionato come qualsiasi rettangolo". Quando l'altezza di un rettangolo cambia, la larghezza rimane la stessa. Ma quando l'altezza di un quadrato cambia, la larghezza deve cambiare di conseguenza. Il problema non è solo ridimensionabile, è ridimensionabile in entrambe le dimensioni in modo indipendente.

    
risposta data 07.05.2014 - 07:31
fonte
9

Ciò che stai descrivendo è in contrasto con il cosiddetto Principio di sostituzione di Liskov . L'idea di base dell'LSP è che ogni volta che usi un'istanza di una particolare classe, dovresti sempre essere in grado di scambiare un'istanza di qualsiasi sottoclasse di quella classe, senza introducendo bug.

Il problema di Rectangle-Square non è davvero un ottimo modo per introdurre Liskov. Cerca di spiegare un principio generale usando un esempio che è in realtà abbastanza sottile, e si imbatte in uno di questi più comuni definizioni intuitive in tutta la matematica. Alcuni lo chiamano il problema Ellipse-Circle per questo motivo, ma è solo leggermente migliore per quanto riguarda questo. Un approccio migliore consiste nel fare un leggero passo indietro, usando quello che io chiamo il problema Parallelogramma-Rettangolo. Questo rende le cose molto più facili da capire.

Un parallelogramma è un quadrilatero con due coppie di lati paralleli. Ha anche due coppie di angoli congruenti. Non è difficile immaginare un oggetto Parallelogram lungo queste linee:

class Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Un modo comune di pensare a un rettangolo è come un parallelogramma con angoli retti. A prima vista, potrebbe sembrare che Rectangle sia un buon candidato per l'ereditarietà di Parallelogram , in modo da poter riutilizzare tutto quel codice gustoso. Tuttavia:

class Rectangle extends Parallelogram {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* BUG: Liskov violations ahead */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

Perché queste due funzioni introducono bug in Rectangle? Il problema è che non puoi cambiare gli angoli in un rettangolo : sono definiti come sempre a 90 gradi, e quindi questa interfaccia non funziona per Rettangolo che eredita da Parallelogramma. Se cambio un Rettangolo in codice che si aspetta un Parallelogramma, e quel codice prova a cambiare l'angolo, ci saranno quasi sicuramente dei bug. Abbiamo preso qualcosa che era scrivibile nella sottoclasse e l'ho reso di sola lettura, e questa è una violazione di Liskov.

Ora, come si applica questo a Squares and Rectangles?

Quando diciamo che puoi impostare un valore, generalmente intendiamo qualcosa di un po 'più strong del semplice essere in grado di scrivere un valore in esso. Noi implichiamo un certo grado di esclusività: se imposti un valore, escludendo alcune circostanze straordinarie, rimarrà tale valore finché non lo imposti di nuovo. Ci sono molti usi per i valori che possono essere scritto ma non rimanere fisso, ma ci sono anche molti casi che dipendono da un valore che rimane dove è una volta impostato. Ed è qui che incontriamo un altro problema.

class Square extends Rectangle {
    function getSideA() {};
    function getSideB() {};
    function getAngleA() {};
    function getAngleB() {};

    /* BUG: More Liskov violations */
    function setSideA(newLength) {};
    function setSideB(newLength) {};

    /* Liskov violations inherited from Rectangle */
    function setAngleA(newAngle) {};
    function setAngleB(newAngle) {};
}

La nostra classe Square ha ereditato bug da Rectangle, ma ne ha alcuni nuovi. Il problema con setSideA e setSideB è che nessuno di questi è più veramente impostabile: puoi ancora scrivere un valore in uno dei due, ma cambierà da sotto te se l'altro è scritto. Se lo scambio con un parallelogramma in codice che dipende dall'essere in grado di impostare i lati indipendentemente l'uno dall'altro, andrà fuori di testa.

Questo è il problema, ed è il motivo per cui c'è un problema nell'usare Rectangle-Square come introduzione a Liskov. Rectangle-Square dipende dalla differenza tra essere in grado di scrivere in qualcosa ed essere in grado di impostarlo , e questa è una differenza molto più sottile rispetto all'essere in grado di impostare qualcosa contro l'avere è di sola lettura. Rectangle-Square ha ancora valore come esempio, perché documenta un getcha abbastanza comune che deve essere guardato per, ma non dovrebbe essere usato come un esempio introduttivo . Permetti allo studente di mettere a posto le basi per le basi e quindi lanciare loro qualcosa di più difficile.

    
risposta data 08.05.2014 - 19:04
fonte
8

La sottotipizzazione riguarda il comportamento.

Per tipo B per essere un sottotipo di tipo A , deve supportare ogni operazione che digita A supporta con la stessa semantica (parolacce per "comportamento"). Usando la logica che ogni B è un A fa non funziona - la compatibilità comportamentale ha l'ultima parola. La maggior parte delle volte "B è un tipo di A" si sovrappone a "B si comporta come A", ma non sempre .

Un esempio:

Considera l'insieme di numeri reali. In qualsiasi lingua, possiamo aspettarci che supportino le operazioni + , - , * e / . Consideriamo ora l'insieme di interi positivi ({1, 2, 3, ...}). Chiaramente, ogni intero positivo è anche un numero reale. Ma il tipo di numeri interi positivi è un sottotipo del tipo di numeri reali? Diamo un'occhiata alle quattro operazioni e vediamo se gli interi positivi si comportano allo stesso modo dei numeri reali:

  • + : possiamo aggiungere interi positivi senza problemi.
  • - : non tutte le sottrazioni di interi positivi producono numeri interi positivi. Per esempio. 3 - 5 .
  • * : possiamo moltiplicare interi positivi senza problemi.
  • / : non è sempre possibile dividere interi positivi e ottenere un numero intero positivo. Per esempio. 5 / 3 .

Quindi, nonostante gli interi positivi siano un sottoinsieme di numeri reali, non sono un sottotipo. Un argomento simile può essere fatto per interi di dimensione finita. Chiaramente ogni intero a 32 bit è anche un numero intero a 64 bit, ma 32_BIT_MAX + 1 ti darà risultati diversi per ogni tipo. Quindi, se ti avessi dato un programma e avessi cambiato il tipo di ogni variabile intera a 32 bit in interi a 64 bit, ci sono buone probabilità che il programma si comporti in modo diverso (il che significa quasi sempre erroneamente ).

Ovviamente, è possibile definire + per i valori di 32 bit in modo che il risultato sia un numero intero a 64 bit, ma ora è necessario riservare 64 bit di spazio ogni volta che si aggiungono due numeri a 32 bit. Ciò potrebbe essere accettabile o meno a seconda delle tue esigenze di memoria.

Perché questo è importante?

È importante che i programmi siano corretti. È probabilmente la proprietà più importante per un programma. Se un programma è corretto per un certo tipo A , l'unico modo per garantire che il programma continui a essere corretto per alcuni sottotipo B è se B si comporta come A in ogni modo.

Quindi hai il tipo di Rectangles , la cui specifica dice che i suoi lati possono essere modificati indipendentemente. Hai scritto alcuni programmi che usano Rectangles e presumi che l'implementazione segua le specifiche. Quindi hai introdotto un sottotipo chiamato Square i cui lati non possono essere ridimensionati in modo indipendente. Di conseguenza, la maggior parte dei programmi che ridimensionano i rettangoli ora saranno sbagliati.

    
risposta data 07.05.2014 - 14:18
fonte
6

If a Square is a type of Rectangle than why cant a Square inherit from a Rectangle? Or why is it a bad design?

Per prima cosa, chiediti perché pensi che un quadrato sia un rettangolo.

Naturalmente la maggior parte delle persone l'ha imparato nella scuola elementare, e sembrerebbe ovvio. Un rettangolo è una forma a 4 lati con angoli di 90 gradi e un quadrato soddisfa tutte queste proprietà. Quindi non è un quadrato un rettangolo?

Il fatto è che tutto dipende da quali sono i criteri iniziali per il raggruppamento degli oggetti, quale contesto stai guardando questi oggetti. Nelle forme geometriche sono classificati in base alle proprietà dei loro punti, linee e angeli.

Quindi, prima ancora di dire "un quadrato è un tipo di rettangolo" devi prima chiedertelo, è basato sui criteri a cui tengo .

Nella grande maggioranza dei casi non sarà affatto ciò che ti interessa. La maggior parte dei sistemi che modellano forme, come GUI, grafica e videogiochi, non sono principalmente interessati al raggruppamento geometrico di un oggetto, ma è un comportamento. Hai mai lavorato su un sistema che importava che un quadrato fosse un tipo di rettangolo in senso geometrico. Cosa ti darebbe anche, sapendo che ha 4 lati e 90 gradi di angolo?

Non stai modellando un sistema statico, stai modellando un sistema dinamico in cui le cose stanno per accadere (le forme verranno create, distrutte, alterate, disegnate ecc.). In questo contesto ti preoccupi del comportamento condiviso tra gli oggetti, perché la tua preoccupazione principale è cosa puoi fare con una forma, quali regole devono essere mantenute per avere ancora un sistema coerente.

In questo contesto, un quadrato non è assolutamente un rettangolo , perché le regole che governano come il quadrato può essere modificato non sono le stesse del rettangolo. Quindi non sono lo stesso tipo di cosa.

In tal caso non modellarli come tali. Perchè vorresti? Non ti guadagna altro da una restrizione inutile.

It would only be usable if we create the Square object, and if we override the SetWidth and SetHeight methods for Square than why would there be any issue?

Se lo fai anche se stai praticamente affermando nel codice che non sono la stessa cosa. Il tuo codice direbbe che un quadrato si comporta in questo modo e un rettangolo si comporta in questo modo ma sono sempre gli stessi.

Ovviamente non sono la stessa cosa nel contesto che ti interessa perché hai appena definito due comportamenti diversi. Quindi, perché fingere di essere uguali se sono simili solo in un contesto di cui non ti interessa?

Ciò evidenzia un problema significativo quando gli sviluppatori si rivolgono a un dominio che desiderano modellare. È così importante chiarire a quale contesto sei interessato prima di iniziare a pensare agli oggetti nel dominio. A quale aspetto ti interessi. Migliaia di anni fa i greci si preoccupavano delle proprietà condivise delle linee e degli angeli di forme e li raggruppavano in base a questi. Ciò non significa che sei costretto a continuare quel raggruppamento se non è quello a cui tieni (che nel 99% delle volte si modella nel software di cui non ti importa)

Molte delle risposte a questa domanda si concentrano sulla sottotipazione sul comportamento di raggruppamento perché causano le regole .

Ma è così importante capire che non lo stai facendo solo per seguire le regole. Lo stai facendo perché nella stragrande maggioranza dei casi questo è ciò che ti interessa davvero. Non ti importa se un quadrato e un rettangolo condividono gli stessi angeli interni. Ti importa di quello che possono fare pur essendo ancora quadrati e rettangoli. Ti interessa il comportamento degli oggetti perché stai modellando un sistema focalizzato sulla modifica del sistema in base alle regole del comportamento degli oggetti.

    
risposta data 08.05.2014 - 10:56
fonte
5

If a Square is a type of Rectangle than why cant a Square inherit from a Rectangle?

Il problema sta nel pensare che se le cose sono collegate in qualche modo nella realtà, devono essere correlate esattamente nello stesso modo dopo la modellazione.

La cosa più importante nella modellazione è identificare gli attributi comuni e i comportamenti comuni, definirli nella classe base e aggiungere attributi aggiuntivi nelle classi figlie.

Il problema con il tuo esempio è, che è completamente astratto. Finché nessuno lo sa, per cosa prevedi di usare le classi, è difficile indovinare quale design dovresti fare. Ma se vuoi veramente solo altezza, larghezza e ridimensionamento, sarebbe più logico:

  • definisce Square come classe base, con width parametro e resize(double factor) ridimensiona la larghezza per il fattore specificato
  • definisce la classe Rectangle e la sottoclasse di Square, perché aggiunge un altro attributo, height , e sovrascrive la sua funzione resize , che chiama super.resize e quindi ridimensiona l'altezza in base al fattore specificato

Dal punto di vista della programmazione, non c'è nulla in Square, che Rectangle non abbia. Non ha senso creare Square come sottoclasse di Rectangle.

    
risposta data 07.05.2014 - 16:03
fonte
4

Perché per LSP, la creazione della relazione di ereditarietà tra i due e la sovrascrittura di setWidth e setHeight per garantire che Square abbia entrambi lo stesso introduce un comportamento confuso e non intuitivo. Diciamo che abbiamo un codice:

Rectangle r = createRectangle(); // create rectangle or square here
r.setWidth(10);
r.setHeight(20);
print(r.getWidth()); // expect to print 10
print(r.getHeight()); // expect to print 20

Ma se il metodo createRectangle ha restituito Square , perché è possibile grazie a Square che eredita da Rectange . Quindi le aspettative si infrangono. Qui, con questo codice, ci aspettiamo che l'impostazione della larghezza o dell'altezza causerà solo il cambiamento rispettivamente di larghezza o altezza. Il punto di OOP è che quando lavori con la superclasse, hai zero conoscenza di qualsiasi sottoclasse sottostante. E se la sottoclasse cambia il comportamento in modo che vada contro le aspettative che abbiamo sulla superclasse, allora c'è un'alta probabilità che si verifichino dei bug. E questi tipi di bug sono entrambi difficili da correggere e correggere.

Una delle idee principali su OOP è che si tratta di un comportamento, non di dati ereditati (che è anche uno dei principali fraintendimenti IMO). E se guardi il quadrato e il rettangolo, non hanno alcun comportamento che possiamo riguardare nella relazione di ereditarietà.

    
risposta data 07.05.2014 - 08:37
fonte
2

Ciò che LSP dice è che tutto ciò che eredita da Rectangle deve essere un Rectangle . Cioè, dovrebbe fare tutto ciò che un Rectangle fa.

Probabilmente la documentazione per Rectangle è scritta per dire che il comportamento di un Rectangle chiamato r è il seguente:

r.setWidth(10);
r.setHeight(20);
print(r.getWidth());  // prints 10

Se il tuo quadrato non ha lo stesso comportamento, allora non si comporta come un Rectangle . Quindi LSP dice che non deve ereditare da Rectangle . Il linguaggio non può applicare questa regola, perché non può impedirti di fare qualcosa di sbagliato in un metodo di sovrascrittura, ma ciò non significa che "è OK perché il linguaggio mi consente di ignorare i metodi" è un argomento convincente per farlo!

Ora, sarebbe possibile scrivere la documentazione per Rectangle in modo tale che non implichi che il codice precedente stampi 10, nel qual caso forse il tuo Square potrebbe essere un Rectangle . Potresti vedere una documentazione che dice qualcosa come "questo fa X. Inoltre, l'implementazione in questa classe fa Y". In tal caso, si ha una buona idea per estrarre un'interfaccia dalla classe e distinguere tra ciò che l'interfaccia garantisce e ciò che la classe garantisce in aggiunta a ciò. Ma quando la gente dice che "un quadrato mutabile non è un rettangolo mutevole, mentre un quadrato immutabile è un rettangolo immutabile", in sostanza stanno assumendo che quanto sopra sia effettivamente parte della ragionevole definizione di un rettangolo mutevole.

    
risposta data 07.05.2014 - 18:25
fonte
1

I sottotipi e, per estensione, la programmazione OO, spesso si basano sul Principio di sostituzione di Liskov, che qualsiasi valore di tipo A può essere usato dove è richiesta una B, se A < = B. Questo è praticamente un assioma in OO architettura, ie. si presume che tutte le sottoclassi abbiano questa proprietà (e in caso contrario, i sottotipi sono bacati e devono essere corretti).

Tuttavia, si scopre che questo principio è irrealistico / non rappresentativo della maggior parte del codice, o addirittura impossibile da soddisfare (in casi non banali)! Questo problema, noto come problema del rettangolo quadrato o del problema dell'ellisse del cerchio ( link ) è un famoso esempio di quanto è difficile da soddisfare.

Nota che potremmo implementare sempre più quadrati e rettangoli equivalenti, ma solo lanciando sempre più funzionalità finché la distinzione non è utile.

Ad esempio, vedi link

    
risposta data 08.05.2014 - 18:10
fonte

Leggi altre domande sui tag