Il rettangolo dovrebbe ereditare dal quadrato? [duplicare]

10

Quindi, probabilmente abbiamo tutti familiarità con l'esempio fornito nella maggior parte dei libri di testo del problema di sostituzione di Liskov che coinvolge una eredita da rettangolo . L'obiezione a questo approccio è che mentre un quadrato "è un" rettangolo in senso matematico, la classe genitrice, il rettangolo, non può essere facilmente descritta in termini di un quadrato e quindi questo modello viola una delle parti fondamentali della programmazione SOLIDA: il Liskov principio di sostituzione.

La mia domanda è che ha senso per noi invertire la dipendenza da "quadrato" è un "rettangolo in rettangolo" è un "quadrato". Ovviamente, in un contesto matematico questo è sbagliato. Ma una grande percentuale del motivo per cui utilizziamo OOP è ridurre la quantità di codice semi univoco che deve essere scritto o incollato da un posto all'altro. Sembra che una relazione tra queste due strutture abbia senso, ma chiaramente non è la relazione genitore-figlio che vediamo nella classica affermazione del problema.

Ho appena terminato un corso sull'ingegneria del software e non credo che questo punto sia stato chiaramente spiegato. So che solo perché esiste una relazione "è una" tra due classi non implica necessariamente che dovrebbe esserci alcuna astrazione, ma sembra soddisfacente in senso matematico perché ci sia una connessione tra le due classi.

Si noti inoltre che sto lavorando, come progetto collaterale, un piccolo motore di gioco e una sorta di astrazione tra di loro potrebbe avere senso.

    
posta ampsthatgoto11 23.12.2016 - 17:57
fonte

7 risposte

33

Il classico esempio di quadrato che non è in grado di sostituire il rettangolo senza violare LSP è un po 'una "domanda trabocchetto" e sofisticata.

Il problema sorge a causa di una conflazione ... cioè un'implementazione di un rettangolo non è in realtà un rettangolo.

Avere larghezza e altezza impostabili in modo indipendente non è una proprietà intrinseca di un rettangolo. Un rettangolo è ancora un rettangolo anche se la sua larghezza e altezza sono immutabili.

Quindi per ogni rettangolo dato, è ragionevole che non si debba assumere nulla riguardo ai vincoli (o alla mancanza di vincoli) sulle sue dimensioni. Un rettangolo matematico è proprio questo, niente di meno, niente di più, e un quadrato matematico è in realtà un esempio perfetto specifico di un rettangolo matematico.

Tuttavia un rettangolo che garantisce una larghezza e un'altezza regolabili indipendentemente (come si farebbe normalmente per codificare il comportamento per rendere la classe utile) non è in realtà un rettangolo ... è qualcos'altro. È un rettangolo PIÙ certi comportamenti addizionali garantiti.

Quindi questo esempio si presenta perché crea nuove cose , che non sono né rettangoli né quadrati, ma mis-etichette loro "rettangoli" e "quadrati" e quindi esclama " whoa! Ecco un caso strano di alcuni sottoinsiemi perfetti che violano LSP! ". Beh, non è affatto così. Sono state create nuove cose che non sono né rettangoli né quadrati.

Per rispondere direttamente alla domanda dell'OP, non ha senso né matematicamente né in informatica considerare un rettangolo come una specie di quadrato. Non riesco a vedere alcuna buona ragione per cui si eredita il rettangolo dal quadrato. Creare metodi in quell'architettura porterebbe rapidamente poca somiglianza / mappatura naturale a ciò che queste parole significano per noi normalmente.

    
risposta data 23.12.2016 - 22:57
fonte
6

Il principio di sostituzione di Liskov afferma che se un modulo di programma utilizza una classe Base, il riferimento alla classe Base può essere sostituito con una classe derivata senza influire sulla funzionalità del modulo del programma.

