java - ¿Los métodos Collections.unmodifiableXXX violan LSP?
oop design (4)
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 sustituibilidad para objetos mutables; es decir, si S es un subtipo de T, entonces los objetos de tipo T en un programa pueden ser reemplazados por objetos de tipo S sin alterar ninguna de las propiedades deseables de ese programa (por ejemplo, corrección).
LSP se refiere a subclases .
List es una interfaz, no 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.
El principio de sustitución de Liskov es uno de los principios de SOLID . 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 la jerarquía de clases. Los subtipos deberían poder reemplazarse por supertipo sin violar el contrato.
También he leído algunos otros articles y estoy un poco perdido pensando en esta pregunta. ¿ Collections.unmodifiableXXX()
métodos Collections.unmodifiableXXX()
no violan el LSP?
Un extracto del artículo vinculado anteriormente:
En otras palabras, cuando se utiliza un objeto a través de su interfaz de clase base, el usuario solo conoce las condiciones previas y las postcondiciones de la clase base. Por lo tanto, los objetos derivados no deben esperar que dichos usuarios obedezcan condiciones previas 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 una lista no modificable en el futuro. La compilación funcionará, pero si el cliente de alguna manera intentó alterar la List
devuelta, fallaría en tiempo de ejecución.
¿Es por eso que Guava ha creado interfaces ImmutableXXX separadas para colecciones?
¿No es esto una violación directa de LSP o me he equivocado por completo?
LSP dice que cada subclase debe obedecer los mismos contratos que la superclase. Ya sea que este sea o no el caso de Collections.unmodifiableXXX()
depende de cómo se lea este contrato.
Los objetos devueltos por Collections.unmodifiableXXX()
lanzan una excepción si se intenta llamar a cualquier método de modificación. Por ejemplo, si se llama a add()
, se lanzará una UnsupportedOperationException
.
¿Cuál es el contrato general de add()
? De acuerdo con la documentación de la API es:
Se asegura de que esta colección contenga el elemento especificado (operación opcional). 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 cualquier motivo que no sea el que ya contiene el elemento, debe lanzar una excepción (en lugar de devolver falso). Esto preserva la invariante de que una colección siempre contiene el elemento especificado después de que esta llamada regrese.
Esto permite explícitamente que una implementación tenga código que no agrega 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.
No creo que sea una violación porque el contrato (es decir, la interfaz de la List
) dice que las operaciones de mutación son opcionales.
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 método setEccentricity
, 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 método setEccentricity
. 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 una List
normal que no puedes hacer con una envuelta 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 # puede usar IEnumerable
que captura la idea de una secuencia que puede iterar y leer, pero no escribir. 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 lanzar 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 en realidad no significa que sea una 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 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 método add
. Supongo que esto significa que una Lista que no se puede modificar está obedeciendo la letra de la ley cuando se trata del contrato para add
pero creo que uno debe examinar el código y ver cuántos lugares hay para proteger las llamadas a los métodos de mutación. de List ( add
, add
, 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 a List.add
en una lista que recibió como parámetro está roto.
Eso sin duda estaría diciendo mucho.
(Argumentos similares pueden mostrar que la idea de que el null
es miembro de cada tipo también es 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.