È bello avere un'interfaccia in abbondanza di metodi che appartengono a concetti diversi, solo per preservare il Principio di Liskov?

7

Attualmente sto studiando un corso basato su Software Design e ho avuto una discussione in classe con il mio professore e alcuni compagni di classe su un problema rappresentato dal seguente scenario:

Scenario

Imagine we have a graphic application which lets us plan the interior design of our future house using a perspective from top. We are able to add or remove some elements like furniture and change their position, but obviously, we can't move the walls of the house because it was already built.

Soluzione 1

Per risolvere questo problema, alcuni dei miei compagni di classe hanno proposto una soluzione che potrebbe essere espressa utilizzando il seguente diagramma UML:

Comepuoivedere,hannoconcordatol'usodiun'interfacciacomunechiamata"Disegnabile" che rappresenta gli oggetti grafici visualizzati sull'interfaccia utente. La "App" di classe generale gestisce un elenco di Drawables e ognuno di essi ha un insieme di metodi. Questa interfaccia è implementata da diverse classi come Furniture, Wall o Window. Il fatto è che un oggetto Mobili può essere spostato, ma non i Muri né Windows. Quindi, un mobile implementerebbe il metodo "sposta" definito in Disegnabile. Al contrario, Wall e Window lo scriveranno semplicemente vuoto.

A questo punto, altri compagni di classe e io ci siamo lamentati di questa decisione perché il design non richiede di soddisfare i vincoli (come lo spostamento o meno degli oggetti Drawable, a seconda della loro natura). In questo modo, il design consentirà di spostare alcuni nuovi oggetti, anche se probabilmente non dovrebbero esserlo. Tuttavia, il professore ha detto che questo design è buono perché è flessibile e trasparente per la classe App, perché questa classe non sa che tipo di istanza sta gestendo.

Caso esteso

Inoltre, potremmo pensare a un caso estremo in cui aggiungiamo diversi metodi all'interfaccia Drawable, come "sell", "open" e "lend". Utilizzando lo stesso approccio descritto sopra, avremo un'interfaccia che potrebbe essere in grado di fare qualsiasi cosa, come Superman. Pertanto, ritengo che questa sia una cattiva soluzione di design, poiché stiamo mescolando comportamenti che appartengono a concetti diversi (mobile, vendibile, apribile e così via). Inoltre, stiamo consentendo a un oggetto Wall di implementare il metodo "sell", che non ha assolutamente senso ... ma il mio professore ha continuato a insistere sul suo punto di vista e non ha visto il problema.

Soluzione 2 (in base al caso esteso)

Altre persone suggerivano che potremmo aggiungere quelle interfacce (Movable, Sellable, Openable) e ognuna di esse erediterebbe dall'interfaccia Drawable. In questo modo:

  • Un muro implementerebbe direttamente il disegno
  • Un mobile implementerebbe mobili e vendibili
  • Una finestra implementerebbe solo Openable

Il seguente diagramma riassume questo approccio:

