Si sta verificando un errore in circostanze specifiche sottoclasse imprevedibili una violazione di LSP?

7

Diciamo che volevo creare un Java List<String> (vedi specifica ) implementazione che utilizza un sottosistema complesso, ad esempio un database o un file system, per il proprio archivio in modo che funzioni come una raccolta permanente anziché in memoria.

Quindi ecco un'implementazione di scheletro:

class DbBackedList implements List<String> {
  private DbBackedList() {}
  /** Returns a list, possibly non-empty */
  public static getList() { return new DbBackedList(); }
  public String get(int index) {
    return Db.getTable().getRow(i).asString();  // may throw DbExceptions!
  }
  // add(String), add(int, String), etc. ...
}

Il mio problema sta nel fatto che l'API DB sottostante potrebbe incontrare errori di connessione che non sono specificati in Interfaccia elenco che dovrebbe generare.

Il mio problema è se questo viola il Principio di sostituzione di Liskov (LSP).

Nel suo articolo su LSP , Bob Martin fornisce in realtà un esempio di PersistentSet che viola LSP. La differenza è che il suo Exception appena specificato è determinato dal valore inserito e quindi sta rafforzando la precondizione. Nel mio caso l'errore di connessione / lettura è imprevedibile e dovuto a fattori esterni e quindi non è tecnicamente una nuova precondizione, semplicemente un errore di circostanza, forse come OutOfMemoryError che può verificarsi anche se non specificato.

In circostanze normali, il nuovo errore / eccezione potrebbe non essere mai lanciato. (Il chiamante potrebbe intercettare se è a conoscenza della possibilità, proprio come un programma Java limitato alla memoria potrebbe catturare in modo specifico OOME.)

Questo è quindi un argomento valido per lanciare un errore extra e posso comunque affermare di essere un java.util.List valido (o scegliere il tuo SDK / lingua / collezione in generale) e non in violazione di LSP?

Modifica: questo argomento potrebbe essere più accettabile se consideri un FileBackedList (più affidabile "connessione") piuttosto che un DbBackedList .

Se questo viola effettivamente LSP e quindi non è praticamente utilizzabile, ho fornito due soluzioni alternative meno appetibili come risposte che puoi commentare, vedi sotto.

Nota: casi d'uso

Nel caso più semplice, l'obiettivo è fornire un'interfaccia familiare per i casi in cui (diciamo) un database viene usato come un elenco persistente e consentire operazioni List regolari come ricerca, sottolista e iterazione.

Un altro caso d'uso più avventuroso è una sostituzione di slot per le librerie che funzionano con gli elenchi di base, ad esempio se abbiamo una coda di attività di terze parti che di solito funziona con un elenco semplice:
new TaskWorkQueue(new ArrayList<String>()).start() che è suscettibile di perdere tutta la coda in caso di arresto anomalo, se sostituiamo semplicemente questo con:
new TaskWorkQueue(new DbBackedList()).start() otteniamo una persistenza istantanea e la possibilità di condividere le attività tra più di una macchina.

In entrambi i casi, potremmo gestire le eccezioni di connessione / lettura lanciate, magari riprovare la connessione / leggere prima o consentire loro di lanciare e arrestare il programma (ad es. se non possiamo modificare il codice TaskWorkQueue).

    
posta Motti Strom 10.11.2013 - 16:59
fonte

4 risposte

6

Il motivo per cui il suo esempio è una violazione di LSP non è a causa dell'eccezione di per sé, è il motivo dell'eccezione - sta cambiando il contratto.

Un esempio più semplice, ma più forzato: hai un elenco di valori, decidi di volerlo utilizzare per un elenco di voti elementari completati, con un controllo per assicurarti che i numeri rientrino nell'intervallo richiesto, quindi si sottoclassi ListInt come ListIntElementary. ListIntElementary violare LSP.

D'altra parte, hai dei vincoli del mondo reale che non si applicano alla classe base, ma la sottoclasse può essere usata algoritmicamente ovunque la classe base può, accetta tutti gli input accettabili, restituisce solo valori accettabili. Le eccezioni non sono né input né output nel senso dell'LSP.

Una nuova eccezione o nuova causa per una vecchia eccezione (se puoi trovarne una che mappa in modo pulito) è un dettaglio di implementazione, non una violazione di LSP. Può significare che in pratica non è adatto come sostituto della classe base, ma in teoria una volta creato, può essere usato ovunque che la classe base possa essere utilizzata.

In breve, va bene.

    
risposta data 10.11.2013 - 18:31
fonte
2

Per iniziare, DBException non si qualifica come Errore :

subclasses of Error... are abnormal conditions that should never occur.

