¿Existe un lenguaje o patrón de diseño que permita la * eliminación * del comportamiento o las propiedades del objeto en una jerarquía de clases?

28

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.

Sebastien Diot
fuente
3
'sin tener que usar algunos trucos horribles en todas partes': deshabilitar un comportamiento ES un truco horrible: implicaría que 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)
keppla
¿Una combinación del patrón de estrategia y la herencia podría permitirle "componer" un comportamiento heredado para supertipos específicos? Cuando dices: " 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?
StuperUser
1
Se podría, por supuesto, acaba de lanzar una NotSupportedExceptionde Penguin.fly().
Felix Dombek
En lo que respecta a los idiomas, ciertamente puede desinstalar un método en una clase secundaria. Por ejemplo, en Ruby: class Penguin < Bird; undef fly; end;. Si deberías, es otra pregunta.
Nathan Long
Esto rompería el principio de Liskov y podría decirse que todo el punto de OOP.
deadalnix

Respuestas:

17

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:

var Penguin = Object.create(Bird);
Penguin.fly = undefined;
Penguin.swim = function () { ... };

En este caso particular, Penguinestá sombreando activamente el Bird.flymétodo que hereda escribiendo una flypropiedad con valor undefinedpara el objeto.

Ahora puede decir que Penguinya no se puede tratar como normal Bird. Pero como se mencionó, en el mundo real simplemente no puede. Porque estamos modelando Birdcomo una entidad voladora.

La alternativa es no hacer la suposición general de que Bird's puede volar. Sería sensato tener una Birdabstracció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:

// for some value of Object.make
var Penguin = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Swimmer, ...
);
var Hawk = Object.make(
  /* base class: */ Bird,
  /* mixins: */ Flyer, Carnivore, ...
);

Si tienes curiosidad, tengo una implementación deObject.make

Adición:

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

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.

Raynos
fuente
10
"En el mundo real simplemente no puede". Sí puede. Un pingüino es un pájaro. La capacidad de volar no es una propiedad de las aves, es simplemente una propiedad coincidente de la mayoría de las especies de aves. Las propiedades que definen a las aves son "animales emplumados, alados, bípedos, endotérmicos, que ponen huevos, vertebrados" (Wikipedia), nada sobre volar allí.
pdr
2
@pdr nuevamente, depende de tu definición de pájaro. Cuando estaba usando el término "pájaro", me refería a la abstracción de clase que usamos para representar a las aves, incluido el método mosca. También mencioné que puedes hacer que tu abstracción de clase sea menos específica. Además, un pingüino no está emplumado.
Raynos
2
@Raynos: los pingüinos están emplumados. Sus plumas son bastante cortas y densas, por supuesto.
Jon Purdy
@JonPurdy es justo, siempre imagino que tenían pelaje.
Raynos
+1 en general, y en particular para el "mamut". Jajaja
Sebastien Diot
28

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.

Péter Török
fuente
1
El LSP es para tipos , no para clases .
Jörg W Mittag
2
@ PéterTörök: Esta pregunta no existiría de otra manera :-) Puedo pensar en dos ejemplos de Ruby. Classes una subclase de Moduleaunque ClassIS-NOT-A Module. Pero aún tiene sentido ser una subclase, ya que reutiliza gran parte del código. OTOH, StringIOIS-A IO, pero los dos no tienen ninguna relación de herencia (aparte de lo obvio de que ambos heredan Object, por supuesto), porque no comparten ningún código. Las clases son para compartir código, los tipos son para describir protocolos. IOy StringIOtienen el mismo protocolo, por lo tanto, el mismo tipo, pero sus clases no están relacionadas.
Jörg W Mittag
1
@ JörgWMittag, OK, ahora entiendo mejor lo que quieres decir. Sin embargo, para mí su primer ejemplo parece más un mal uso de la herencia que la expresión de algún problema fundamental que parece sugerir. La herencia pública IMO no debe usarse para reutilizar la implementación, solo para expresar relaciones de subtipo (is-a). Y el hecho de que pueda ser mal utilizado no lo descalifica: no puedo imaginar ninguna herramienta utilizable de ningún dominio que no pueda ser mal utilizado.
Péter Török
2
Para las personas que votan por esta respuesta: tenga en cuenta que esto realmente no responde la pregunta, especialmente después de la aclaración editada. No creo que esta respuesta merezca un voto negativo, porque lo que dice es muy cierto e importante saberlo, pero realmente no respondió la pregunta.
jhocking
1
Imagine un Java en el que solo las interfaces son tipos, las clases no lo son y las subclases pueden "desinstalar" las interfaces de su superclase, y creo que tiene una idea aproximada.
Jörg W Mittag
15

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, FlightlessBirdque tienen el comportamiento correcto inyectado por una Fábrica, que los subtipos relevantes, por ejemplo, se Penguin : FlightlessBirdobtienen automáticamente, y cualquier otra cosa realmente específica es manejada por la Fábrica como algo natural.