[Modifica:ilmetododispostamentodelDrawabledeveessererimosso,questoèunerroreperchéoraèinseritonell'interfacciaMovable]

Domandeedubbi

Infine,unavoltacapitoilproblema,potrestiaiutarmierispondereaquestedomande,perfavore?

  1. Nonstiamoinfrangendoilprincipiodiresponsabilitàsingolaconquestainterfacciasuper-mega-dio?

  2. EntrambigliapproccisonovalidirispettoalprincipiodisostituzionediLiskov?Pensochesiarottonelprimocasoperchélepost-condizionisonointerrotteacausadialcunimetodinonfannonulla.Adognimodo,nelsecondoapprocciononsonosicuroancheperviadell'alberodiereditarietàdelleinterfacce

  3. Forse,un'altraalternativapotrebbeessereladefinizionedioggettiDrawabledibase(conimetodi"click" e "draw"). Quindi, possiamo sfruttare un qualche tipo di meccanismo come il pattern Decorator per aggiungere dinamicamente comportamenti. Ma come?

  4. Se usiamo una lingua senza interfacce come JavaScript o Python, come possiamo affrontare questo problema?

posta albertoblaz 24.05.2013 - 00:31
fonte

2 risposte

11

Aren't we breaking the Single Responsibility Principle with this super-mega-god-interface?

Non esattamente, perché l'interfaccia non sta facendo nulla. Non ha responsabilità.

Stai violando Principio di sostituzione di Liskov e Interfaccia principio di segregazione però.

LSP perché stai scrivendo metodi che non fanno nulla, solo per adattarlo all'interfaccia (ad esempio, ciò che è vero per IDrawable - che puoi spostarlo - non è vero per Wall).

ISP perché avrai situazioni come un metodo commerciale, che dovrebbe avere accesso ai metodi Compra e Vendi, ma avrà anche accesso a Draw.

Are both approaches valid against the Liskov's Substitution Principle? I think it's broken in the first case because the post-conditions are broken due to some methods don't do anything. Anyway, in the second approach I'm not sure as well because of the interfaces inheritance tree

È altrettanto rotto nel secondo caso come il primo.

Modifica: ripensandoci, non lo è. Ma è ancora rotto. Non sembra automaticamente vero che tutto ciò che potrebbe avere un metodo Move avrà anche un metodo Draw. Potrebbe essere vero nel tuo caso ma, se in seguito ti trovi a voler passare un oggetto con un metodo Move in un metodo che riceve un oggetto mobile, dovrai implementare Draw su quell'oggetto, anche se non lo fai ne ho bisogno.

E non ti guadagna nulla su una terza soluzione ...

Maybe, another alternative could be the definition of basic Drawable objects (with 'click' and 'draw' methods). Thus, we may take advantage of some kind of mechanism like the Decorator pattern in order to add behaviors dynamically. But how?

L'alternativa migliore è quella di separare completamente le tue interfacce. Puoi comunque avere oggetti con molti metodi (ma Principio di dipendenza-Inversione e Principio di responsabilità singola impone di spingere la logica verso il basso nei servizi), ma devono solo implementare le interfacce di cui hanno bisogno. Wall non dovrebbe implementare IMovable, se non può essere spostato.

public class Wall : IDrawable
public class Furniture : IDrawable, IMovable, ISellable
public class Window : IDrawable, IOpenable

Qui nulla impone che una di queste interfacce debba derivare da un'altra.

public interface IDrawable
{
    void Draw(Canvas canvas);
}

public interface IMovable
{
    void MoveTo(int x, int y);
}

etc

Dovresti quindi gestire il tuo elenco di IDrawables, che puoi solo essere sicuro di implementare Draw () e eseguire il cast per vedere se sono disponibili altri metodi.

Ad esempio,

ITradable tradable = drawable as ITradable;
if (tradable != null)
{
    tradable.Trade(buyer, seller);
}

Dovrai comunque fare quanto sopra nella tua opzione 2, quindi perché legare tutto a IDrawable?

If we use a language without interfaces such as JavaScript or Python, how can we deal with this problem?

Le lingue OO tendono a supportare almeno una delle tre cose:

Linguaggi prototipi , come Javascript, supporto ... beh, prototipi, che consentono una modifica leggermente modificata versione di digitazione anatra (nei prototipi, generalmente si chiede se esiste un metodo prima di chiamarlo).

Altri paradigmi generalmente gestiscono questi problemi a modo loro.

    
risposta data 24.05.2013 - 01:30
fonte
1
  1. La Soluzione 1 viola non solo il Principio di Sostituzione di Liskov, ma viola anche il Principio di Segregazione dell'Interfaccia e il Principio di Responsabilità Unico. La soluzione 2 è l'approccio corretto (vedi correzione in basso) .

  2. Come sopra

  3. Puoi farlo e la soluzione 2 allo stesso tempo, non sono esclusivi.

  4. Dovresti utilizzare soluzioni e schemi non OO.

CORREZIONE: Anche la soluzione 2 è sbagliata. Sellable and Movable non devono ereditare da Drawable. Dovrebbero essere un'interfaccia separata e indipendente.

    
risposta data 24.05.2013 - 01:18
fonte