Prevenire la violazione di Liskov Sostituzione

4

Sto reimplementando alcuni componenti e ho notato che la versione originale ha una violazione di Liskov Substitution. Non è poi così importante, anche se mi piacerebbe sbarazzarmene nella nuova implementazione. Tuttavia, non mi è chiaro come farlo.

Ho un componente che definisce classi di valori molto semplici usate dal sistema. Questo componente ha un'interfaccia DataValue e una dozzina di implementazioni come NumberValue e GeoCoordinateValue.

Il componente che sto rielaborando è un livello di archiviazione pensato per l'indicizzazione dei dati, quindi può essere facilmente interrogato. Questo componente contiene un insieme di classi che forniscono informazioni di archiviazione per una particolare classe di implementazione DataValue. Queste informazioni sono cose come i campi in cui una tabella deve contenere il DataValue in questione, quale campo deve essere usato per l'ordinamento e quali indici devono essere posizionati. Queste classi implementano un'interfaccia DataValueHandler.

La violazione LSP si verifica per due metodi particolari in questa interfaccia DataValueHandler:

getWhereConditions( DataValue $dataValue )

getInsertValues( DataValue $dataValue )

L'interfaccia definisce questi metodi come valore di DataValue. Le implementazioni prevedono tuttavia il DataValue per il quale forniscono informazioni. Ad esempio, NumberValueHandler si aspetta un valore Number e genererà un'eccezione se ottiene un valore GeoCoordinateValue.

Quindi, come posso liberarmi di questo? (Prima che qualcuno lo suggerisca: non accadrà alcuna informazione su qualche specifico back-end di storage in DataValue, perché questo sarebbe peggio per il design, quindi l'attuale violazione di LSP.)

    
posta Jeroen De Dauw 27.02.2013 - 14:50
fonte

3 risposte

4

La classe DataValue specifica deve essere un parametro per il costruttore DataValueHandler e memorizzata come variabile membro e i metodi get non devono includere un oggetto DataValue come parametro.

(Alcuni pseudocodi java-ish qui sotto per illustrare)

class NumberDataValueHandler implements DataValueHandler {
    private NumberDataValue myDataValue;

    NumberDataValueHandler (NumberDataValue newNumberDataValue) {
       myDataValue = newNumberDataValue;
    }

    String getWhereConditions () {
        myDataValue.someNumberSpecificMethod();
        // do some NumberDataValue specific operations...
    }

}
    
risposta data 27.02.2013 - 15:26
fonte
2

Ci scusiamo per la risposta molto tarda, ma questo è uno scenario valido per PHP fino ad oggi (v5.6 / 7.1), e confonde molti programmatori. Ho svolto molte ricerche sul tema dell'SPL in relazione agli obblighi contrattuali e alle violazioni, poiché sto sviluppando un quadro pienamente conforme a SOLID e posso dire che ci sono molte opinioni diverse in merito.

Dato che PHP non ha attualmente DbC ( Design by Contract ), e nella migliore delle ipotesi ha un tipo di suggerimento invariante per il metodo parametri e tipi di rendimento, dobbiamo considerare il sentimento di fondo del Principio di sostituzione di Liskov. Sebbene sia possibile applicare parametri e tipi di ritorno in entrambe le interfacce e funzioni astratte, questo è solo uno strumento per garantire che Liskov non sia violato, ma non copre DbC. Come afferma Robert Martin nel suo articolo Il principio di sostituzione di Liskov (grassetto / corsivo aggiunto da me):

The LSP makes clear that in OOD the ISA relationship pertains to behavior. Not intrinsic private behavior, but extrinsic public behavior; behavior that clients depend upon

In DbC, la validità di un DataType in relazione a una classe di consumo è qualcosa che è concettualmente identico su tutto il cemento DataTypeHandlers - se passi un DataType a un DataTypeHandler , è valido o non valido. Lo rivisiteremo più tardi.

Ora, per la mente umana sembra logico implementare quella regola come parametro digitato nel metodo, in questo modo:

public function getWhereConditions(DataValue $dataValue) {...}

// Override in a child class...
public function getWhereConditions(NumberDataValue $dataValue) {...}

Ma questo viola il LSP in quanto implica una covarianza dei parametri del metodo (cioè NumberDataValue è un più raffinato tipo di DataValue) e modifica il comportamento pubblico estrinseco.

Se, anziché applicare questa regola come tipo di parametro del metodo (pubblico), la estrapoliamo invece a un controllo di validità (privato) rispetto all'argomento passato, quindi spostiamo il comportamento da "pubblico" a "privato" . Come affermato da Robert Martin, questo è non una violazione di LSP. (Leggi le ultime pagine alla fine del suo articolo per vedere un esempio usando PersistentSet che non hanno una relazione ISA con Sets, che esegue il backup di questo reclamo).

Ecco un esempio in PHP che dimostra cosa intendo. Si noti che i metodi astratti hanno parametri invarianti (cioè DataType), ma consentono la variazione del comportamento privato. Inoltre, tieni presente che il metodo getWhereConditions() è definitivo e ereditato da tutte le implementazioni concrete, assicurando che il comportamento pubblico rimanga coerente (lancia un DataTypeException o restituisca un WhereConditionResultInterface ):

abstract class DataHandler()
{
    public function getWhereConditions(DataValue $dataValue)
    {
        if(!$this->isDataValid($dataValid) {
            throw new DataTypeException('Invalid data given');
        }
        $this->processGetWhereConditions($dataValue);
    }

    abstract protected function isDataValid(DataValue $dataValue);
    /** @return WhereConditionResultInterface */
    abstract protected function processGetWhereConditions(DataValue $dataValue);
}

final class NumberDataHandler extends DataHandler
{
    protected function isDataValid(DataValue $dataValue)
    {
        return is_a($dataValue, NumberDataValueInterface::class);
    }

    protected function processGetWhereConditions(DataValue $dataValue)
    {
        /** @var NumberDataValueInterface $dataValue - Type hinting for modern IDEs */
        // Do stuff specific to NumberDataValueInterface here
    }
}

final class GeoCoordinateDataHandler extends DataHandler
{
    protected function isDataValid(DataValue $dataValue)
    {
        return is_a($dataValue, GeoCoordinateDataValueInterface::class);
    }

    protected function processGetWhereConditions(DataValue $dataValue)
    {
        /** @var GeoCoordinateDataValueInterface $dataValue - Type hinting for modern IDEs */
        // Do stuff specific to GeoCoordinateDataValueInterface here
    }
}
    
risposta data 16.08.2017 - 03:27
fonte
0

Le classi DataValue implementano tutti IDataValue o ereditano dalla stessa classe astratta DataValue?

Mi aspetto che tutte le classi abbiano un insieme comune di metodi pubblici e che le Condizioni GetWhere agiscano solo su questi metodi comuni (facilitando così la sostituzione).

Altrimenti sono solo classi diverse, nel qual caso ti aspetteresti di vedere ...

GetWhereConditions(NumberValue){...}
GetWhereConditions(GeoCoordinateValue){...}

... per riflettere questo.

    
risposta data 27.02.2013 - 15:14
fonte