Il quadrato derivante dal rettangolo è una violazione del principio di sostituzione di Liskov?

Sono nuovo per progettare e imparare i principi del design.

Dice che ricavare il quadrato dal rettangolo è un classico esempio di violazione del Principio di sostituzione di Liskov.

Se è così, quale dovrebbe essere il design corretto?

Credo che il ragionamento sia qualcosa del genere:

Supponiamo che tu abbia un metodo che accetta un rettangolo e ne regola la larghezza:

public void SetWidth(Rectangle rect, int width) { rect.Width = width; } 

Dovrebbe essere perfettamente ragionevole, dato quale sia un rettangolo, assumere che questo test passi:

 Rectangle rect = new Rectangle(50, 20); // width, height SetWidth(rect, 100); Assert.AreEqual(20, rect.Height); 

… perché la modifica della larghezza di un rettangolo non influisce sulla sua altezza.

Tuttavia, supponiamo tu abbia derivato una nuova class Square da Rectangle. Per definizione, un quadrato ha altezza e larghezza sempre uguali. Proviamo di nuovo quel test:

 Rectangle rect = new Square(20); // both width and height SetWidth(rect, 100); Assert.AreEqual(20, rect.Height); 

Quel test fallirà, perché l’impostazione della larghezza di un quadrato su 100 cambierà anche la sua altezza.

Quindi, il principio di sostituzione di Liskov viene violato derivando Square da Rectangle.

La regola “is-a” ha senso nel “mondo reale” (un quadrato è sicuramente una specie di rettangolo), ma non sempre nel mondo del software design.

modificare

Per rispondere alla tua domanda, il design corretto dovrebbe probabilmente essere che sia Rettangolo e Quadrato derivino da una comune class “Poligono” o “Forma”, che non impone alcuna regola riguardo la larghezza o l’altezza.

La risposta dipende dalla mutevolezza. Se il tuo rettangolo e le classi quadrate sono immutabili, allora Square è in realtà un sottotipo di Rectangle ed è perfettamente OK derivare prima dal secondo. Altrimenti, Rectangle e Square potrebbero entrambi esporre un IRectangle senza mutatori, ma derivarne uno dall’altro è sbagliato poiché nessuno dei due tipi è correttamente un sottotipo dell’altro.

Non sono d’accordo che la derivazione del quadrato dal rettangolo necessariamente viola LSP.

Nell’esempio di Matt, se si dispone di un codice che si basa sulla larghezza e sull’altezza che sono indipendenti, in realtà viola LSP.

Se, tuttavia, è ansible sostituire un rettangolo per un quadrato in tutto il codice senza rompere alcun presupposto, quindi non si sta violando LSP.

Quindi si riduce a ciò che il rettangolo di astrazione significa nella tua soluzione.

Ultimamente ho lottato molto con questo problema e ho pensato di aggiungere il mio cappello sul ring:

 public class Rectangle { protected int height; protected int width; public Rectangle (int height, int width) { this.height = height; this.width = width; } public int computeArea () { return this.height * this.width; } public int getHeight () { return this.height; } public int getWidth () { return this.width; } } public class Square extends Rectangle { public Square (int sideLength) { super(sideLength, sideLength); } } public class ResizableRectangle extends Rectangle { public ResizableRectangle (int height, int width) { super(height, width); } public void setHeight (int height) { this.height = height; } public void setWidth (int width) { this.width = width; } } 

Si noti l’ultima class, ResizableRectangle . Spostando la “resizableness” in una sottoclass, otteniamo il riutilizzo del codice migliorando il nostro modello. Pensalo in questo modo: un quadrato non può essere ridimensionato liberamente rimanendo un quadrato, mentre i rettangoli non quadrati possono. Tuttavia, non tutti i rettangoli possono essere ridimensionati, poiché un quadrato è un rettangolo (e non può essere ridimensionato liberamente pur mantenendo la sua “id quadro”). (o_O) Quindi ha senso creare una class Rectangle base che non sia ridimensionabile, poiché questa è una proprietà extra di alcuni rettangoli.