Quindi:

  • Square non può ereditare da Rectangle .

    Immagina la seguente funzione, copiata senza vergogna da Sviluppo software agile di Robert C. Martin , pagina 115, The Real Problem :

    void g(Rectangle& r)
    {
        r.SetWidth(5);
        r.SetHeight(4);
        assert(r.Area() == 20);
    }
    

    Si potrebbe trovare utile citare il libro alcuni paragrafi sotto:

    One might contend that the problem lays in function g—that the author had no right to make the assumption that width and height were independent. The author of g would disagree. The function g takes a Rectangle as its argument. There are invariants, statements of truth, that obviously apply to a class named Rectangle, and one of those invariants is that height and width are independent. The author of g had every right to assert this invariant. It is the author of Square who has violated the invariant.

    In effetti, ciò che accade è che potremmo immaginare una serie di contratti associati alla classe del rettangolo. Questi contratti possono essere scritti in codice, come in C # o Java, o essere parte dell'interfaccia pubblica, come ad esempio Eiffel o Spec #, o essere solo assunti o scritti sotto forma di commenti.

    Uno dei contratti è suggerito da Robert C. Martin e consiste nella post-condizione di Rectangle::SetWidth(double w) :

    assert((itsWidth == w) && (itsHeight == old.itsHeight));
    

    Square viola questa postcondizione, poiché itsHeight == old.itsHeight restituirebbe false .

  • Rectangle non può ereditare da Square .

    Qui, si applica la stessa logica. Immagina che Rectangle erediti ora da Square . La funzione g(Square& s) modifica la larghezza del quadrato, ad esempio moltiplicandolo per due. Data la definizione del quadrato, è saggio presumere che l'area della superficie sarà moltiplicata per quattro.

    Tuttavia, passare un'istanza della classe Rectangle a g viola l'ipotesi, poiché la nuova superficie raddoppierà invece che quadrupla. La stessa logica si applica qui e significa che ereditare Rectangle da Square è ugualmente male.

risposta data 23.12.2016 - 18:45
fonte
5

Il comportamento dei modelli di progettazione orientati agli oggetti, piuttosto che le proprietà matematiche.

Quindi la risposta breve è che, poiché un quadrato non può agire come un rettangolo e un rettangolo non si comporta come un quadrato, non hanno una relazione genitore-figlio, in entrambi i casi.

La risposta più lunga è che un quadrato è una forma speciale di un rettangolo solo in un dominio specifico, che è proprietà geometriche. OOD si occupa di un dominio diverso, modellando il comportamento degli oggetti interagenti. In quanto tale, se le relazioni non sono rilevanti per questo dominio, dovresti ignorarle. Il tentativo di modellare relazioni che sono effettivamente irrilevanti rispetto al modo in cui il sistema si comporterà causa grossi problemi in un sistema. Attenersi alle relazioni che contano per il comportamento del sistema.

    
risposta data 23.12.2016 - 18:03
fonte
2

L'ereditarietà non si applica affatto, in entrambi i casi. Sebbene ogni quadrato sia un rettangolo, non aggiunge nulla al rettangolo. È solo un punto rettangolare, non estende il rettangolo. L'ereditarietà è inutile a meno che il discendente non sia (previsto) per estendere in qualche modo la classe base.

La confusione deriva dall'etichetta umana "quadrato" che è totalmente arbitraria. Se la gente chiamasse un rettangolo con i lati lunghi due volte la lunghezza dei lati corti un dubbio, e la parola sarebbe nel dizionario inglese, si vedrebbe la stessa domanda sui dubbi e sui rettangoli sullo stackexchange.

Lo stesso vale per cerchi ed ellissi. Ellisse è il tipo, il cerchio è solo un'incarnazione comune di esso.

    
risposta data 24.12.2016 - 01:08
fonte
2

Rectangle non deve ereditare da Square . Un quadrato è una forma speciale di un rettangolo (dove tutti i lati hanno la stessa lunghezza). Quindi la forma generale è Rectangle e Square è una specializzazione. Quindi puoi disegnare una generalizzazione da Square a Rectangle , ma non viceversa.