Nota anche che se ti aspetti che DBException venga gettato in get sovrascritto, deve essere deselezionato. In caso contrario, il codice non verrà compilato, in base a JLS 8.4 .8.3. Requisiti in Override e Nascondere :

A method that overrides or hides another method, including methods that implement abstract methods defined in interfaces, may not be declared to throw more checked exceptions than the overridden or hidden method...

Con sopra detto, sembra sì, violerebbe LSP - perché come utente di Java Collections Framework , non mi aspetto che List.get lanci eccezioni di runtime per qualsiasi tipo problema diverso da "errori di programmazione", cioè per qualcosa che è possibile evitare cambiando il codice (nota, non importa come si modifica il codice, la connessione al database non sarà garantita).

Se guardi il IndexOutOfBoundsException specificato per List.get, si legge come uno che il programmatore può evitare con il controllo dei limiti preliminari:

if the index is out of range (index < 0 || index >= size())

Un'altra eccezione di runtime documentata per le raccolte, incluso Elenco, è ConcurrentModificationException e per i documenti API, ci si aspetta anche che venga risolto correggendo il codice che lo ha causato:

ConcurrentModificationException should be used only to detect bugs.

Una spiegazione più dettagliata di ciò che mi aspetto è fornita in Domande frequenti sulla progettazione JCF . Affronta UnsupportedOperationException , ma se dai un'occhiata agli esempi precedenti di IOOBE e CME, il ragionamento si adatta anche a questi:

Won't programmers have to surround any code that calls optional operations with a try-catch clause in case they throw an UnsupportedOperationException?

It was never our intention that programs should catch these exceptions: that's why they're unchecked (runtime) exceptions. They should only arise as a result of programming errors, in which case, your program will halt due to the uncaught exception.

Riassumendo, se volessi esporre i dati del database supportato come List (o qualsiasi Collection per quella materia) in un modo che sarebbe meno confuso per gli utenti della mia API, vorrei probabilmente racchiudere quei dati in qualche oggetto helper che esporrebbe le eccezioni relative al DB solo quando usate "al di fuori" del contesto di raccolta - cioè, quando il codice client tenterebbe di accedere ai dati che si prevede vengano memorizzati "all'interno" del wrapper, non quando le raccolte di wrapper vengono trasferite sul codice client.

Per un esempio concreto di come ciò potrebbe essere fatto, dai un'occhiata a java.util.concurrent.Future che racchiude i risultati di un calcolo asincrono in un modo che non espone eccezioni "interne", avvolte quando trasferite nelle raccolte.

    
risposta data 11.11.2013 - 13:10
fonte
2

L'LSP in pratica dice la risposta a "Il codice che utilizza correttamente questa interfaccia fa la cosa sbagliata se usato con la sua implementazione?" dovrebbe essere no Lanciare nuove e diverse eccezioni potrebbe causare il codice che pensava che stesse gestendo tutte le eccezioni per fallire quando si sono verificate eccezioni nuove e impreviste. Se è possibile mappare le eccezioni a quelle già fornite dall'interfaccia, allora questo è un possibile percorso verso il basso, ma devono mappare in modo pulito nell'intento che stanno cercando di comunicare. Non vuoi mappare a un errore fatale da un errore non fatale e viceversa.

    
risposta data 11.11.2013 - 15:45
fonte
0

Se LSP è incontestabilmente violato, potrei invece introdurre una classe Tipo di opzione che la lista nominalmente memorizza e restituisce, che incapsula i tipi di ritorno e gli eventuali errori nel recupero.

/**
 * Wraps a String for writing or reading from a database.
 */
interface DbString {
  public DbString(String str) { /* ... */ }
  /** @returns the String, or "" if hasError() returns true */
  public String() getString();
  /** @returns true if there was an error in retrieving the string */
  public boolean hasError();
}

/**
 * A list of DbStrings
 */
class DbBackedList implements List<DbString> {
  // .. as before

  public DbString get(int index) {
    return Db.getConnection.getTable().getRow(i).asString();  // may throw!
  }

  // add(DbString), add(int, DbString), etc. ...
}

L'utilizzo è simile agli elenchi regolari.

List<String> l = new DbBackedList();
l.add(new DbString("foo"));
assertFalse(a.get(0).hasError());
assertEquals("foo", a.get(0).getString());

A questo punto, tuttavia, è probabilmente più facile creare una nuova interfaccia da zero e non modificarne una esistente, tuttavia perdiamo parte dell'apparente familiarità con l'interfaccia Elenco. Se vogliamo fornire altre raccolte, come Set o Map, dovremo creare nuove interfacce per ciascuna di esse.

    
risposta data 10.11.2013 - 16:59
fonte