Últimamente me preocupa el uso de clases abstractas.
A veces se crea una clase abstracta por adelantado y funciona como una plantilla de cómo funcionarían las clases derivadas. Eso significa, más o menos, que proporcionan una funcionalidad de alto nivel, pero omite ciertos detalles para ser implementados por las clases derivadas. La clase abstracta define la necesidad de estos detalles al implementar algunos métodos abstractos. En tales casos, una clase abstracta funciona como un plano, una descripción de alto nivel de la funcionalidad o como quiera llamarlo. No se puede usar por sí solo, pero debe estar especializado para definir los detalles que se han dejado fuera de la implementación de alto nivel.
En otras ocasiones, sucede que la clase abstracta se crea después de la creación de algunas clases "derivadas" (dado que la clase principal / abstracta no está allí, todavía no se han derivado, pero ya sabes a qué me refiero). En estos casos, la clase abstracta generalmente se usa como un lugar donde puede colocar cualquier tipo de código común que contengan las clases derivadas actuales.
Habiendo hecho las observaciones anteriores, me pregunto cuál de estos dos casos debería ser la regla. ¿Debería extenderse algún tipo de detalle a la clase abstracta solo porque actualmente son comunes en todas las clases derivadas? ¿Debería existir un código común que no sea parte de una funcionalidad de alto nivel?
¿Debería existir un código que no tenga significado para la clase abstracta en sí solo porque resulta ser común para las clases derivadas?
Permítanme dar un ejemplo: la clase abstracta A tiene un método a () y un método abstracto aq (). El método aq (), en ambas clases derivadas AB y AC, utiliza un método b (). ¿Debería b () moverse a A? En caso afirmativo, en caso de que alguien mire solo a A (supongamos que AB y AC no están allí), ¡la existencia de b () no tendría mucho sentido! ¿Esto es malo? ¿Debería alguien poder echar un vistazo a una clase abstracta y comprender lo que está sucediendo sin visitar las clases derivadas?
Para ser honesto, al momento de preguntar esto, tiendo a creer que escribir una clase abstracta que tenga sentido sin tener que buscar en las clases derivadas es una cuestión de código limpio y arquitectura limpia. Ι Realmente no me gusta la idea de una clase abstracta que actúe como un volcado para cualquier tipo de código que es común en todas las clases derivadas.
¿Qué piensas / practicas?
fuente
Writing an abstract class that makes sense without having to look in the derived classes is a matter of clean code and clean architecture.
-- ¿Por qué? ¿No es posible que, en el curso del diseño y desarrollo de una aplicación, descubra que varias clases tienen una funcionalidad común que naturalmente se puede refactorizar en una clase abstracta? ¿Debo ser lo suficientemente clarividente como para anticipar esto antes de escribir cualquier código para las clases derivadas? Si no soy tan astuto, ¿tengo prohibido realizar una refactorización de este tipo? ¿Debo tirar mi código y comenzar de nuevo?Respuestas:
Lo que pregunta es dónde colocar
b()
, y, en otro sentido, una pregunta es siA
es la mejor opción como la súper clase inmediata paraAB
yAC
.Parece que hay tres opciones:
b()
en ambosAB
yAC
ABAC-Parent
que heredaA
y que introduceb()
, y luego se usa como la superclase inmediata paraAB
yAC
b()
enA
(sin saber si otra clase futuroAD
va a quererb()
o no)Hasta que se presente otra clase
AD
que no quierab()
, (3) parece ser la elección correcta.En el momento de un
AD
regalo, podemos refactorizar el enfoque en (2). ¡Después de todo, es software!fuente
b()
en ninguna clase. Conviértalo en una función libre que tome todos sus datos como argumentos y tenga ambosAB
yAC
llámelo. Esto significa que no necesita moverlo o crear más clases cuando agregaAD
.b()
no necesita un estado de instancia de larga vida.ac
que llame enb
lugar deac
ser abstracto?Una clase abstracta no está destinada a ser el vertedero de varias funciones o datos que se agregan a la clase abstracta porque es conveniente.
La regla general que parece proporcionar el enfoque orientado a objetos más confiable y extensible es " Prefiero la composición sobre la herencia ". Una clase abstracta probablemente se considere mejor como una especificación de interfaz que no contiene ningún código.
Si tiene algún método que es un tipo de método de biblioteca que acompaña a la clase abstracta, un método que es el medio más probable de expresar alguna funcionalidad o comportamiento que las clases derivadas de una clase abstracta normalmente necesitan, entonces tiene sentido crear un clase entre la clase abstracta y otras clases donde este método está disponible. Esta nueva clase proporciona una instanciación particular de la clase abstracta que define una alternativa o ruta de implementación particular al proporcionar este método.
La idea de una clase abstracta es tener un modelo abstracto de lo que se supone que deben proporcionar las clases derivadas que realmente implementan la clase abstracta en cuanto a servicio o comportamiento. La herencia es fácil de usar y muchas veces las clases más útiles se componen de varias clases que usan un patrón de mezcla .
Sin embargo, siempre hay preguntas de cambio, qué va a cambiar y cómo va a cambiar y por qué va a cambiar.
La herencia puede conducir a cuerpos de fuente frágiles y frágiles (vea también Herencia: ¡simplemente deje de usarla ya! ).
fuente
b()
sea una función pura. Podría ser un functoid o una plantilla u otra cosa. Estoy usando la frase "método de biblioteca" como un componente, ya sea una función pura, un objeto COM o lo que sea que se use para la funcionalidad que proporciona a la solución.Su pregunta sugiere una o una aproximación a las clases abstractas. Pero creo que debería considerarlos como una herramienta más en su caja de herramientas. Y luego la pregunta es: ¿para qué trabajos / problemas son las clases abstractas la herramienta adecuada?
Un excelente caso de uso es implementar el patrón de método de plantilla . Pones toda la lógica invariante en la clase abstracta y la lógica variante en sus subclases. Tenga en cuenta que la lógica compartida en sí misma es incompleta y no funcional. La mayoría de las veces se trata de implementar un algoritmo, donde varios pasos son siempre iguales, pero al menos uno varía. Ponga este paso como un método abstracto que se llama desde una de las funciones dentro de la clase abstracta.
Creo que su primer ejemplo es esencialmente una descripción del patrón del método de plantilla (corríjame si me equivoco), por lo que consideraría esto como un caso de uso perfectamente válido de clases abstractas.
Para su segundo ejemplo, creo que el uso de clases abstractas no es la opción óptima, porque existen métodos superiores para lidiar con la lógica compartida y el código duplicado. Digamos que usted tiene clase abstracta
A
, las clases derivadasB
yC
y ambas clases derivadas compartir algo de lógica en forma de métodos()
. Para decidir el enfoque correcto para deshacerse de la duplicación, es importante saber si el métodos()
es parte de la interfaz pública o no.Si no es así (como en su propio ejemplo concreto con el método
b()
), el caso es bastante simple. Simplemente cree una clase separada, extrayendo también el contexto que se necesita para realizar la operación. Este es un ejemplo clásico de composición sobre herencia . Si se necesita poco o ningún contexto, una función auxiliar simple podría ser suficiente como se sugiere en algunos comentarios.Si
s()
es parte de la interfaz pública, se vuelve un poco más complicado. Bajo la suposición de ques()
no tiene nada que verB
y con lo queC
está relacionadoA
, no debes ponerlos()
dentroA
. Entonces, ¿dónde ponerlo? Yo argumentaría por declarar una interfaz separadaI
, que defines()
. Por otra parte, debe crear una clase separada que contenga la lógica para implementars()
y ambas,B
yC
dependa de ello.Por último, aquí hay un enlace a una excelente respuesta de una pregunta interesante sobre SO que podría ayudarlo a decidir cuándo ir a una interfaz y cuándo para una clase abstracta:
fuente
Me parece que te estás perdiendo el punto de orientación del objeto, tanto lógica como técnicamente. Describe dos escenarios: agrupar el comportamiento común de los tipos en una clase base y el polimorfismo. Ambas son aplicaciones legítimas de una clase abstracta. Pero si debe hacer que la clase sea abstracta o no depende de su modelo analítico, no de las posibilidades técnicas.
Debes reconocer un tipo que no tiene encarnaciones en el mundo real, pero que sienta las bases para tipos específicos que sí existen. Ejemplo: un animal. No hay tal cosa como un animal. Siempre es un perro o un gato o lo que sea, pero no hay un animal real. Sin embargo, Animal los enmarca a todos.
Entonces hablas de niveles. Los niveles no son parte del trato cuando se trata de herencia. Tampoco es reconocer datos comunes o comportamientos comunes, ese es un enfoque técnico que probablemente no ayudará. Debe reconocer un estereotipo y luego insertar la clase base. Si no existe este estereotipo, puede ser mejor con un par de interfaces implementadas por múltiples clases.
El nombre de su clase abstracta junto con sus miembros debería tener sentido. Debe ser independiente de cualquier clase derivada, tanto técnica como lógicamente. Like Animal podría tener un método abstracto Eat (que sería polimórfico) y propiedades abstractas booleanas IsPet e IsLifestock, que tiene sentido sin saber acerca de gatos, perros o cerdos. Cualquier dependencia (técnica o lógica) debe ir solo en un sentido: de descendiente a base. Las clases base tampoco deberían tener conocimiento de las clases descendentes.
fuente
Primer caso , de su pregunta:
Segundo caso:
Entonces, tu pregunta :
En mi opinión, tiene dos escenarios diferentes, por lo tanto, no puede dibujar una sola regla para decidir qué diseño debe aplicarse.
Para el primer caso, tenemos cosas como el patrón del Método de plantilla ya mencionado en otras respuestas.
Para el segundo caso, dio el ejemplo del método abstracto de clase A que recibe ab (); En mi opinión, el método b () solo debe moverse si tiene sentido para TODAS las otras clases derivadas. En otras palabras, si lo está moviendo porque se usa en dos lugares, probablemente esta no sea una buena opción, porque mañana podría haber una nueva clase Derivada concreta en la que b () no tiene ningún sentido. Además, como dijiste, si miras A clase por separado yb () tampoco tiene sentido en ese contexto, entonces probablemente sea una pista de que esta no es una buena opción de diseño también.
fuente
¡He tenido exactamente las mismas preguntas hace algún tiempo!
A lo que llegué es que cada vez que me encuentro con este problema, encuentro que hay algún concepto oculto que no he encontrado. Y este concepto más probablemente debe expresarse con algún objeto de valor . Tienes un ejemplo muy abstracto, así que me temo que no puedo demostrar lo que quiero decir con tu código. Pero aquí hay un caso de mi propia práctica. Tenía dos clases que representaban una forma de enviar una solicitud a un recurso externo y analizar la respuesta. Hubo similitudes en cómo se formaron las solicitudes y cómo se analizaron las respuestas. Así es como se veía mi jerarquía:
Tal código indica que simplemente uso incorrectamente la herencia. Así es como se podría refactorizar:
Desde entonces trato la herencia con mucho cuidado porque me lastimé muchas veces.
Desde entonces llegué a otra conclusión sobre la herencia. Estoy totalmente en contra de la herencia basada en la estructura interna . Rompe la encapsulación, es frágil, es procesal después de todo. Y cuando descompongo mi dominio de la manera correcta , la herencia simplemente no ocurre muy a menudo.
fuente