Supponiamo di avere la class Rectangle con le due proprietà (per semplicità public) width, height. Possiamo cambiare queste due proprietà: r.width = 1, r.height = 2.
Ora diciamo che un quadrato è un rettangolo. Ma sebbene il claim sia “un quadrato si comporterà come un rettangolo” non possiamo impostare .width = 1 e .height = 2 su un object quadrato (la tua class probabilmente regola la larghezza se si imposta l’altezza e viceversa). Quindi c’è almeno un caso in cui un object di tipo Square non si comporta come un rettangolo e quindi non è ansible sostituirli (completamente).

Credo che esistano le tecniche OOD / OOP per consentire al software di rappresentare il mondo reale. Nel mondo reale un quadrato è un rettangolo che ha lati uguali. Il quadrato è un quadrato solo perché ha lati uguali, non perché ha deciso di essere un quadrato. Pertanto, il programma OO deve affrontarlo. Ovviamente, se la routine che crea un’istanza dell’object vuole che sia quadrata, potrebbe specificare la proprietà length e la proprietà width uguali alla stessa quantità. Se il programma che utilizza l’object deve sapere in seguito se è quadrato, deve solo chiederglielo. L’object potrebbe avere una proprietà booleana di sola lettura chiamata “Square”. Quando la routine chiamante la richiama, l’object può tornare (Lunghezza = Larghezza). Ora questo può essere il caso anche se l’object rettangolo è immutabile. Inoltre, se il rettangolo è effettivamente immutabile, il valore della proprietà Square può essere impostato nel costruttore e fatto con esso. Perché questo è un problema? L’LSP richiede che gli oggetti secondari siano immutabili da applicare e che il quadrato sia un sottoobject di un rettangolo viene spesso utilizzato come esempio della sua violazione. Ma questo non sembra essere un buon design perché quando la routine di utilizzo richiama l’object come “objSquare”, deve conoscerne i dettagli interni. Non sarebbe meglio se non gli importasse se il rettangolo fosse quadrato o no? E ciò sarebbe dovuto al fatto che i metodi del rettangolo sarebbero corretti a prescindere. C’è un esempio migliore di quando viene violato l’LSP?

Un’altra domanda: in che modo un object è reso immutabile? Esiste una proprietà “Immutabile” che può essere impostata all’istanziazione?

Ho trovato la risposta ed è quello che mi aspettavo. Dato che sono uno sviluppatore VB .NET, questo è ciò che mi interessa. Ma i concetti sono gli stessi in tutte le lingue. In VB .NET si creano classi immutabili rendendo le proprietà di sola lettura e si utilizza il nuovo costruttore per consentire alla routine di creazione dell’istanza di specificare i valori delle proprietà quando viene creato l’object. Puoi anche usare le costanti per alcune proprietà e saranno sempre le stesse. Dalla creazione in avanti l’object è immutabile.

Il problema è che ciò che viene descritto non è realmente un “tipo” ma una proprietà emergente cumulativa.

Tutto quello che hai veramente è un quadrilatero e che sia “quadratura” sia “rettangolo” sono solo artefatti emergenti derivati ​​dalle proprietà degli angoli e dei lati.

L’intero concetto di “Square” (o anche di un rettangolo) è solo una rappresentazione astratta di una collezione di proprietà dell’object in relazione tra loro e l’object in questione, non il tipo di object in e di se stesso.

È qui che si può pensare al problema nel contesto di un linguaggio senza stile, perché non è il tipo che determina se è “un quadrato” ma le proprietà effettive dell’object che determina se è “un quadrato”.

Immagino che se vuoi prendere l’astrazione ancora di più non diresti nemmeno che hai un quadrilatero ma che hai un poligono o anche solo una forma.

È piuttosto semplice 🙂 Più “base” la class (la prima nella catena di derivazione) dovrebbe essere la più generale.

Ad esempio forma -> Rettangolo -> Quadrato.

Qui un quadrato è un caso speciale di un rettangolo (con dimensioni vincolate) e un rettangolo è un caso speciale di una forma.

Detto in un altro modo: usa il test “è un”. Uno scudiero è un rettangolo. Ma una rectange non è sempre un quadrato.