StuperUser
fuente
1
Mencioné el patrón Decorador en mi respuesta, pero el patrón Estrategia también funciona bastante bien.
jhocking
1
+1 para "Favorecer la composición sobre la herencia". Sin embargo, la necesidad de patrones de diseño especiales para implementar la composición en lenguajes de tipo estático refuerza mi sesgo hacia lenguajes dinámicos como Ruby.
Roy Tinker el
11

¿No es el verdadero problema que estás asumiendo que Birdtiene un Flymétodo? Por qué no:

class Bird
{
    // features that all birds have
}

class BirdThatCanSwim : Bird
{
    public void Swim() {...};
}

class BirdThatCanFly : Bird
{
    public void Fly() {...};
}


class Penguin : BirdThatCanSwim { }
class Sparrow : BirdThatCanFly { }

Ahora el problema obvio es la herencia múltiple ( Duck), por lo que lo que realmente necesita son interfaces:

interface IBird { }
interface IBirdThatCanSwim : IBird { public void Swim(); }
interface IBirdThatCanFly : IBird { public void Fly(); }
interface IBirdThatCanQuack : IBird { public void Quack(); }

class Duck : BirdThatCanFly, IBirdThatCanSwim, IBirdThatCanQuack
{
    public void Swim() {...};
    public void Quack() {...};
}
Scott Whitlock
fuente
3
El problema es que la evolución no sigue el Principio de sustitución de Liskov y hereda con la eliminación de características.
Donal Fellows
7

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:

  • Bird es una clase con un método fly ()
  • El pingüino debe heredar de Bird
  • El pingüino no puede volar ()
  • No me importa si es un buen diseño o si coincide con el mundo real, como es el ejemplo proporcionado en esta pregunta.

Tu dijiste :

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

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):

class Penguin extends Bird {
     function fly():void {
          throw new Exception("Hey, I'm a penguin, I can't fly !");
     }
}

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):

var bird:Bird = new Penguin();
bird.fly();

lanzará una excepción de tiempo de ejecución.

David
fuente
"Simplemente haga que su Penguin arroje una excepción o herede de una clase NonFlyingBird que arroje una excepción" Eso sigue siendo una violación del LSP. Todavía está sugiriendo que un pingüino puede volar, a pesar de que su implementación es fallida. Nunca debería haber un método de vuelo en Penguin.
pdr
@pdr: no sugiere que un pingüino pueda volar, sino que debería volar (es un contrato). La excepción te dirá que no puede . Por cierto, no estoy afirmando que sea una buena práctica de OOP, solo estoy respondiendo una parte de la pregunta
David
El punto es que no se debe esperar que un pingüino vuele solo porque es un pájaro. Si quiero escribir un código que diga "Si x puede volar, haz esto; si no, hazlo" Tengo que usar un try / catch en su versión, donde debería poder preguntarle al objeto si puede volar (existe un método de conversión o verificación). Puede ser solo en la redacción, pero su respuesta implica que lanzar una excepción cumple con LSP.
pdr
@pdr "Tengo que usar un try / catch en tu versión" -> ese es el objetivo de pedir perdón en lugar de permiso (porque incluso un pato podría haber roto sus alas y no poder volar). Arreglaré la redacción.
David
"Ese es el objetivo de pedir perdón en lugar de permiso". Sí, excepto que permite que el framework arroje el mismo tipo de excepción para cualquier método faltante, por lo que "try: except AttributeError:" de Python es exactamente equivalente a C # 's "if (X es Y) {} else {}" e instantáneamente reconocible como tal. Pero si lanzó deliberadamente una CannotFlyException para anular la funcionalidad fly () predeterminada en Bird, entonces se vuelve menos reconocible.
pdr
7

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.