Per quanto riguarda la sostituzione di Liskov: se si introducono vincoli, si aggirano le insidie. Sicuramente si è tentati di "ereditare" ma ovviamente hai ancora il cervello. E questo ha rivelato il problema dietro l'eredità del cerchio / ellissi. Quindi devi essere consapevole di ciò che stai facendo e prendi cura del non ovvio. E: la programmazione dogmatica non è necessariamente una buona programmazione.

Diamo un'occhiata a un design semplice:

Poiché height/width è protetto, è necessario disporre dei metodi di setter per modificarli. E il vincolo su Square garantirà che sia l'altezza che la larghezza devono essere uguali. Ora, ci possono essere diverse implementazioni per Square . O un setHeight come effetto collaterale altera la larghezza (che non sembra essere una buona idea). O si genera un'eccezione quando si tenta di farlo, che può essere implementata nelle sostituzioni. Per comodità, puoi aggiungere setSize con un solo int per altezza / larghezza che è disponibile solo in Square . Puoi estendere questo design in modo simile con vari metodi.

    
risposta data 23.12.2016 - 18:11
fonte
0

Come hai sottolineato, non è una buona idea per Square ereditare da Rectangle modificabile (qualsiasi nuovo codice in Rectangle potrebbe creare Square s che non sono, beh, i quadrati), né è una buona idea per Rectangle ereditare da Square (il rettangolo generale è non un quadrato, quindi è una chiara violazione della regola di sostituibilità).

Il problema deve essere affrontato in un altro modo. Le opzioni sono:

  • Rendi immutabili sia Square che Rectangle . Se il costruttore è l'unico punto in cui è possibile impostare le lunghezze, non c'è pericolo che un Square diventi improvvisamente un non quadrato attraverso l'uso di metodi in Rectangle . Inoltre, questo aiuta a implementare la semantica del valore rigoroso, che rende il codice molto più leggibile.

  • Dimentica Square e usa semplicemente Rectangle che ha la stessa larghezza e altezza. Può essere combinato con l'immutabilità come sopra, e potresti voler aggiungere un costruttore che accetta solo un argomento di dimensione singola per costruire un Rectangle che sembra essere un quadrato.

  • Se hai bisogno comunque di trasformazioni affini, puoi anche percorrere la strada esattamente opposta: hai solo un quadrato che occupa il rettangolo tra (0,0) e (1,1) , e aggiungi una trasformazione affine a ogni piazza per tradurlo, allungalo, ruotalo, taglialo alla forma precisa del parallelogramma che vuoi . Naturalmente, avrai un costruttore per i quadrati allineati sugli assi e uno per i rettangoli allineati sugli assi, ma da quel punto in poi avrai quasi la libertà assoluta. Certo, questa è una soluzione piuttosto pesante, ma piuttosto buona in certi contesti.

Quale soluzione funziona meglio per te dipende interamente dal tuo caso d'uso. Ma ho scoperto che di solito non è un'idea brillante forzare qualcosa in una relazione di ereditarietà che non vuole esserci dentro. Square e Rectangle sono una tale coppia. Invece, cercare soluzioni che evitino il problema in primo luogo. Cioè, ciò che rende un buon sviluppatore di software: la capacità di pensare fuori dagli schemi e di trovare soluzioni creative e semplici che si adattino facilmente al problema attuale.

    
risposta data 24.12.2016 - 14:02
fonte
-3

Penso che possa funzionare in entrambi i modi. È possibile applicare il modello di strategia. Sebbene non ci siano molti oggetti rettangolari di vita reale con lati diversi.

Secondo il modello di strategia, il tuo quadrato può estendere un rettangolo - così come può essere corretto matematicamente. Inoltre, poiché i rettangoli e i quadrati hanno tutti 4 lati, questo dovrebbe essere comune per la classe del rettangolo genitore. Poiché la lunghezza dei lati è diversa, puoi creare un'interfaccia < > e hanno varie lunghezze laterali per implementarlo. Mentre la classe Parent del rettangolo sarà composta da variabili di istanza di SideLengths.

Penso che puoi semplicemente evitare queste complessità e creare solo classi separate di quadrati e rettangoli.

    
risposta data 23.12.2016 - 20:27
fonte

Leggi altre domande sui tag