PDA

Visualizza la versione completa : Principio di sostituzione di Liskov


gaten
09-12-2013, 22:11
Buonasera ragazzi,

volevo fare una domanda riguardo al principio di sostituzione di Liskov, il quale ci dice deve essere rispettata la sostitutività dei tipi derivati rispetto ai tipi base.

Il classico esempio del rettangolo e del quadrato, dove il quadrato è visto come specializzazione di rettangolo.



class Rectangle
{
protected int m_width;
protected int m_height;
public void setWidth(int width){ m_width = width; }
public void setHeight(int height){ m_height = height; }
public int getWidth(){ return m_width; }
public int getHeight(){ return m_height; }
public int getArea(){ return m_width * m_height; }
}
class Square extends Rectangle
{
public void setWidth(int width){ m_width = width; m_height = width; }
public void setHeight(int height){ m_width = height; m_height = height; }
}
}


Qualora andassi a fare:



class LspTest
{
private static Rectangle getNewRectangle()
{
// it can be an object returned by some factory ...
return new Square();
}
public static void main (String args[])
{
Rectangle r = LspTest.getNewRectangle();
r.setWidth(5);
r.setHeight(10);
// user knows that r it's a rectangle.
// It assumes that he's able to set the width and height as for the
base class
System.out.println(r.getArea());
// now he's surprised to see that the area is 100 instead of 50.
}
}



"Crediamo" di avere a che fare con un rettangolo ma alla fine ci ritroviamo che l'area del rettangolo avente base 5 e altezza 10 è uguale a 100 anzichè 50.

Volevo capire come si risolve, qual è la soluzione riguardo al principio di sostituzione di Liskov.
Nell'esempio si fà override dei metodi setWeight e setHeight, e alla fine esce fuori un macello poichè non viene rispettata la sostitutività dei tipi derivati.

MItaly
09-12-2013, 23:38
[Parzialmente OT] Secondo me gli esempi con le figure geometriche non funzionano mai bene con l'OOP "alla C++" (e Java, C# e compagnia), perché aggiungono constraint sui parametri invece di implementare comportamenti più specializzati rispetto alla classe base, oltre al fatto che poi ci si ritrova con situazioni complicate da gestire come quadrato che dovrebbe gestire ereditarietà multipla da rombo e da rettangolo. Ma aspetto anch'io una risposta da qualcuno che abbia una preparazione teorica più solida della mia. :stordita: [/Parzialmente OT]

shodan
10-12-2013, 13:04
Diciamo che il problema in esame enfatizza veri e propri errori concettuali: un quadrato è un quadrato, un rettangolo un rettangolo. Il fatto che il quadrato sia un caso particolare di rettangolo è perché a noi fa comodo vederlo così. Il problema si ripropone pari pari con il problema dello studente lavoratore etc.
Il modo più semplice è definire il quadrato come una figura geometrica e il rettangolo idem come spiegato qui:
http://www.eptacom.net/pubblicazioni/pub_it/ered.html dove entrambi gli esempi sono affrontati.

gaten
11-12-2013, 01:00
Quindi, in questo caso, conviene creare una interfaccia "Figura" e "Rettangolo" e "Quadrato" che implementano l'interfaccia "Figura", dopodichè implemento distintamente i metodi setWeight e setHeight in Rettangolo e Quadrato.

Scara95
11-12-2013, 08:02
Diciamo che il problema in esame enfatizza veri e propri errori concettuali: un quadrato è un quadrato, un rettangolo un rettangolo. Il fatto che il quadrato sia un caso particolare di rettangolo è perché a noi fa comodo vederlo così. Il problema si ripropone pari pari con il problema dello studente lavoratore etc.
Il modo più semplice è definire il quadrato come una figura geometrica e il rettangolo idem come spiegato qui:
http://www.eptacom.net/pubblicazioni/pub_it/ered.html dove entrambi gli esempi sono affrontati.

tecnicamente un quadrato è un caso speciale di rettangolo, e rettangolo sarebbe un caso speciale di parallelogramma, rombo un caso speciale di parallelogramma e quadrato un caso speciale di rombo. E questo lo dice la geometria, infatti ogni caso speciale può essere trattato come il caso base. Non puoi negare che un quadrato sia anche un rettangolo. Tuttavia non ho una soluzione per il problema di programmazione.

