Pregunta ¿Los métodos Collections.unmodifiableXXX violan LSP?


Principio de sustitución Liskov es uno de los principios de SÓLIDO. He leído este principio varias veces y he intentado comprenderlo.

Esto es lo que hago de eso,

Este principio está relacionado con un fuerte contrato de comportamiento entre los   jerarquía de clases. Los subtipos deberían poder ser reemplazados por   supertipo sin violar el contrato.

He leído algunos otros artículos también y estoy un poco perdido pensando en esta pregunta. Hacer Collections.unmodifiableXXX() métodos no violan LSP?

Un extracto del artículo vinculado anteriormente:

En otras palabras, cuando se usa un objeto a través de su interfaz de clase base,   el usuario solo conoce las condiciones previas y las postcondiciones de la base   clase. Así, los objetos derivados no deben esperar que dichos usuarios obedezcan   precondiciones que son más fuertes que las requeridas por la clase base

¿Por qué creo eso?

antes de

class SomeClass{
      public List<Integer> list(){
           return new ArrayList<Integer>(); //this is dumb but works
      }
}

Después

class SomeClass{
     public List<Integer> list(){
           return Collections.unmodifiableList(new ArrayList<Integer>()); //change in implementation
     }
}

No puedo cambiar la implementación de SomeClass para devolver la lista no modificable en el futuro. La compilación funcionará, pero si el cliente de alguna manera trató de alterar el List devuelto entonces fallaría en tiempo de ejecución.

Es por eso que la guayaba ha creado por separado ImmutableXXX interfaces para colecciones?

¿No es esto una violación directa de LSP o me he equivocado por completo?


32
2018-02-26 19:06


origen


Respuestas:


LSP dice que cada subclase debe obedecer los mismos contratos que la superclase. Ya sea que este sea el caso o no Collections.unmodifiableXXX() por lo tanto, depende de cómo se lea este contrato.

Los objetos devueltos por Collections.unmodifiableXXX() lanza una excepción si intentas llamar a cualquier método de modificación. Por ejemplo, si add() se llama, un UnsupportedOperationException será arrojado.

¿Cuál es el contrato general de add()? De acuerdo con la Documentación de API es:

Asegura que esta colección contenga el elemento especificado (opcional   operación). Devuelve verdadero si esta colección cambió como resultado de la   llamada. (Devuelve falso si esta colección no permite duplicados y   ya contiene el elemento especificado.)

Si este fuera el contrato completo, entonces la variante no modificable no se podría usar en todos los lugares donde se puede usar una colección. Sin embargo, la especificación continúa y también dice que:

Si una colección se niega a agregar un elemento en particular por alguna razón   Aparte de que ya contiene el elemento, debe arrojar un   excepción (en lugar de devolver falso). Esto preserva la invariante   que una colección siempre contiene el elemento especificado después de esto   devolución de llamadas.

Esto permite explícitamente que una implementación tenga código que no agregue el argumento de add a la colección, pero da como resultado una excepción. Por supuesto, esto incluye la obligación para el cliente de la colección de que tenga en cuenta esa posibilidad (legal).

Por lo tanto, el subtipo de comportamiento (o el LSP) aún se cumple. Pero esto muestra que si uno planea tener diferentes comportamientos en subclases que también deben estar previstos en la especificación de la clase toplevel.

Buena pregunta por cierto.


30
2018-02-26 19:20



Sí, creo que lo tienes correcto. Básicamente, para cumplir con el LSP, debe poder hacer cualquier cosa con un subtipo que pueda hacer con el supertipo. Esta es también la razón por la cual el problema de Elipse / Círculo surge con el LSP. Si una Elipse tiene un setEccentricity método, y un círculo es una subclase de Ellipse, y se supone que los objetos son mutables, no hay forma de que Circle pueda implementar el setEccentricity método. Por lo tanto, hay algo que puedes hacer con una Elipse que no puedes hacer con un Círculo, por lo que se viola el LSP. † De manera similar, hay algo que puedes hacer con un List que no se puede hacer con uno envuelto por Collections.unmodifiableList, entonces eso es una violación LSP.

El problema es que aquí hay algo que queremos (una lista inmutable, no modificable, de solo lectura) que no sea capturada por el sistema de tipos. En C # podrías usar IEnumerable que captura la idea de una secuencia que puede iterar y leer desde, pero no escribir en. Pero en Java solo hay List, que a menudo se utiliza para una lista mutable, pero que a veces nos gustaría usar para una lista inmutable.

Ahora, algunos podrían decir que Circle puede implementar setEccentricity y simplemente lanza una excepción, y de manera similar una lista no modificable (o una inmutable de Guava) arroja una excepción cuando intentas modificarla. Pero eso realmente no significa que es un Lista desde el punto de vista del LSP. En primer lugar, al menos viola el principio de menor sorpresa. Si la persona que llama obtiene una excepción inesperada al intentar agregar un elemento a una lista, eso es bastante sorprendente. Y si el código de llamada necesita tomar pasos para distinguir entre una lista que puede modificar y una que no puede (o una forma cuya excentricidad puede establecer, y una que no puede), entonces una no es realmente sustituible para la otra .

Sería mejor si el sistema de tipo Java tuviera un tipo para una secuencia o colección que solo permitiera iterar, y otro que permitiera la modificación. Quizás Iterable puede usarse para esto, pero sospecho que le faltan algunas características (como size()) que uno realmente quisiera. Desafortunadamente, creo que esto es una limitación de la API de colecciones Java actual.

Varias personas han notado que la documentación para Collection permite que una implementación arroje una excepción desde el add método. Supongo que esto significa que una Lista que no se puede modificar está obedeciendo la letra de la ley cuando se trata del contrato de add pero creo que uno debería examinar el código de uno y ver cuántos lugares hay para proteger las llamadas a métodos mutantes de List (add, addAll, remove, clear) con try / catch blocks antes de argumentar que el LSP no está violado. Quizás no lo es, pero eso significa que todo el código que llama List.add en una lista que recibió como parámetro está roto.

Eso sin duda estaría diciendo mucho.

(Argumentos similares pueden demostrar que la idea de null es un miembro de cada tipo es también una violación del Principio de Sustitución de Liskov).

† Sé que hay otras maneras de abordar el problema Elipse / Círculo, como hacer que sean inmutables, o eliminar el método setEccentricity. Estoy hablando aquí solo del caso más común, como una analogía.


10
2018-02-26 19:27



No creo que sea una violación porque el contrato (es decir, el List interfaz) dice que las operaciones de mutación son opcionales.


4
2018-02-26 19:11



Creo que no estás mezclando cosas aquí.
De LSP:

La noción de Liskov de un subtipo de comportamiento define una noción de   posibilidad de sustitución de objetos mutables; es decir, si S es un subtipo de T,   entonces los objetos de tipo T en un programa pueden ser reemplazados con objetos de   escriba S sin alterar ninguna de las propiedades deseables de ese   programa (por ejemplo, corrección).

LSP se refiere a subclases.

La lista es una interfaz no es una superclase Especifica una lista de métodos que proporciona una clase. Pero la relación no está acoplada como con una clase de padres. El hecho de que la clase A y la clase B implementen la misma interfaz, no garantiza nada sobre el comportamiento de estas clases. Una implementación siempre puede regresar verdadera y la otra arrojar una excepción o siempre devolver falsa o lo que sea, pero ambas se adhieren a la interfaz a medida que implementan los métodos de la interfaz para que la persona que llama pueda llamar al método en el objeto.


1
2018-02-26 23:22