Questo codice risolve l'esempio di principio di sostituzione di Liskov quadrato / rettangolo?

6

Volevo solo verificare di aver compreso correttamente l'LSP e di poterlo risolvere. Sto prendendo il classico problema rettangolo / quadrato e tentando una soluzione:

class Rectangle{
    public $width;
    public $height;

    function setWidth($width){
        $this->width = $width;
    }

    function setHeight($height){
        $this->height = $height;
    }
}

class Square extends Rectangle{

    function setWidth($width){
        $this->width = $width;
        $this->height = $width;
    }

    function setHeight($height){
        $this->height = $height;
        $this->width = $height;
    }
}

Se hai un codice come:

function changeSize(Rectangle $rect){
  $rect->setWidth(10);
  $rect->setHeight(30);
  $this->assertEquals(10,$rect->width);
  $this->assertEquals(30,$rect->height);
}

Quindi ovviamente i rettangoli e i quadrati non sono intercambiabili, poiché piazza introduce un vincolo alla classe genitore. Pertanto, un quadrato non dovrebbe ereditare dai rettangoli.

Ma sicuramente possiamo essere d'accordo sul fatto che sia il quadrato sia il rettangolo sono forme a quattro lati? Questa è la mia soluzione proposta, basata su questa premessa:

abstract class AFourSidedShape{
    public $width;
    public $height;

    abstract public function __construct($width,$height);

    public function scaleUp($percentage){
        $this->height = $this->height + (($this->height / 100) * $percentage);
        $this->width = $this->width + (($this->width / 100) * $percentage);
    }

    public function scaleDown($percentage){
        $this->height = $this->height - (($this->height / 100) * $percentage);
        $this->width = $this->width - (($this->width / 100) * $percentage);
    }
}

class Rectangle extends AFourSidedShape{
    function __construct($width, $height){
        $this->width = $width;
        $this->height = $height;
    }
}

class Square extends AFourSidedShape{
    function __construct($width, $height){
        if($width != $height){
            throw new InvalidArgumentException('Sides must be equal');
        }else{
            $this->width = $width;
            $this->height = $height;
        }
    }
}

Il nostro codice cliente dovrebbe essere cambiato in qualcosa tipo:

function changeSize(AFourSidedShape $shape){
  $origWidth = $shape->width;
  $origHeight = $shape->height;
  $shape->scaleUp(10);
  $this->assertEquals($origWidth + (($origWidth/100) * 10),$shape->width);
  $this->assertEquals($origHeight + (($origHeight/100) * 10),$shape->height);
}

La mia teoria è: rettangoli e quadrati sono in realtà entrambi formati da un lato, quindi non dovrebbe esserci un problema con l'ereditare dalla classe astratta foursidedshape. Mentre il quadrato sta ancora aggiungendo vincoli extra nel costruttore (cioè lanciare un errore se i lati non sono uguali), non dovrebbe essere un problema poiché non abbiamo implementato il costruttore nella classe genitore astratta, e quindi il codice cliente non dovrei fare supposizioni su ciò che puoi / non puoi passare comunque.

La mia domanda è: ho capito LSP, e questo nuovo design risolve il problema LSP per quadrato / rettangolo?

Quando si usano le interfacce come suggerito:

interface AFourSidedShape{
    public function setWidth($width);
    public function setHeight($height);
    public function getWidth();
    public function getHeight();
}

class Rectangle implements AFourSidedShape{
    private $width;
    private $height;

    public function __construct($width,$height){
        $this->width = $width;
        $this->height = $height;
    }

    public function setWidth($width){
        $this->width = $width;
    }

    public function setHeight($height){
        $this->height = $height;
    }

    //getwidth, getheight
}

class Square implements AFourSidedShape{
    private $width;
    private $height;

    public function __construct($sideLength){
        $this->width = $sideLength;
        $this->height = $sideLength;
    }

    public function setWidth($width){
        $this->width = $width;
        $this->height = $width;
    }

    public function setHeight($height){
        $this->height = $height;
        $this->width = $height;
    }

    //getwidth, getheight
}
    
posta user1578653 25.11.2015 - 14:10
fonte

6 risposte

21

Immagino che tu stia cercando di risolvere la violazione tipica "sezione dell'articolo Wikipedia di LSP. Se è così, non l'hai risolto e la sezione indica chiaramente perché. In particolare, inizia con il setup: "La classe Square che deriva da una classe Rectangle, assumendo che esistano metodi getter e setter sia per la larghezza che per l'altezza." LSP afferma che la sottoclasse dovrebbe essere in grado di stare in piedi per la super classe.

Ma non è solo questo! Questo è ciò che tu e i tuoi commentatori mancano. Se si trattava solo di torturare il tuo progetto fino a quando non si poteva fare lo swap (tutto è un AFourSidedShape ), si può semplicemente progettare un oggetto generico (senza forma o comportamento), avere tutto ereditato dall'oggetto e quindi scambiare implementazioni specifiche. Pensa a cosa succederebbe se lo facessi. Saresti costantemente interrogato sugli oggetti per determinare cosa possono fare e / o violare le loro condizioni post. Questo è ciò che odia l'LSP.