alex
fuente
Estoy de acuerdo, Fly debería ser una interfaz que el pájaro pueda implementar. También podría implementarse como un método con un comportamiento predeterminado que se puede anular, pero un enfoque más limpio es usar una interfaz.
Jon Raynor
6

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:

class Bird {
public:
   virtual void fly()=0;
};

Deberías tener algo como esto:

   class Bird {
   public:
      virtual float fly(float x) const=0;
   };

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 ().

tp1
fuente
1
Me gusta mucho tu respuesta. Creo que merece más votos.
Sebastien Diot
2
Excelente respuesta El problema es que la pregunta simplemente agita sus manos sobre lo que realmente hace fly (). Cualquier implementación real de fly tendría, como mínimo, un destino: fly (destino coordinado) que, en el caso del pingüino, podría anularse para implementar {return currentPosition)}
Chris Cudmore
4

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.

jhocking
fuente
3

Peter ha mencionado el Principio de sustitución de Liskov, pero creo que es necesario explicarlo.

Sea q (x) una propiedad comprobable sobre objetos x de tipo T. Entonces q (y) debería ser demostrable para objetos y de tipo S donde S es un subtipo de T.

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

pdr
fuente
2

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.

DJClayworth
fuente
0

La forma típica de manejar tal situación es lanzar algo así como un UnsupportedOperationExceptionresp (Java). NotImplementedException(DO#).

usuario281377
fuente
Siempre que documente esta posibilidad en Bird.
DJClayworth
0

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

Sebastien Diot
fuente
0

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.

class eagle : bird, IFly
class penguin : bird

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.

Jon Raynor
fuente
-1

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:

  • nada
  • lanza una excepción
  • llama al método Penguin :: swim () en su lugar
  • afirma que el pingüino está bajo el agua ("vuelan" a través del agua)

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.

Caleb
fuente
2
-1 por incluso sugerir llamar a Penguin :: swim (). Eso viola el principio de mínimo asombro y hará que los programadores de mantenimiento de todas partes maldigan tu nombre.
DJClayworth
1
@DJClayworth Como el ejemplo era ridículo, en primer lugar, el voto negativo por violación del comportamiento inferido de fly () y swim () parece un poco exagerado. Pero si realmente quiere ver esto en serio, estoy de acuerdo en que es más probable que vaya al otro lado e implemente swim () en términos de fly (). Los patos nadan remando sus pies; los pingüinos nadan agitando sus alas.
Caleb
1
Estoy de acuerdo en que la pregunta era tonta, pero el problema es que he visto a personas hacer esto en la vida real: use las llamadas existentes que "realmente no hacen nada" para implementar una funcionalidad rara. Realmente arruina el código, y generalmente termina con tener que escribir "if (! (MyBird instanceof Penguin)) fly ();" en muchos lugares, esperando que nadie cree una clase de avestruz.
DJClayworth
La afirmación es aún peor. Si tengo una serie de Birds, todos los cuales tienen el método fly (), no quiero una falla de aserción cuando llamo a fly () en ellos.
DJClayworth
1
No leí la documentación de Penguin , porque me entregaron una serie de Birds y no sabía que un Penguin estaría en la matriz. Leí la documentación de Bird que decía que cuando llamo a fly () el pájaro vuela. Si esa documentación hubiera declarado claramente que se podría lanzar una excepción si el pájaro no vuela, lo habría permitido. Si dijera que llamar a fly () a veces lo habría hecho nadar, hubiera cambiado a usar una biblioteca de clase diferente. O ido a tomar una copa muy grande.
DJClayworth