c++ - modo - herencia en java ejemplos
por qué derivar de una clase concreta es un diseño pobre (5)
Creo que esto se debe a que la clase concreta tiene un comportamiento concreto. Cuando se deriva de ello, se compromete a conservar el mismo "contrato", pero el contrato real se define mediante la implementación específica de la clase base, y en realidad tendrá muchas sutilezas que probablemente se rompa sin saberlo.
Descargo de responsabilidad: no soy un desarrollador experimentado; es solo una sugerencia
Estaba leyendo sobre el patrón NonVirtual Interface : Herb Sutter habla de por qué las funciones virtuales deben ser privadas en la mayoría de los casos, protegidas en algún caso y nunca públicas.
Pero al final del artículo, él escribe:
No deriva de clases concretas. O, como dice Scott Meyers en el Ítem 33 de C ++ Más Efectivo, [8] "Haz que las clases que no sean hojas sean abstractas". (Es cierto que puede suceder en la práctica, en un código escrito por otra persona, por supuesto, ¡no por usted!) Y en este caso puede que tenga que tener un destructor virtual público para acomodar lo que ya es un diseño pobre. Es mejor refactorizar y arregla el diseño, si puedes)
Pero no entiendo por qué este es un diseño pobre
Cuando deriva de una clase base concreta, hereda la funcionalidad, que puede no revelarse si no tiene el código fuente. Cuando programa en una interfaz (o clase abstracta) no hereda la funcionalidad, lo que significa que es una mejor manera de hacer cosas como exponer una API. Dicho esto, creo que hay muchas veces en las que la herencia de una clase concreta es aceptable
En general, es difícil mantener estrictamente un contrato entre dos objetos concretos distintos.
La herencia se vuelve realmente más fácil y más robusta cuando tratamos con el comportamiento genérico.
Imagine que queremos crear una clase llamada Ferrari
y una subclase llamada SuperFerrari
. El contrato es: turnTheKey()
, goFast()
, skid()
A primera vista, suena como una clase muy similar, sin conflicto. Vayamos con la herencia de estas dos clases concretas.
Sin embargo, ahora queremos agregar una función a SuperFerrari
: turnOnBluRayDisc()
.
Problema: la herencia espera una relación IS-A entre los componentes. Con esta nueva característica, SuperFerrari
ya no es un simple Ferrari
con su propio comportamiento; ahora ADMITE comportamiento. Llevaría a algún código feo, con algún elenco necesario, para elegir la nueva característica al tratar con una referencia de Ferrari
inicialmente (polimorfismo).
Con una clase abstracta / de interfaz en común para ambas clases, este problema desaparecería ya que Ferrari
y SuperFerrari
serían dos hojas, y es más lógico ya que ahora Ferrari
y SuperFerrari
no deberían considerarse similares (ya no es una relación IS-A) .
En resumen, derivar de la clase concreta conduciría en muchos casos a un código pobre / feo, menos flexible y difícil de leer y mantener.
Hacer una jerarquía impulsada por clase / interfaz abstracta permite que las clases de niños concretas evolucionen por separado sin ningún problema, especialmente cuando se implementan sub jerarquías dedicadas a una cantidad especial de clases concretas sin aburrir a otras hojas, y aún así se benefician del polimorfismo.
Un problema con heredar de un tipo concreto es que crea cierta ambigüedad en cuanto a si el código que especifica un cierto tipo realmente quiere un objeto del tipo concreto específico, o quiere un objeto de un tipo que se comporta de la manera que el tipo concreto se comporta . Esta distinción es vital en C ++, ya que hay muchos casos en que las operaciones que funcionarán correctamente en objetos de cierto tipo fallarán gravemente en objetos de tipos derivados. En Java y .NET, hay muchas menos situaciones donde se puede usar un objeto de un tipo particular, pero no se puede usar un objeto de un tipo derivado. Como tal, heredar de un tipo concreto no es tan problemático. Incluso allí, sin embargo, tener clases concretas selladas que heredan de clases abstractas, y usar los tipos de clases abstractas en todas partes excepto en invocaciones de constructor (que deben usar tipos concretos) facilitará el cambio de partes de la jerarquía de clases sin romper el código.
Puede comprar una copia de C ++ más efectivo o consultar en su biblioteca local una copia y leer el artículo 33 para obtener una explicación completa. La explicación dada aquí es que hace que tu clase sea propensa a la asignación parcial, también conocida como cortar:
Hay dos problemas aquí. Primero, el operador de asignación invocado en la última línea es el de la clase
Animal
, aunque los objetos involucrados son de tipoLizard
. Como resultado, solo se modificará la parteAnimal
deliz1
. Esta es una tarea parcial. Después de la asignación, los miembros deAnimal
liz1
tienen los valores que obtuvieron deliz2
, pero los miembros deLizard
liz1
permanecen sin cambios.El segundo problema es que los programadores reales escriben código como este. No es raro hacer asignaciones a objetos a través de punteros, especialmente para programadores de C experimentados que se han trasladado a C ++. Siendo ese el caso, nos gustaría hacer que la asignación se comporte de una manera más razonable. Como señala el ítem 32, nuestras clases deberían ser fáciles de usar y difíciles de usar de forma incorrecta, y las clases en la jerarquía anterior son fáciles de usar de forma incorrecta.
Consulte esta pregunta y otras para obtener una descripción del problema de corte de objetos en C ++.
El artículo 33 también dice esto, más adelante:
El reemplazo de una clase base de concreto como
Animal
con una clase base abstracta comoAbstractAnimal
produce beneficios mucho más allá de simplemente hacer que el comportamiento deloperator=
sea más fácil de entender. También reduce las posibilidades de que intente tratar las matrices de forma polimórfica, cuyas desagradables consecuencias se examinan en el Ítem 3. Sin embargo, el beneficio más significativo de la técnica ocurre en el nivel de diseño, porque reemplaza las clases de base de concreto con una base abstracta las clases te obligan a reconocer explícitamente la existencia de abstracciones útiles. Es decir, te hace crear nuevas clases abstractas para conceptos útiles, incluso si no eres consciente del hecho de que existen conceptos útiles.