shodan
11-12-2013, 14:32
D'accordo, ma la geometria è stata inventata e sviluppata dall'uomo. E se all'uomo fa comodo pensare un quadrato come a un caso speciale di rettangolo o rombo non è che la natura pensi allo stesso modo.
"In natura non esistono linee dritte" disse Gaudì a proposito delle sue costruzioni strane.
Nella OOP pensare un quadrato come un rettangolo e/o rombo particolare (geometricamente parlando) porta a creare interfacce sconnesse e piene di errori (e non lo dico io, ma l'articolo).
La soluzione la suggerisci tu quando dici:


tecnicamente un quadrato è un caso speciale di rettangolo, e rettangolo sarebbe un caso speciale di parallelogramma

ergo "un quadrato è un caso speciale di parallelogramma".
Io parlo di figura geometrica più generale, ma il concetto è quello.

@gaten


Quindi, in questo caso, conviene creare una interfaccia "Figura" e "Rettangolo" e "Quadrato" che implementano l'interfaccia "Figura", dopodichè implemento distintamente i metodi setWeight e setHeight in Rettangolo e Quadrato.

Io farei proprio così per semplicità. Se si trattasse di C++ al massimo farei ereditare Quadrato da Rettangolo in modo protected, ma poi come la mettiamo col Rombo?
Tra l'altro io sarei anche più "talebano": toglierei la possibilità di modificare le dimensioni di una figura i metodi appositi set_qualcosa() e ne permetterei il dimensionamento solo attraverso il costruttore e successivo assegnamento.

Scara95
11-12-2013, 15:23
D'accordo, ma la geometria è stata inventata e sviluppata dall'uomo. E se all'uomo fa comodo pensare un quadrato come a un caso speciale di rettangolo o rombo non è che la natura pensi allo stesso modo.
"In natura non esistono linee dritte" disse Gaudì a proposito delle sue costruzioni strane.
qui cadi in contradizione: dato che la natura non ha forme rigide la natura non può pensare niente del quadrato perché non esiste.


Nella OOP pensare un quadrato come un rettangolo e/o rombo particolare (geometricamente parlando) porta a creare interfacce sconnesse e piene di errori (e non lo dico io, ma l'articolo). questo è un problema dell'OOP e non della geometria.


La soluzione la suggerisci tu quando dici:

ergo "un quadrato è un caso speciale di parallelogramma".
Io parlo di figura geometrica più generale, ma il concetto è quello.
errato: il parallelogramma è solo un caso speciale di poligono a 4 lati che a sua volta è un caso speciale di poligono che è un caso speciale di poligonale... figura. Inoltre rombo e rettangolo sono un caso speciale di parallelogramma e quadrato un caso speciale di questi ultimi due.
Edit: ovviamente il quadrato è anche un caso speciale di parallelogramma, ma lo è allo stesso modo in cui è un caso speciale di linea.


@gaten

Io farei proprio così per semplicità. Se si trattasse di C++ al massimo farei ereditare Quadrato da Rettangolo in modo protected, ma poi come la mettiamo col Rombo?
Una soluzione già accennata da MItaly è la ereditarietà multipla.


Tra l'altro io sarei anche più "talebano": toglierei la possibilità di modificare le dimensioni di una figura i metodi appositi set_qualcosa() e ne permetterei il dimensionamento solo attraverso il costruttore e successivo assegnamento.
Questa è la soluzione più logica ed è anche il motivo per cui nei linguaggi che adottano principi funzionali non si incorre in questi problemi.

shodan
11-12-2013, 16:47
qui cadi in contradizione: dato che la natura non ha forme rigide la natura non può pensare niente del quadrato perché non esiste.

Io non la vedo. Se in natura non esistono quadrati, allora il problema quadrato => caso particolare del rettangolo è solo un problema umano. E dato che la geometria è stata inventata dall'uomo, all'uomo fa comodo definire un caso particolare del rettangolo, quadrato.



questo è un problema dell'OOP e non della geometria.

Ma qui si sta parlando di OOP non di geometria. Se per sua natura l'OOP è meno precisa della geometria, si adatta la geometria all'OOP non viceversa.


errato: il parallelogramma è solo un caso speciale di poligono a 4 lati che a sua volta è un caso speciale di poligono che è un caso speciale di poligonale... figura. Inoltre rombo e rettangolo sono un caso speciale di parallelogramma e quadrato un caso speciale di questi ultimi due.

E quindi? Il concetto rimane quello di raffinare il grezzo. Il punto di partenza scelto è tra quelli che ci fa più comodo. Altrimenti dovrei dire che in ultima analisi il quadrato è un caso particolare di punto in movimento. Bello, ma poco pratico :)


Una soluzione già accennata da MItaly è la ereditarietà multipla.

Se non ricordo male Java ha tolto questa possibilità e nemmeno in C++ che chi ne è entusiasta. Io in C++ userei l'ereditarietà protetta, più che altro per nascondere l'interfaccia del rettangolo e definire la nuova interfaccia solo per il quadrato. Ma si può fare in Java (non lo conosco molto)?


Questa è la soluzione più logica ed è anche il motivo per cui nei linguaggi che adottano principi funzionali non si incorre in questi problemi.

Ce ne saranno altri. Non mancano i problemi, mancano le soluzioni :)

