Cuando en sus clases de idioma también hay objetos, ¿se aplica el principio de sustitución de Liskov a sus interfaces?

8

Según Wikipedia , el principio de sustitución de Liskov establece que

  

los objetos en un programa deben ser reemplazables con instancias de sus   subtipos sin alterar la corrección de ese programa

Normalmente, el principio de sustitución de Liskov no se aplica a las clases porque no son objetos.

Ahora en Ruby, las clases también son objetos. La clase más básica de la que se heredan todas las demás clases es BasicObject . BasicObject permite que se invoque el método de clase .new (el "constructor"; asigna un objeto y ejecuta el método de instancia #initialize en él) sin argumentos.

En este caso, sospecho que el principio de Liskov implicaría que ahora cada subclase también debe admitir .new sin ningún argumento, porque la subclase es un objeto y si no lo apoyara, la subclase no sería un sustituto de su clase padre. Si se aplica literalmente, una consecuencia sería que cada constructor debe admitir que se le llame sin ningún argumento.

Esto parece extraño. Pero me pregunto si hay algún punto en ir en esa dirección? O es esto solo una incompatibilidad del principio de Liskov (que asume que el conjunto de todos los objetos y el conjunto de todas las clases están disjuntados) y Ruby (donde el conjunto de todas las clases es un subconjunto del conjunto de todos los objetos).

Esto me recuerda extrañamente a antinomia de Russell , una paradoja que, según tengo entendido, se resolvió al separar las clases y las instancias (hacerlas disjuntas).

    
pregunta aef 11.04.2016 - 22:02

4 respuestas

3

Los idiomas varían mucho en la forma en que se manifiestan las clases (es decir, en tiempo de ejecución). En algunos sistemas de lenguaje,

  • no hay una manifestación en tiempo de ejecución de una clase, por lo que Liskov simplemente no es aplicable a las clases.

  • la manifestación de tiempo de ejecución de toda clase es una instancia directa (de alguna clase como TYPE o CLASS, por ejemplo), y todo lo que son es un token que representa una clase real específica, pero de lo contrario no participa en Subclasificarse a sí mismos. Por lo tanto, Liskov aún no se aplica (ya que son solo tokens, es decir, instancias de una sola clase en lugar de una jerarquía de clases).

  • Sin embargo, en otros sistemas, la manifestación en tiempo de ejecución de la clase es una instancia de un mecanismo de metaclase formal. En un sistema de lenguaje de este tipo, esperaríamos permitir la especificación de las metaclases por parte del usuario, y además permitiríamos la subclasificación normal definida por el usuario entre las metaclases.

En sistemas como el último, esperaríamos que se apliquen las reglas y los problemas de subclasificación normales. En esos sistemas podríamos crear subclases de metaclase que deberían seguir a Liskov, al igual que podemos crear subclases regulares que deberían seguir a Liskov. (Ver nota más abajo en la definición de Liskov.)

Además, en algunos de estos sistemas, los métodos de instancia de una metaclase proporcionan los métodos estáticos para sus clases; lo que se traduce en una forma bastante regular en eso: agregar un método de instancia a una metaclase base haría que ese método de instancia esté disponible para las subclases de esa metaclase, por lo tanto, hace que esos métodos estén disponibles como métodos estáticos en cualquier instancia de las clases de esos meta-sub- clases Como siempre cuando se hace una subclase, Liskov puede seguirse correctamente o no.

No sé en cuál de las categorías anteriores (si corresponde) se encuentra Ruby.

  

En este caso, sospecho que el principio de Liskov implicaría que ahora cada subclase también debe admitir .new sin ningún argumento, porque la subclase es un objeto y si no lo apoyara, la subclase no sería un sustituto. por su clase padre.

Estoy de acuerdo. (¡pero eso no significa necesariamente que Ruby sí!)

  

Si se aplica literalmente, una consecuencia sería que cada constructor debe admitir que se le llame sin ningún argumento.

No es que todos los constructores deban ser invocables sin argumentos, sino que cada clase debe incluir un constructor sin parámetros. (Muchos idiomas permiten que una clase determinada tenga varios constructores con firmas diferentes. No sé sobre Ruby).

NOTA: recuerde que Liskov es más que disponibilidad de métodos en las subclases, pero también sobre los comportamientos esperados de esos métodos entre la base y las subclases. No creo que pueda tener a Liskov en una subclase que no proporcionó los métodos de la clase base, por lo que creo que hacerlo es un requisito previo de Liskov, pero mientras sea necesario, proporcionar los mismos métodos y firmas no es suficiente para Liskov (El comportamiento en tiempo de ejecución también debe coincidir). (Una serie de idiomas se aplicarán en el momento de la compilación a través de su sistema de tipos, y las subclases proporcionan métodos de clase base; pero no conozco ningún idioma; el cumplimiento del comportamiento esperado es correcto, ya que es sustancialmente más subjetivo).

Por ejemplo, una subclase que proporciona el método de la clase base, aunque siempre lanza una excepción (por ejemplo, un método no permitido o inesperado) viola a Liskov (asumiendo que se espera un comportamiento más normal en la base o en otras subclases).

    
respondido por el Erik Eidt 11.04.2016 - 23:27
2
  

En este caso, sospecho que el principio de Liskov implicaría que ahora   cada subclase también debe admitir .new sin ningún argumento

Tu sospecha es errónea.

El problema es que se deduce de la implementación de la clase base de lo que es el contrato de la clase base. Pero son dos cosas distintas, aunque no estén escritas literalmente por separado. En este caso, el contrato de la clase base es que puedes construir cosas con new , y si los argumentos son incorrectos, puedes cometer un error (por ejemplo, lanzar).

El hecho de que en la clase base, los argumentos cero es aceptable, no requiere de inmediato que en el contrato, los argumentos cero sean aceptables y todas las clases derivadas deben permitirlos.

Es simplemente una implementación predeterminada de un contrato. Puede permitir cosas fuera de ese contrato. Puede ofrecer cosas que son opcionalmente parte de ese contrato. No hay una relación estricta 1: 1 entre la implementación de la clase base y el contrato. No se puede asumir que solo porque la implementación de la clase base permita algo, es una parte obligatoria del contrato de inmediato.

Sin embargo, en respuesta a la parte más general de su pregunta, entonces sí. Los metatipos de clase son instancias como cualquier otra y obedecen las mismas reglas que cualquier otra jerarquía de herencia, incluido el LSP.

    
respondido por el DeadMG 11.04.2016 - 23:52
1

La respuesta correcta implica una gran cantidad de "No del todo", con una pizca de "Tienes un punto".

Primero tomemos una interpretación literal. Ruby tiene métodos de clase y métodos de instancia. Los métodos de clase son los métodos del objeto de clase. Los métodos de instancia son los métodos de instancias de esa clase. La herencia es una relación entre los objetos de clase: llamar a .superclass en una clase le da otra. Sin embargo, esa relación entre clases NO es estrictamente hablando una relación de subtipo. Y por lo tanto, el principio de sustitución de Liskov se aplica solo a los métodos de instancia, no a los métodos de clase. Por lo tanto, tener diferentes métodos de clase para las clases en un árbol de herencia no es una violación del principio.

Puede demostrarlo usted mismo creando subclases independientes de Class , cree un objeto foo de la clase. Cree un objeto de la otra clase, utilizando MyClass2.new(foo) cuando lo cree para hacer que se herede de la primera. Y ahora ves muy explícitamente que una clase puede heredar de otra clase mientras que los objetos de clase son diferentes tipos de cosas.

Esa es la respuesta "no del todo". Ahora para la aspersión.

En el uso original, un "tipo" es un contrato para las operaciones que se admiten en un objeto. En los idiomas escritos de forma estática, los contratos son explícitos: el compilador no le permitirá intentar pasar un objeto del tipo incorrecto para que funcione y sepa lo que quiere.

Sin embargo, Ruby utiliza la escritura de pato. Puede pasar cualquier cosa a cualquier lugar, y siempre que responda a los métodos correctos o haga lo correcto, las cosas funcionarán. Esto hace que el contrato de un "tipo" sea implícito, no explícito. En particular, "tipo" no siempre significa clase. (A menudo lo hace, pero no siempre). Y la relación real de un tipo se basa en las operaciones que se encuentran en el código.

Entonces, si su código especifica un contrato por lo que hace un complemento, cualquier clase que implemente ese contrato es del tipo correcto. Si su código realiza una meta-programación y llama a .class para obtener la clase, entonces comienza a hacer cosas con la clase, los métodos de clase en el objeto son parte de lo que hace que un tipo sea un tipo. Si su código utiliza la introspección para descubrir todas las clases cuyo nombre sigue una convención dada y hace algo con ellas (las personas hacen cosas así con marcos de prueba de unidad), entonces el patrón de denominación de la clase es parte de su tipo.

Piense en esto como: "Un tipo es un contrato, y el contrato se define implícitamente por las operaciones que se encuentran en el código".

Si lo piensas de esa manera, las características dinámicas de Ruby permiten que casi cualquier cosa se convierta en parte del tipo. Y a qué se aplica el Principio de Sustitución de Liskov depende de las características dinámicas que elija utilizar. Además, en cualquier pieza compleja del software Ruby, incluidas sus clases principales, es fácil escribir un nuevo código que descubra que estás violando el principio de sustitución de Liskov.

Esto suena aterrador. ¿Qué significa en la práctica?

Significa que debe tener cuidado al agregar una metaprogramación compleja a un sistema existente. También significa que debe tener en claro qué tipo de contratos de software mantiene. Y, asumiendo que no está realizando una metaprogramación compleja, debe prestar más atención al Principio de Sustitución de Liskov en los métodos de instancia que a los de clase.

    
respondido por el btilly 12.04.2016 - 21:14
0
  

En este caso, sospecho que el principio de Liskov implicaría que ahora cada subclase también debe admitir .new sin ningún argumento

Eso no es correcto.

Aquí se requiere que el metaobjeto de alguna clase derivada de BaseObject necesite compatibilidad con niladic new, porque el metaobjeto de BaseObject sí lo hace.

El LSP establece que los objetos de una subclase deben admitir la misma interfaz que la clase base.

La desconexión aquí es que los metaobjetos de Derived y BaseObject son instancias de Class , no de Derived y BaseObject respectivamente, por lo que el LSP no se aplica. Podría argumentar que, como instancias de la misma clase, aún deberían tener la misma interfaz, pero a) eso no es parte del LSP, yb) no es así como funciona Ruby.

    
respondido por el Sebastian Redl 12.04.2016 - 09:08

Lea otras preguntas en las etiquetas