¿Se produce un error en el LSP?

7

Digamos que quería crear un Java List<String> (consulte spec ) implementación que utiliza un subsistema complejo, como una base de datos o un sistema de archivos, para su almacenamiento, de modo que actúa como una colección persistente en lugar de una en memoria.

Aquí hay una implementación esquelética:

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. ...
}

Mi problema radica en el hecho de que la API de base de datos subyacente puede encontrar errores de conexión que no se especifican en Interfaz de lista que debe lanzar.

Mi problema es si esto viola el principio de sustitución de Liskov (LSP).

En su artículo sobre LSP , Bob Martin en realidad da un ejemplo de un conjunto persistente que viola LSP. La diferencia es que su Exception especificado recientemente está determinado por el valor insertado y, por lo tanto, está fortaleciendo la condición previa. En mi caso, el error de conexión / lectura es impredecible y debido a factores externos, por lo que no es técnicamente una condición previa nueva, simplemente un error de circunstancia, tal como OutOfMemoryError, que puede ocurrir incluso cuando no se especifica.

En circunstancias normales, la nueva Error / Excepción nunca puede ser lanzada. (La persona que llama podría detectar si está al tanto de la posibilidad, al igual que un programa Java con memoria restringida puede detectar específicamente OOME).

Por lo tanto, ¿este es un argumento válido para lanzar un error adicional y todavía puedo afirmar que es un java.util.List válido (o elegir su SDK / idioma / colección en general) y no violar el LSP?

Editar: Este argumento podría ser más aceptable si consideras un FileBackedList ("conexión" más confiable) en lugar de un DbBackedList .

Si esto realmente viola el LSP y, por lo tanto, no es prácticamente utilizable, proporcioné dos soluciones alternativas menos agradables como respuestas que puede comentar, consulte a continuación.

Nota al pie: casos de uso

En el caso más simple, el objetivo es proporcionar una interfaz familiar para los casos en que (digamos) una base de datos solo se está utilizando como una lista persistente, y permitir operaciones de lista regulares como búsqueda, lista secundaria e iteración.

Otro caso de uso más aventurero es el reemplazo de las bibliotecas que funcionan con listas básicas, por ejemplo, si tenemos una cola de tareas de terceros que generalmente funciona con una lista simple:
new TaskWorkQueue(new ArrayList<String>()).start() que es susceptible de perder toda su cola en caso de una falla, si simplemente reemplazamos esto con:
new TaskWorkQueue(new DbBackedList()).start() obtenemos una persistencia instantánea y la capacidad de compartir las tareas entre más de una máquina.

En cualquiera de los dos casos, podríamos manejar las excepciones de conexión / lectura que se generan, quizás volvamos a intentar la conexión / lectura primero, o permitirles lanzar y bloquear el programa (por ejemplo, si no podemos cambiar el código de TaskWorkQueue).

    
pregunta Motti Strom 10.11.2013 - 16:59

4 respuestas

6

La razón por la que su ejemplo es una violación de LSP no se debe a la excepción en sí misma, es la razón de la excepción: está cambiando el contrato.

Un ejemplo más simple, pero más ingenioso: tiene una Lista de entradas, decide utilizarla para obtener una lista de las calificaciones elementales completadas, con una verificación para asegurarse de que los números estén dentro del rango requerido, por lo que usted subclase ListInt como ListIntElementary. ListIntElementary viola el LSP.

Por otro lado, usted tiene restricciones del mundo real que no se aplicarían a la clase base, pero su subclase puede usarse algorítmicamente en cualquier lugar que la clase base pueda, acepta todas las entradas aceptables y solo devuelve valores aceptables. Las excepciones no son entrada ni salida en el sentido de LSP.

Una nueva excepción o una nueva causa para una antigua excepción (si puede encontrar una que se asigna de manera clara) es un detalle de la implementación, no una violación de LSP. Puede significar que, en la práctica, no es adecuado como un reemplazo para la clase base, pero en teoría, una vez creado, puede usarse en todas partes en que se puede usar la clase base.

En resumen, esto está bien.

    
respondido por el jmoreno 10.11.2013 - 18:31
2

Para comenzar, DBException difícilmente califica como Error :

  