MItaly
11-12-2013, 17:03
Il discorso secondo me è che in OOP all'atto pratico non importa definire una tassonomia rigida di classi basata su criteri arbitrari - in questo caso, una classificazione basata sulle lunghezze sul parallelismo dei lati. L'utilità delle classi e dell'ereditarietà si manifesta piuttosto essenzialmente da due punti di vista:

implementazione di un'interfaccia - ovvero, i miei tipi possono essere usati da codice che ci lavora sopra in maniera generica, sfruttando le sole funzioni definite nell'interfaccia;
riutilizzo del codice delle classi basi - ovvero, la mia classe può riciclare il grosso del codice definito nelle classi base per portare a termine i suoi compiti.


In questo caso, una gerarchia di classi che ricalchi rigidamente la classificazione geometrica non dà alcun vantaggio pratico, perché l'interfaccia che ha senso avere in comune tra le varie figure alla fine è al più quella di un quadrilatero (e solo a livello di metodi getter); in altri termini, per quello che ci importa in OOP un quadrato in genere non è un rettangolo e un rombo, perché riciclare l'interfaccia di un rettangolo o di un rombo per il quadrato ci dà più che altro inutili complicazioni e scarso riutilizzo di codice.

Scara95
11-12-2013, 17:15
Io non la vedo. Se in natura non esistono quadrati, allora il problema quadrato => caso particolare del rettangolo è solo un problema umano. E dato che la geometria è stata inventata dall'uomo, all'uomo fa comodo definire un caso particolare del rettangolo, quadrato.

Invece c'è perché dato che siamo noi a creare la gerarchia definendola siamo noi che abbiamo creato il quadrato. La natura non ha una definizione di quadrato e non ha una definizione di figura per il semplice fatto che queste non esistono ma sono nostre rappresentazioni (Che Schopenhauer sia con me). A parte tutto questo discorso filosofico: è semplicemente innaturale per l'uomo non pensare al quadrato come un caso speciale di rettangolo e rombo e, di conseguenza, è innaturale per ogni cosa "creata dall'uomo". Che poi tu dica: "Meglio andare contro le definizioni e la natura umana perché così è più semplice" è un'altro discorso, ma così non definisci la corretta e gerarchia.



Ma qui si sta parlando di OOP non di geometria. Se per sua natura l'OOP è meno precisa della geometria, si adatta la geometria all'OOP non viceversa.

OOP è un po' generico: ad esempio già utilizzando Eiffel come linguaggio si dimezzano i problemi.


E quindi? Il concetto rimane quello di raffinare il grezzo. Il punto di partenza scelto è tra quelli che ci fa più comodo. Altrimenti dovrei dire che in ultima analisi il quadrato è un caso particolare di punto in movimento. Bello, ma poco pratico :)

No, il concetto è diverso: puoi partire da più in alto o saltare delle astrazioni se non ti servono, ma non puoi modificare la gerarchia pretendendo che rimanga "corretta in linea teorica". Può funzionare anche da alterata, ma non è comunque corretta: ed è proprio qui che emergono evidenti problemi nell'oop, specialmente in quello a ereditarietà singola: perché come principio si propone di ricostruire la gerarchia reale, ma non ne è in grado perché troppo limitato (già Eiffel migliora molto la situazione).


Se non ricordo male Java ha tolto questa possibilità e nemmeno in C++ che chi ne è entusiasta. Io in C++ userei l'ereditarietà protetta, più che altro per nascondere l'interfaccia del rettangolo e definire la nuova interfaccia solo per il quadrato. Ma si può fare in Java (non lo conosco molto)?

In C++ l'ereditarietà multipla fa altamente schifo, è per questo che ho citato Eiffel.


Ce ne saranno altri. Non mancano i problemi, mancano le soluzioni :)
Non ho detto che questo è l'unico problema, ma che la tua soluzione, seppur possibile, è concettualmente sbagliata. La soluzione di non consentire modifiche invece può essere implementata senza errori concettuali.

Con questo non voglio dire che uno non possa scrivere la tua soluzione, ma che non si attiene alla realtà umana e che quindi commette errori concettuali e non si attiene al proposito dell'OOP.

Note:
Natura: che non riguarda l'uomo.
Natura umana/dell'uomo: modo di agire o pensare innato nell'uomo che impone i suoi schemi di ragionamento sul reale.
Rappresentazione: realtà mediata dagli schemi mentali umani.

Loading