I casi speciali con fallback violano il principio di sostituzione di Liskov?


20

Diciamo che ho un'interfaccia FooInterfaceche ha la seguente firma:

interface FooInterface {
    public function doSomething(SomethingInterface something);
}

E una classe concreta ConcreteFooche implementa tale interfaccia:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Vorrei ConcreteFoo::doSomething()fare qualcosa di unico se viene passato un tipo speciale di SomethingInterfaceoggetto (diciamo che si chiama SpecialSomething).

È sicuramente una violazione di LSP se rafforzo i presupposti del metodo o lancio una nuova eccezione, ma sarebbe comunque una violazione di LSP se inserissi SpecialSomethingoggetti con casing speciale fornendo un fallback per SomethingInterfaceoggetti generici ? Qualcosa di simile a:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
        if (something instanceof SpecialSomething) {
            // Do SpecialSomething magic
        }
        else {
            // Do generic SomethingInterface magic
        }
    }

}

Risposte:


19

Esso potrebbe essere una violazione della LSP a seconda di cosa altro è nel contratto per il doSomethingmetodo. Ma è quasi sicuramente un odore di codice anche se non viola LSP.

Ad esempio, se parte del contratto di doSomethingè che chiamerà something.commitUpdates()almeno una volta prima di tornare, e per il caso speciale che chiama commitSpecialUpdates()invece, questa è una violazione di LSP. Anche se SpecialSomethingil commitSpecialUpdates()metodo è stato progettato consapevolmente per fare tutte le stesse cose di commitUpdates(), si tratta solo di hackerare preventivamente la violazione di LSP ed è esattamente il tipo di pirateria informatica che non si dovrebbe fare se si seguisse LSP in modo coerente. Se qualcosa del genere si applica al tuo caso è qualcosa che dovrai capire controllando il tuo contratto per quel metodo (esplicito o implicito).

Il motivo per cui questo è un odore di codice è perché il controllo del tipo concreto di uno dei tuoi argomenti non ha il punto di definire un'interfaccia / tipo astratto per esso in primo luogo e perché in linea di principio non puoi più garantire che il metodo funzioni anche (immagina se qualcuno scrive una sottoclasse di SpecialSomethingcon il presupposto che commitUpdates()verrà chiamato). Prima di tutto, prova a far funzionare questi aggiornamenti speciali all'interno di quelli esistentiSomethingInterface; questo è il miglior risultato possibile. Se sei davvero sicuro di non poterlo fare, devi aggiornare l'interfaccia. Se non controlli l'interfaccia, potresti dover considerare di scrivere la tua interfaccia che fa quello che vuoi. Se non riesci nemmeno a trovare un'interfaccia che funzioni per tutti loro, forse dovresti scartare completamente l'interfaccia e avere più metodi che prendono diversi tipi concreti, o forse è necessario un refactor ancora più grande. Dovremmo sapere di più sulla magia che hai commentato per dire quale di questi è appropriato.


Grazie! Questo aiuta. Ipoteticamente, lo scopo del doSomething()metodo sarebbe la conversione del tipo in SpecialSomething: se lo riceve SpecialSomethingrestituirebbe l'oggetto non modificato, mentre se riceve un SomethingInterfaceoggetto generico eseguirà un algoritmo per convertirlo in un SpecialSomethingoggetto. Poiché le condizioni preliminari e postcondizioni rimangono le stesse, non credo che il contratto sia stato violato.
Evan,

1
@Evan Oh wow ... questo è un caso interessante. Questo potrebbe in realtà essere completamente privo di problemi. L'unica cosa a cui riesco a pensare è che se stai restituendo l'oggetto esistente invece di costruire un nuovo oggetto, forse qualcuno dipende da questo metodo per restituire un oggetto nuovo di zecca ... ma se questo genere di cose può rompere le persone potrebbe dipendere da la lingua. È possibile che qualcuno chiami y = doSomething(x), quindi x.setFoo(3), e poi trovi che y.getFoo()restituisce 3?
Ixrec il

