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

20

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

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

E una classe concreta ConcreteFoo che implementa tale interfaccia:

class ConcreteFoo implements FooInterface {

    public function doSomething(SomethingInterface something) {
    }

}

Mi piacerebbe ConcreteFoo::doSomething() di fare qualcosa di unico se ha passato un tipo speciale di oggetto SomethingInterface (diciamo che è chiamato SpecialSomething ).

È sicuramente una violazione LSP se rafforzo le precondizioni del metodo o lanci una nuova eccezione, ma sarebbe comunque una violazione LSP se ho oggetti SpecialSomething con casing speciale mentre fornisco un fallback per oggetti generici SomethingInterface ? Qualcosa come:

class ConcreteFoo implements FooInterface {

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

}
    
posta Evan 30.12.2015 - 11:11
fonte

5 risposte

19

È potrebbe essere una violazione di LSP a seconda di che cosa c'è nel contratto per il metodo doSomething . Ma è quasi certamente 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 chiama invece commitSpecialUpdates() , quindi è una violazione di LSP. Anche se il metodo SpecialSomething di commitSpecialUpdates() è stato progettato in modo consapevole per eseguire tutte le stesse cose di commitUpdates() , si tratta solo di hackerare preventivamente la violazione LSP, ed è esattamente il tipo di hacker che non si dovrebbe fare se uno ha seguito costantemente LSP. Se qualcosa del genere si applica al tuo caso è qualcosa che dovrai capire controllando il tuo contratto per quel metodo (sia esplicito che implicito).

Il motivo per cui questo è un odore del codice è perché il controllo del tipo concreto di uno dei tuoi argomenti non ha il significato di definire un'interfaccia / tipo astratto per il primo, e perché in linea di principio non puoi più garantire il metodo funziona (immagina se qualcuno scrive una sottoclasse di SpecialSomething con l'ipotesi che venga chiamata commitUpdates() ). Prima di tutto, prova a far funzionare questi aggiornamenti speciali all'interno del SomethingInterface esistente; questo è il miglior risultato possibile. Se sei davvero sicuro di non poterlo fare, devi aggiornare l'interfaccia. Se non si controlla l'interfaccia, potrebbe essere necessario prendere in considerazione la scrittura della propria interfaccia che fa ciò che si desidera. Se non riesci nemmeno a trovare un'interfaccia che funzioni per tutti, forse dovresti scartare completamente l'interfaccia e avere più metodi che prendono diversi tipi di calcestruzzo, o forse un refactor ancora più grande è in ordine. Dovremmo sapere di più sulla magia che hai commentato per sapere quale di questi è appropriato.

    
risposta data 30.12.2015 - 12:14
fonte
1

Non è una violazione dell'LSP. Tuttavia, è ancora una violazione della regola "non fare controlli di tipo". Sarebbe meglio progettare il codice in modo tale che lo speciale sorga naturalmente; forse SomethingInterface ha bisogno di un altro membro che potrebbe realizzare questo, o forse è necessario iniettare una fabbrica astratta da qualche parte.

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

    
risposta data 30.12.2015 - 11:41
fonte
1

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

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

I modelli C ++ spesso lo fanno per fornire prestazioni migliori, ad esempio richiedendo InputIterator s, ma dando garanzie aggiuntive se chiamate con RandomAccessIterator s.

Se invece devi decidere in fase di esecuzione (ad esempio utilizzando la trasmissione dinamica), 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 ripetersi), poiché potresti dover duplicare il codice e contro KISS (Keep it Simple), poiché è più complesso.

    
risposta data 30.12.2015 - 17:25
fonte
0

Esiste un compromesso tra il principio di "non fare controlli di tipo" e "segregare le tue interfacce". Se molte classi forniscono un mezzo fattibile ma possibilmente inefficiente per eseguire alcune attività, e alcune di esse possono anche offrire un mezzo migliore, e vi è la necessità di un codice che possa accettare qualsiasi categoria più ampia di elementi in grado di svolgere l'attività (forse inefficiente) ma poi 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 avere il codice che riceve l'oggetto controlla se supporta un'interfaccia estesa e, in caso affermativo, esegui il cast.

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

    
risposta data 30.12.2015 - 17:34
fonte
0

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

Ciò che è violato qui è però un principio di Open closed. Se hai un nuovo requisito - Do SpecialSomething magic - e devi modificare il codice esistente, allora sei sicuramente violare l'OCP .

    
risposta data 16.12.2017 - 19:24
fonte

Leggi altre domande sui tag