Un defecto conocido de las jerarquías de clase tradicionales es que son malas a la hora de modelar el mundo real. Como ejemplo, tratar de representar especies de animales con clases. En realidad, hay varios problemas al hacer eso, pero uno para el que nunca vi una solución es cuando una subclase "pierde" un comportamiento o propiedad que se definió en una superclase, como un pingüino que no puede volar (hay son probablemente mejores ejemplos, pero ese es el primero que me viene a la mente).
Por un lado, no desea definir, para cada propiedad y comportamiento, algún indicador que especifique si está presente, y verificarlo cada vez antes de acceder a ese comportamiento o propiedad. Solo quisiera decir que los pájaros pueden volar, simple y claramente, en la clase de Aves. Pero entonces sería bueno si se pudieran definir "excepciones" después, sin tener que usar algunos trucos horribles en todas partes. Esto sucede a menudo cuando un sistema ha sido productivo por un tiempo. De repente, encuentra una "excepción" que no cabe en absoluto en el diseño original, y no desea cambiar una gran parte de su código para acomodarlo.
Entonces, ¿hay algún lenguaje o patrón de diseño que pueda manejar este problema de manera limpia, sin requerir cambios importantes en la "superclase" y todo el código que lo utiliza? Incluso si una solución solo maneja un caso específico, varias soluciones juntas podrían formar una estrategia completa.
Después de pensar más, me doy cuenta de que olvidé el Principio de sustitución de Liskov. Por eso no puedes hacerlo. Suponiendo que defina "rasgos / interfaces" para todos los "grupos de características" principales, puede implementar libremente rasgos en diferentes ramas de la jerarquía, como el rasgo Volador podría ser implementado por Birds y algún tipo especial de ardillas y peces.
Entonces, mi pregunta podría equivaler a "¿Cómo podría desinstalar un rasgo?" Si su superclase es Java Serializable, debe ser uno también, incluso si no hay forma de serializar su estado, por ejemplo, si contiene un "Socket".
Una forma de hacerlo es definir siempre todos sus rasgos en pares desde el principio: Flying and NotFlying (que arrojaría UnsupportedOperationException, si no se compara). Not-trait no definiría ninguna interfaz nueva, y simplemente podría verificarse. Suena como una solución "barata", en particular si se usa desde el principio.
fuente
function save_yourself_from_crashing_airplane(Bird b) { f.fly() }
sería mucho más complicado. (como dijo Peter Török, viola el LSP)" it would be nice if one could define "exceptions" afterward, without having to use some horrible hacks everywhere"
¿consideras un método de fábrica que controla el comportamiento hacky?NotSupportedException
dePenguin.fly()
.class Penguin < Bird; undef fly; end;
. Si deberías, es otra pregunta.Respuestas:
Como otros han mencionado, tendrías que ir contra LSP.
Sin embargo, se puede argumentar que una subclase es simplemente una extensión arbitraria de una superclase. Es un objeto nuevo por derecho propio y la única relación con la superclase es que se utiliza una base.
Esto puede tener sentido lógico, en lugar de decir que Penguin es un pájaro. Tu dicho Penguin hereda un subconjunto de comportamiento de Bird.
Generalmente, los lenguajes dinámicos le permiten expresar esto fácilmente, a continuación se muestra un ejemplo usando JavaScript:
En este caso particular,
Penguin
está sombreando activamente elBird.fly
método que hereda escribiendo unafly
propiedad con valorundefined
para el objeto.Ahora puede decir que
Penguin
ya no se puede tratar como normalBird
. Pero como se mencionó, en el mundo real simplemente no puede. Porque estamos modelandoBird
como una entidad voladora.La alternativa es no hacer la suposición general de que Bird's puede volar. Sería sensato tener una
Bird
abstracción que permita a todas las aves heredar de ella, sin fallar. Esto significa solo hacer suposiciones que todas las subclases pueden contener.En general, la idea de Mixin se aplica muy bien aquí. Tenga una clase base muy delgada y mezcle todos los demás comportamientos en ella.
Ejemplo:
Si tienes curiosidad, tengo una implementación de
Object.make
Adición:
No "desinstalas" un rasgo. Simplemente arreglas tu jerarquía de herencia. O puedes cumplir con tu contrato de superclases o no debes fingir que eres de ese tipo.
Aquí es donde brilla la composición del objeto.
Por otro lado, Serializable no significa que todo deba ser serializado, solo significa que el "estado que le interesa" debe ser serializado.
No deberías estar usando un rasgo "NotX". Eso es simplemente una horrenda hinchazón de código. Si una función espera un objeto volador, debería estrellarse y quemarse cuando le das un mamut.
fuente
AFAIK todos los idiomas basados en herencia se basan en el Principio de sustitución de Liskov . Eliminar / deshabilitar una propiedad de clase base en una subclase violaría claramente LSP, por lo que no creo que tal posibilidad se implemente en ninguna parte. El mundo real es realmente desordenado, y no puede ser modelado con precisión por abstracciones matemáticas.
Algunos idiomas proporcionan rasgos o mixins, precisamente para tratar estos problemas de una manera más flexible.
fuente
Class
es una subclase deModule
aunqueClass
IS-NOT-AModule
. Pero aún tiene sentido ser una subclase, ya que reutiliza gran parte del código. OTOH,StringIO
IS-AIO
, pero los dos no tienen ninguna relación de herencia (aparte de lo obvio de que ambos heredanObject
, por supuesto), porque no comparten ningún código. Las clases son para compartir código, los tipos son para describir protocolos.IO
yStringIO
tienen el mismo protocolo, por lo tanto, el mismo tipo, pero sus clases no están relacionadas.Fly()
se encuentra en el primer ejemplo en: Head First Design Patterns para The Strategy Pattern , y esta es una buena situación de por qué debería "Favorecer la composición sobre la herencia". .Puede mezclar composición y herencia al tener supertipos de
FlyingBird
,FlightlessBird
que tienen el comportamiento correcto inyectado por una Fábrica, que los subtipos relevantes, por ejemplo, sePenguin : FlightlessBird
obtienen automáticamente, y cualquier otra cosa realmente específica es manejada por la Fábrica como algo natural.fuente
¿No es el verdadero problema que estás asumiendo que
Bird
tiene unFly
método? Por qué no:Ahora el problema obvio es la herencia múltiple (
Duck
), por lo que lo que realmente necesita son interfaces:fuente
Primero, SÍ, cualquier lenguaje que permita una fácil modificación dinámica de objetos le permitirá hacerlo. En Ruby, por ejemplo, puede eliminar fácilmente un método.
Pero como dijo Péter Török, violaría el LSP .
En esta parte, me olvidaré de LSP y asumiré que:
Tu dijiste :
Parece que lo que quieres es que Python " pida perdón en lugar de permiso "
Simplemente haga que su Penguin arroje una excepción o herede de una clase NonFlyingBird que arroje una excepción (pseudocódigo):
Por cierto, lo que elija: generar una excepción o eliminar un método, al final, el siguiente código (suponiendo que su idioma admita la eliminación del método):
lanzará una excepción de tiempo de ejecución.
fuente
Como alguien señaló anteriormente en los comentarios, los pingüinos son pájaros, los pingüinos no vuelan, ergo, no todos los pájaros pueden volar.
Por lo tanto, Bird.fly () no debe existir o se le debe permitir que no funcione. Prefiero lo primero.
Tener FlyingBird extiende Bird tiene un método .fly () sería correcto, por supuesto.
fuente
El verdadero problema con el ejemplo fly () es que la entrada y la salida de la operación no están definidas correctamente. ¿Qué se requiere para que un pájaro vuele? ¿Y qué pasa después de que el vuelo tiene éxito? Los tipos de parámetros y los tipos de retorno para la función fly () deben tener esa información. De lo contrario, su diseño depende de efectos secundarios aleatorios y cualquier cosa puede suceder. La parte de cualquier cosa es la que causa todo el problema, la interfaz no está definida correctamente y se permite todo tipo de implementación.
Entonces, en lugar de esto:
Deberías tener algo como esto:
Ahora define explícitamente los límites de la funcionalidad: su comportamiento de vuelo solo tiene que decidir un flotador único: la distancia desde el suelo, cuando se le da la posición. Ahora todo el problema se resuelve automáticamente. Un pájaro que no puede volar solo devuelve 0.0 de esa función, nunca abandona el suelo. Es un comportamiento correcto para eso, y una vez que se decide ese flotador, sabes que has implementado completamente la interfaz.
El comportamiento real puede ser difícil de codificar para los tipos, pero esa es la única forma de especificar sus interfaces correctamente.
Editar: quiero aclarar un aspecto. Esta versión float-> float de la función fly () es importante también porque define una ruta. Esta versión significa que un pájaro no puede duplicarse mágicamente mientras está volando. Esta es la razón por la cual el parámetro es flotante simple: es la posición en el camino que toma el ave. Si desea rutas más complejas, entonces Point2d posinpath (float x); que usa la misma x que la función fly ().
fuente
Técnicamente, puede hacer esto en casi cualquier lenguaje dinámico / tipado (JavaScript, Ruby, Lua, etc.) pero casi siempre es una muy mala idea. Eliminar los métodos de una clase es una pesadilla de mantenimiento, similar al uso de variables globales (es decir, no se puede decir en un módulo que el estado global no se ha modificado en otro lugar).
Los buenos patrones para el problema que describió es Decorador o Estrategia, diseñando una arquitectura de componentes. Básicamente, en lugar de eliminar comportamientos innecesarios de las subclases, construye objetos agregando los comportamientos necesarios. Entonces, para construir la mayoría de las aves, agregaría el componente volador, pero no agregue ese componente a sus pingüinos.
fuente
Peter ha mencionado el Principio de sustitución de Liskov, pero creo que es necesario explicarlo.
Por lo tanto, si un pájaro (objeto x del tipo T) puede volar (q (x)), entonces un pingüino (objeto y del tipo S) puede volar (q (y)), por definición. Pero claramente ese no es el caso. También hay otras criaturas que pueden volar pero que no son del tipo Bird.
Cómo lidiar con esto depende del idioma. Si un idioma admite herencia múltiple, entonces debe usar una clase abstracta para las criaturas que pueden volar; si un idioma prefiere interfaces, entonces esa es la solución (y la implementación de fly debería encapsularse en lugar de heredarse); o, si un idioma admite Duck Typing (sin juego de palabras), puede implementar un método fly en las clases que puedan y llamarlo si está allí.
Pero cada propiedad de una superclase debería aplicarse a todas sus subclases.
[En respuesta a la edición]
Aplicar un "rasgo" de CanFly a Bird no es mejor. Todavía sugiere al código de llamada que todas las aves pueden volar.
Un rasgo en los términos que definió es exactamente lo que Liskov quiso decir cuando dijo "propiedad".
fuente
Permítanme comenzar mencionando (como todos los demás) el Principio de sustitución de Liskov, que explica por qué no deberían hacer esto. Sin embargo, el problema de lo que debe hacer es uno de diseño. En algunos casos, puede no ser importante que Penguin no pueda volar. Tal vez pueda hacer que Penguin arroje InsufficientWingsException cuando se le pida que vuele, siempre y cuando tenga claro en la documentación de Bird :: fly () que puede arrojar eso para las aves que no pueden volar. De tener una prueba para ver si realmente puede volar, aunque eso hincha la interfaz.
La alternativa es reestructurar sus clases. Creemos la clase "FlyingCreature" (o mejor una interfaz, si se trata del lenguaje que lo permite). "Bird" no hereda de FlyingCreature, pero puedes crear "FlyingBird" que sí. Alondra, Buitre y Águila todos heredan de FlyingBird. Penguin no lo hace. Simplemente hereda de Bird.
Es un poco más complicado que la estructura ingenua, pero tiene la ventaja de ser preciso. Notarás que todas las clases esperadas están allí (Bird) y el usuario generalmente puede ignorar las 'inventadas' (FlyingCreature) si no es importante si tu criatura puede volar o no.
fuente
La forma típica de manejar tal situación es lanzar algo así como un
UnsupportedOperationException
resp (Java).NotImplementedException
(DO#).fuente
Muchas buenas respuestas con muchos comentarios, pero no todas están de acuerdo, y solo puedo elegir una sola, así que resumiré aquí todas las opiniones con las que estoy de acuerdo.
0) No asumas "tipeo estático" (lo hice cuando pregunté, porque hago Java casi exclusivamente). Básicamente, el problema depende mucho del tipo de lenguaje que se use.
1) Se debe separar la jerarquía de tipos de la jerarquía de reutilización de código en el diseño y en la cabeza, incluso si se superponen en su mayoría. En general, use clases para reutilizar e interfaces para tipos.
2) La razón por la que normalmente Bird IS-A Fly es porque la mayoría de las aves pueden volar, por lo que es práctico desde el punto de vista de reutilización de código, pero decir que Bird IS-A Fly es realmente incorrecto ya que hay al menos una excepción (Pingüino).
3) En los lenguajes estáticos y dinámicos, puede lanzar una excepción. Pero esto solo debe usarse si se declara explícitamente en el "contrato" de la clase / interfaz que declara la funcionalidad, de lo contrario es un "incumplimiento de contrato". También significa que ahora debe estar preparado para detectar la excepción en todas partes, por lo que debe escribir más código en el sitio de la llamada, y es un código feo.
4) En algunos lenguajes dinámicos, en realidad es posible "eliminar / ocultar" la funcionalidad de una superclase. Si verificando la presencia de la funcionalidad es cómo verifica "IS-A" en ese idioma, entonces esta es una solución adecuada y sensata. Si, por otro lado, la operación "IS-A" es otra cosa que aún dice que su objeto "debería" implementar la funcionalidad que falta ahora, entonces su código de llamada asumirá que la funcionalidad está presente y la llamará y se bloqueará, por lo que equivale a lanzar una excepción.
5) La mejor alternativa es separar realmente el rasgo Volar del rasgo Pájaro. Entonces, un ave voladora tiene que extender / implementar explícitamente tanto Bird como Fly / Flying. Este es probablemente el diseño más limpio, ya que no tiene que "eliminar" nada. La única desventaja ahora es que casi todas las aves tienen que implementar tanto Bird como Fly, por lo que escribes más código. La solución a esto es tener una clase intermediaria FlyingBird, que implementa tanto Bird como Fly, y representa el caso común, pero esta solución podría ser de uso limitado sin herencia múltiple.
6) Otra alternativa que no requiere herencia múltiple es usar composición en lugar de herencias. Cada aspecto de un animal está modelado por una clase independiente, y un Bird concreto es una composición de Bird, y posiblemente Fly or Swim, ... Obtiene la reutilización completa del código, pero tiene que hacer uno o más pasos adicionales para llegar a la funcionalidad Flying, cuando tienes una referencia de un Bird concreto. Además, el lenguaje natural "object IS-A Fly" y "object AS-A (cast) Fly" ya no funcionarán, por lo que debe inventar su propia sintaxis (algunos lenguajes dinámicos pueden tener una solución). Esto podría hacer que su código sea más engorroso.
7) Defina su rasgo Volar de modo que ofrezca una salida clara para algo que no puede volar. Fly.getNumberOfWings () podría devolver 0. Si Fly.fly (direction, currentPotinion) debería devolver la nueva posición después del vuelo, entonces Penguin.fly () podría devolver la posición actual sin cambiarla. Puede terminar con un código que técnicamente funciona, pero hay algunas advertencias. En primer lugar, algunos códigos pueden no tener un comportamiento obvio de "no hacer nada". Además, si alguien llama a x.fly (), esperaría que haga algo , incluso si el comentario dice que fly () no puede hacer nada . Finalmente, el pingüino IS-A Flying aún volvería verdadero, lo que podría ser confuso para el programador.
8) Haz como 5), pero usa la composición para sortear casos que requerirían herencia múltiple. Esta es la opción que preferiría para un lenguaje estático, ya que 6) parece más engorroso (y probablemente requiera más memoria porque tenemos más objetos). Un lenguaje dinámico puede hacer que 6) sea menos engorroso, pero dudo que se vuelva menos engorroso que 5).
fuente
Defina un comportamiento predeterminado (márquelo como virtual) en la clase base y anúlelo según sea necesario. De esa manera cada pájaro puede "volar".
¡Incluso los pingüinos vuelan, se desliza por el hielo a cero altitud!
El comportamiento de volar puede anularse según sea necesario.
Otra posibilidad es tener una interfaz Fly. No todas las aves implementarán esa interfaz.
Las propiedades no se pueden eliminar, por eso es importante saber qué propiedades son comunes en todas las aves. Creo que es más un problema de diseño asegurarse de que las propiedades comunes se implementen en el nivel base.
fuente
Creo que el patrón que estás buscando es un buen polimorfismo. Si bien es posible que pueda eliminar una interfaz de una clase en algunos idiomas, probablemente no sea una buena idea por las razones dadas por Péter Török. Sin embargo, en cualquier lenguaje OO, puede anular un método para cambiar su comportamiento, y eso incluye no hacer nada. Para tomar prestado su ejemplo, puede proporcionar un método Penguin :: fly () que haga lo siguiente:
Las propiedades pueden ser un poco más fáciles de agregar y eliminar si planifica con anticipación. Puede almacenar propiedades en un mapa / diccionario / matriz asociativa en lugar de utilizar variables de instancia. Puede usar el patrón Factory para producir instancias estándar de tales estructuras, por lo que un Bird que viene de BirdFactory siempre comenzará con el mismo conjunto de propiedades. La codificación de valor clave de Objective-C es un buen ejemplo de este tipo de cosas.
Nota: La lección seria de los comentarios a continuación es que, aunque anular la eliminación de un comportamiento puede funcionar, no siempre es la mejor solución. Si necesita hacer esto de manera significativa, debe considerar que es una señal fuerte de que su gráfico de herencia es defectuoso. No siempre es posible refactorizar las clases de las que hereda, pero cuando lo es, esa suele ser la mejor solución.
Usando su ejemplo Penguin, una forma de refactorizar sería separar la habilidad de volar de la clase Bird. Como no todas las aves pueden volar, incluir un método fly () en Bird fue inapropiado y condujo directamente al tipo de problema sobre el que está preguntando. Entonces, mueva el método fly () (y quizás takeoff () y land ()) a una clase o interfaz Aviator (según el idioma). Esto le permite crear una clase FlyingBird que hereda de Bird y Aviator (o hereda de Bird e implementa Aviator). Penguin puede continuar heredando directamente de Bird pero no de Aviator, evitando así el problema. Tal disposición también podría facilitar la creación de clases para otras cosas voladoras: FlyingFish, FlyingMammal, FlyingMachine, AnnoyingInsect, etc.
fuente