las subclases de error ... son condiciones anormales que nunca deberían ocurrir.

Tenga en cuenta también que si espera que DBException se lance en get invalidado, debe estar desactivado. De lo contrario, su código no se compilará, según JLS 8.4 .8.3. Requisitos para anular y ocultar :

  

Un método que invalida u oculte otro método, incluidos los métodos que implementan métodos abstractos definidos en interfaces, no se puede declarar que arroje más excepciones marcadas que el método anulado u oculto ...

Dicho lo anterior, parece que sí, violaría LSP , porque como usuario de Framework de Colecciones de Java , no esperaría que List.get lance una excepción de tiempo de ejecución de ningún tipo Un problema que no sea "errores de programación", es por algo que se puede evitar cambiando el código (tenga en cuenta que no importa cómo cambie el código, la conexión a la base de datos no estará garantizada).

Si observa el IndexOutOfBoundsException especificado para List.get, se lee como uno que el programador puede evitar mediante la verificación de los límites preliminares:

  

si el índice está fuera de rango (índice < 0 || índice > = tamaño ())

Otra excepción de tiempo de ejecución documentada para Colecciones, incluida la Lista, es ConcurrentModificationException y según los documentos API, también se espera que se solucione corrigiendo el código que lo causó:

  

ConcurrentModificationException debe usarse solo para detectar errores.

Se proporciona una explicación más detallada de lo que espero en Preguntas frecuentes sobre el diseño de JCF . Aborda UnsupportedOperationException , pero si observa ejemplos anteriores de IOOBE y CME, el razonamiento también se ajusta a estos:

  

¿No tendrán que rodear los programadores ningún código que llame a operaciones opcionales con una cláusula try-catch en caso de que lancen una excepción UnsupportedOperationException?

     

Nunca fue nuestra intención que los programas detecten estas excepciones: por eso son excepciones sin control (tiempo de ejecución). Solo deben surgir como resultado de errores de programación, en cuyo caso, su programa se detendrá debido a la excepción no detectada.

Resumiendo, si quisiera exponer los datos respaldados por la base de datos como Lista (o cualquier Recopilación para esa materia) de una manera que sería menos confuso para los usuarios de mi API, probablemente envolvería esos datos en algún objeto auxiliar que expondría las excepciones relacionadas con la base de datos solo cuando se use "fuera" del contexto de recopilación, es decir, cuando el código del cliente intentaría acceder a los datos que se espera que se almacenen "dentro" del contenedor, no cuando las colecciones de envoltorios se llevan sobre el código del cliente.

Para ver un ejemplo concreto de cómo se podría hacer esto, eche un vistazo a java.util.concurrent.Future que envuelve los resultados de un cálculo asíncrono de una manera que no expone las excepciones envueltas "internas" cuando se transfieren en colecciones.

    
respondido por el gnat 11.11.2013 - 13:10
2

El LSP básicamente dice la respuesta a "¿El código que usa correctamente esta interfaz hará lo incorrecto si se usa con su implementación?" debe ser no Lanzar excepciones nuevas y diferentes puede hacer que el código que pensaba que estaba manejando todas las excepciones fallara cuando aparecen excepciones nuevas e inesperadas. Si es posible asignar excepciones a las ya proporcionadas por la interfaz, entonces ese es un camino posible para ir hacia abajo, pero tienen que asignarse de manera clara en la intención que intentan comunicar. No desea asignar un error fatal de un error no fatal y viceversa.

    
respondido por el stonemetal 11.11.2013 - 15:45
0

Si se está violando el LSP, sin duda, podría introducir una clase de Option Type que la Lista almacena y devuelve nominalmente. que encapsula los tipos de retorno y cualquier error en la recuperación.

/**
 * 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. ...
}

El uso es similar a las listas normales.

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

En este punto, sin embargo, es posiblemente más fácil crear una nueva interfaz desde cero y no adaptar una existente, sin embargo, perdemos parte de la familiaridad aparente con la interfaz de la Lista. Si queremos proporcionar otras colecciones, como Establecer o Mapa, tendremos que crear nuevas interfaces para cada una de ellas.

    
respondido por el Motti Strom 10.11.2013 - 16:59

Lea otras preguntas en las etiquetas