Ciò è problematico a seconda della lingua, anche se dovrebbe essere facilmente risolto semplicemente restituendo una copia SpecialSomethingdell'oggetto. Sebbene per purezza, potrei anche vedere rinunciare all'ottimizzazione del caso speciale quando l'oggetto è passato SpecialSomethinge semplicemente eseguirlo attraverso l'algoritmo di conversione più grande dal momento che dovrebbe ancora funzionare a causa del fatto che è anche SomethingInterfaceoggetto.
Evan,

1

Non è una violazione dell'LSP. Tuttavia, è ancora una violazione della regola "non eseguire controlli di tipo". Sarebbe meglio progettare il codice in modo tale che lo speciale si presenti naturalmente; forse ha SomethingInterfacebisogno di un altro membro che possa farlo, o forse devi iniettare una fabbrica astratta da qualche parte.

Tuttavia, questa non è una regola dura e veloce, quindi devi decidere se ne vale la pena. In questo momento hai un odore di codice e un possibile ostacolo a miglioramenti futuri. Liberarsene potrebbe significare un'architettura considerevolmente più contorta. Senza ulteriori informazioni non posso dire quale sia la migliore.


1

No, approfittando del fatto che un determinato argomento non fornisce solo l'interfaccia A, ma anche A2, non viola l'LSP.

Assicurati solo che il percorso speciale non abbia pre-condizioni più forti (a parte quelle testate nel decidere di prenderlo), né eventuali post-condizioni più deboli.

I modelli C ++ spesso lo fanno per fornire prestazioni migliori, ad esempio richiedendo InputIterators, ma fornendo garanzie aggiuntive se chiamato con RandomAccessIterators.

Se invece devi decidere in fase di esecuzione (ad esempio utilizzando il cast dinamico), fai attenzione alla decisione su quale percorso prendere consumando tutti i tuoi potenziali guadagni o anche di più.

Sfruttare il caso speciale spesso va contro DRY (non ripeterti), poiché potresti dover duplicare il codice e contro KISS (Keep It Simple), poiché è più complesso.


0

C'è un compromesso tra il principio di "non eseguire controlli di tipo" e "segrega le tue interfacce". Se molte classi forniscono un mezzo praticabile ma possibilmente inefficiente per eseguire alcune attività, e alcune di esse possono anche offrire un mezzo migliore, e vi è la necessità di un codice in grado di accettare qualsiasi categoria più ampia di elementi in grado di eseguire l'attività (forse in modo inefficiente) ma quindi eseguire l'attività nel modo più efficiente possibile, sarà necessario che tutti gli oggetti implementino un'interfaccia che includa un membro per dire se il metodo più efficiente è supportato e un altro per usarlo se lo è, oppure fare in modo che il codice che riceve l'oggetto controlli se supporta un'interfaccia estesa e in tal caso lanciarlo.

Personalmente, preferisco il primo approccio, anche se desidero che i framework orientati agli oggetti come .NET consentirebbero alle interfacce di specificare metodi predefiniti (rendendo meno dolorose le interfacce più grandi con cui lavorare). Se l'interfaccia comune include metodi opzionali, una singola classe wrapper può gestire oggetti con molte diverse combinazioni di abilità promettendo per i consumatori solo quelle abilità presenti nell'oggetto avvolto originale. Se molte funzioni sono suddivise in interfacce diverse, sarà necessario un oggetto wrapper diverso per ogni diversa combinazione di interfacce che gli oggetti avvolti potrebbero dover supportare.


0

Il principio di sostituzione di Liskov riguarda i sottotipi che agiscono in conformità con un contratto del loro supertipo. Quindi, come ha scritto Ixrec, non ci sono abbastanza informazioni per rispondere se si tratta di una violazione LSP.

Ciò che viene violato qui è un principio chiuso aperto. Se hai un nuovo requisito - Fai SpecialSomething Magic - e devi modificare il codice esistente, allora stai sicuramente violando OCP .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.