Quindi nel tuo caso, un AFourSidedShape non risolve davvero nulla perché verificherai costantemente se le tue implementazioni specifiche fossero Rettangoli o Quadretti per garantire che le condizioni del post siano corrette - la larghezza di un Quadrato può cambiare magicamente quando aggiorni la sua altezza ma non quella di un rettangolo. Questo è ciò che le sezioni significano quando dice "questi metodi indeboliranno (violeranno) le post-condizioni per i setter di Rectangle". Non puoi allontanarti dal comportamento non ideale. È un compromesso. (A meno che non rendi i tuoi oggetti immutabili! Evviva l'immutabilità!)

Non sentirti male. Il rettangolo / quadrato con getter / setter per problemi di larghezza / altezza è pensato per essere irrisolvibile in senso banale. È un ottimo, semplice esempio del perché LSP è difficile in pratica.

    
risposta data 25.11.2015 - 15:58
fonte
10

L'LSP riguarda il contratto di una classe e le classi ereditate devono ancora soddisfare lo stesso contratto della loro classe base. Un'interfaccia solo in codice generalmente definisce solo parti di quel contratto, principalmente la parte sintattica, e la semantica potrebbe essere parzialmente fornita da nomi descrittivi dei metodi o dei parametri. Altre parti del contratto sono spesso definite nei commenti, aggiungendo "dichiarazioni di asserzione", facendo uso di caratteristiche linguistiche specifiche, oppure potrebbero essere codificate in test unitari.

Quindi, se hai risolto la violazione LSP in realtà, dipende dal contratto semantico completo della tua interfaccia. Se il contratto appare come questo (che è IMHO il comportamento più ovvio):

// contract: a four sided shape is an object with two individual, independent
// properties "width" and "height"
interface AFourSidedShape{
    public function setWidth($width);
    public function setHeight($height);
    public function getWidth();
    public function getHeight();
}

allora la classe Square non soddisfa lo stesso contratto della sua classe base, quindi viola ancora l'LSP. Si potrebbe scrivere in termini più formali in termini di "condizioni post", controllando che ogni volta che si chiama setHeight , il valore di getWidth non cambi, e viceversa.

Se, tuttavia, il tuo contratto semantico appare così:

 // contract: a four sided shape is an object with two 
 // properties "width" and "height" which must not 
 // be assumed to be independent; maybe changing one can change the other
 interface AFourSidedShape{
     // ...
 }

quindi non c'è più nessuna violazione LSP. Dalla tua domanda e dal modo in cui descrivi i vincoli del costruttore, immagino che questo sia il contratto che hai in mente. Tuttavia, quest'ultimo potrebbe violare il principio di minimo stupore , un "setter" che per alcuni oggetti cambia un altro valore, e per altri no, possono essere sbalorditivi per l'utente medio della tua classe. È meglio evitare di mettere tali setter nell'interfaccia comune, LSP obbedito o meno.

Nota a margine : ovviamente, il problema originale di Square / Rectangle può essere inteso come "esiste un'implementazione in cui Square deriva da Rectangle o viceversa direttamente che non violare la LSP ". Se si legge in questo modo, @ScantRoger è corretto e il tuo progetto risolve un problema diverso. Tuttavia, ho letto il problema in modo abbastanza diverso poiché esiste un modo per incorporare l'ereditarietà per trattare rettangoli e quadrati in modo generico senza violare l'LSP , e per questo problema la risposta è "sì, il tuo suggerimento risolvi quel problema ".

    
risposta data 25.11.2015 - 16:43
fonte
5

In modo pratico, questo è stato risolto molte volte.

Programmi come Inscake, Photoshop, The Gimp, Illustrator e così via non hanno rettangoli separati e strumenti quadrati nei loro pallets. Hanno solo uno strumento per disegnare rettangoli e quadrati. E non lo chiamano "quadrilatero" , lo chiamano rettangolo. Alcuni lo chiamano anche "strumento per rattangoli e quadrati", quindi gli utenti confusi non migrerebbero ad altre app che permetterebbero loro di disegnare quadrati.

Quindi mi sembra che abbiano trovato un modo per farlo funzionare per loro. E sospetto che quello che hanno fatto sia ipotizzare che un quadrato sia solo un rettangolo con uguale altezza e larghezza. Ti consentono persino di utilizzare il tasto Maiusc durante il disegno per bloccarlo in modo da creare un quadrato.

Non penso davvero che quei programmi passino da un oggetto rettangolo a un oggetto quadrato completamente diverso e incompatibile quando si preme shift, solo per tornare a un rettangolo incompatibile quando si rilascia il tasto Maiusc. Ciò potrebbe essere facilmente corroborato scaricando il codice sorgente di quei programmi che sono open-source, come Inskscape o The Gimp.

Quindi penso che quando hai cambiato il progetto dall'ereditarietà di una superclasse all'implementazione di un'interfaccia, tu l'hai "risolto".

Perché nel caso dell'ereditarietà stai affermando che un quadrato è un rettangolo, che è falso. D'altra parte quando si implementa un'interfaccia, si sta dicendo che un quadrato può agire come un rettangolo. In quello scenario ciò che i metodi fanno o non fanno sotto la cappa è un dettaglio di implementazione. Meglio ancora se ti dimentichi completamente dei quadrati, tutto va bene perché niente ti impedisce di creare un rettangolo con lati uguali.

Quindi è davvero un problema filosofico e non pratico.

Nella definizione di Wikipedia

A square...It can also be defined as a rectangle in 
which two adjacent sides have equal length.
    
risposta data 25.11.2015 - 17:03
fonte
1

L'LSP e l'ereditarietà significano fondamentalmente: la classe figlia può fare tutto ciò che la classe genitrice può e un po 'di più.

E qui arriva il problema:

  • Per un quadrato: puoi piegare un quadrato in un triangolo di mezzo quadrato. Il che significa che potresti fare un metodo: "makequaretriangle". Questo non è vero per tutti i rettangoli.

  • Per un rettangolo: se moltiplichi l'altezza di un fattore e la larghezza per l'inverso di quel fattore, avrà comunque la stessa superficie. Il che significa che puoi creare un metodo: "doublemyheightbutkeepsurface" che raddoppia l'altezza e dimezza automaticamente la larghezza.
    Questo non è vero per un quadrato.

Quindi vedi, sia il quadrato che il rettangolo hanno metodi che non hanno senso sull'altro, per questo non possono essere sempre sostituiti. E perché non possono ereditare gli uni dagli altri in un modo o nell'altro.

L'LSP è specifico per i casi in cui vuoi ereditare A da B o vuoi ereditare B da A, ma non puoi. Gli altri casi in cui si ereditano sia B che A da una classe base comune non rientrano nell'ambito di applicazione dell'LSP. Quindi la tua soluzione non ha nulla a che fare con il LSP.

    
risposta data 25.11.2015 - 16:38
fonte
0

AFoursidedShape nasconde le informazioni sulle dipendenze tra altezza e larghezza, quindi LSP non viene violato come nel caso di Square inhereting from Rectangle.

Il rettangolo e il quadrato della classe sono rappresentazioni di quadrati e rettangoli, quindi non è necessario condividere la stessa relazione di ciò che rappresentano.  In altre parole, un quadrato matematico ha una relazione con il rettangolo matematico, ma ciò non implica che il quadrato di classe debba necessariamente avere lo stesso rapporto con il rettangolo di classe. Se vogliamo ancora trattare Square come Rectangle un altro modo di farlo senza violare LSP potrebbe essere quello di avere un'interfaccia che si chiama ARectangleReader che ha solo un contratto per i geter getHeight e getWidth che è implementato dalla classe Square e dalla classe Rectangle.

Un altro modo di pensarci: un quadrato mutabile si comporta davvero come un rettangolo mutabile?

    
risposta data 03.12.2015 - 00:41
fonte
0

Vedo che è una vecchia domanda, ma voglio dare i miei 2 centesimi.

Non hai slove il problema e il tuo design non rispetta l'LSP, perché un utente potrebbe avere questo codice:

AFourSidedShape square = new Square(5, 5);
... //Later in code.
shape.setWidth(4);
shape.setHeight(3);
... //And later.
shape.getHeight() * shape.getWidth() // The user would expect its area is 12 but its currently 9.

Primo: prova a manipolare le forme nel modo più generale possibile, i quadrati sono rettangoli, quindi manipola i quadrati come rettangoli e lascia che sia il resto ad occuparsi del resto del codice, ad esempio, se hai un programma di pittura, sarebbe bene lasciarlo gli utenti creano quadrati, rettangoli quando un utente crea un quadrato usando la GUI, in realtà crea un rettangolo che ha la stessa larghezza e altezza e quindi lascia che l'interfaccia lo mostri, per le operazioni di ridimensionamento, potresti anche offrire la possibilità di crescere e mantenere la larghezza / altezza di una percentuale, il controllore di questa azione applicherà la formula generale e funzionerà.

Secondo: dovresti fidarti degli oggetti, ecco cos'è l'incapsulamento, Prova ad inserire le proprietà comuni sull'interfaccia, ad esempio:

interface Shape { //This interface represents all the 2D shapes.
    double getArea();
    double getPerimeter();
}

class Rectangle implements Shape {
    private width;
    private height;
    ...
    public double getArea() { return width * height; }
    public double getPerimeter() { return width * 2 + height * 2; }
}

e quindi implementa quei metodi sulle classi specificate, facendo così, incapsuliamo l'area e il calcolo perimetrale all'interno della classe, questo è decisamente meglio del calcolo di queste proprietà al di fuori dell'oggetto shape, perché rivela che ci si fida di cosa l'oggetto ti darà, ma quando lo fai fuori dall'oggetto, sembra che non ti fidi di quell'oggetto e assegni questa responsabilità a un altro oggetto (che in effetti ti fidi di esso).

    
risposta data 22.11.2017 - 13:06
fonte