class - tipos - ¿Diferencia entre una instancia de una clase y una clase que representa una instancia ya?
que es una clase en programacion (6)
Utilizo Java como ejemplo, pero esto es más una pregunta general relacionada con el diseño de POO.
IOException
como ejemplo las IOException
s en Java. ¿Por qué hay una clase FileNotFoundException
por ejemplo? ¿No debería ser una instancia de una IOException
donde la causa es FileNotFound
? Yo diría que FileNotFoundException
es una instancia de IOException
. ¿Dónde termina esto? FileNotFoundButOnlyCheckedOnceException
, FileNotFoundNoMatterHowHardITriedException
...?
También he visto código en proyectos en los que trabajé donde existían clases como FirstLineReader
y LastLineReader
. Para mí, estas clases en realidad representan instancias, pero veo tal diseño en muchos lugares. Mire el código fuente de Spring Framework, por ejemplo, viene con cientos de clases de este tipo, donde cada vez que veo una, veo una instancia en lugar de un plano. ¿No son las clases destinadas a ser planos?
Lo que estoy tratando de preguntar es cómo se toma la decisión entre estas 2 opciones muy simples:
Opción 1:
enum DogBreed {
Bulldog, Poodle;
}
class Dog {
DogBreed dogBreed;
public Dog(DogBreed dogBreed) {
this.dogBreed = dogBreed;
}
}
Opcion 2:
class Dog {}
class Bulldog extends Dog {
}
class Poodle extends Dog {
}
La primera opción le da al llamante el requisito de configurar la instancia que está creando. En la segunda opción, la clase ya representa la instancia en sí misma (como la veo, lo que podría estar totalmente equivocado ...).
Si está de acuerdo en que estas clases representan instancias en lugar de planos, diría que es una buena práctica crear clases que representen instancias o es totalmente incorrecto la forma en que lo veo y mi declaración "clases que representan instancias" es solo una carga de ¿disparates?
Editado
En primer lugar : conocemos la definición de herencia y podemos encontrar muchos ejemplos en SO e Internet. Pero, creo que deberíamos mirar en profundidad y un poco más científico .
Nota 0 :
Aclaración sobre herencia y terminología de instancia .
Primero, permítame nombrar el Ámbito de Desarrollo para el ciclo de vida del desarrollo, cuando estamos modelando y programando nuestro sistema y el Runtime Scope para algunas veces nuestro sistema se está ejecutando.
Tenemos Clases y modelos y desarrollándolas en Ámbito de Desarrollo. Y Objetos en Runtime Scope. No hay Objeto en el Ámbito de Desarrollo .
Y en Orientado a objetos, la definición de instancia es: Crear un objeto a partir de una clase.
Por otro lado, cuando estamos hablando de clases y objetos, debemos aclarar nuestro punto de vista sobre el alcance del desarrollo y el alcance del tiempo de ejecución .
Entonces, con esta introducción, quiero aclarar la herencia :
La herencia es una relación entre Clases, NO Objetos .
La herencia puede existir en Development Scope, no en Runtime Scope. No hay herencia en el tiempo de ejecución.
Después de ejecutar nuestro proyecto, no hay ninguna relación entre padre e hijo (si solo hay herencia entre una clase hijo y una clase padre). Entonces, la pregunta es: ¿qué es super.invokeMethod1()
o super.attribute1
? , no son la relación entre niño y padre. Todos los atributos y métodos de un padre se transmiten al niño y eso es solo una notación para acceder a las partes que se transmiten desde un padre.
Además, no hay ningún Objeto en el Ámbito de Desarrollo. Así que no hay instancias en el ámbito de desarrollo. Es solo una relación Is-A y Has-A .
Por eso, cuando dijimos:
Yo diría que
FileNotFoundException
es una instancia de unaIOException
Debemos aclarar sobre nuestro alcance (desarrollo y tiempo de ejecución).
Por ejemplo, si FileNotFoundException
es una instancia de IOException
, entonces, ¿cuál es la relación entre una excepción FileNotFoundException
específica en tiempo de ejecución (el Objeto) y FileNotFoundException
? ¿Es una instancia de instancia?
Nota 1 :
¿Por qué usamos la herencia? El objetivo de la herencia es extender las funcionalidades de la clase padre (basadas en el mismo tipo).
- Esta extensión puede ocurrir agregando nuevos atributos o nuevos métodos.
- O anular los métodos existentes.
- Además, al extender una clase principal, también podemos llegar a la reutilización.
- No podemos restringir la funcionalidad de la clase padre (Principio de Liskov)
- Deberíamos poder reemplazar al niño como padre en el sistema (principio de Liskov)
- y etc.
Nota 2 :
El ancho y la profundidad de las jerarquías de herencia
El ancho y la profundidad de la herencia pueden estar relacionados con muchos factores:
- El proyecto: La complejidad del proyecto (Complejidad de tipos) y su arquitectura y diseño. El tamaño del proyecto, el número de clases y etc.
- El equipo: la experiencia de un equipo para controlar la complejidad del proyecto.
- y etc.
Sin embargo, tenemos algunas heurísticas al respecto. (Heurística de diseño orientado a objetos, Arthur J. Riel)
En teoría, las jerarquías de herencia deben ser profundas: cuanto más profundas, mejores.
En la práctica, las jerarquías de herencia no deben ser más profundas de lo que una persona promedio puede mantener en su memoria a corto plazo. Un valor popular para esta profundidad es seis.
Tenga en cuenta que son heurísticas y se basan en el número de memoria a corto plazo (7). Y tal vez la experiencia de un equipo afecte a este número. Pero en muchas jerarquías se utilizan como organigramas.
Nota 3 :
¿Cuándo estamos usando herencia equivocada?
Residencia en :
- Nota 1: el objetivo de Herencia (Extender las funcionalidades de la clase padre)
- Nota 2: el ancho y la profundidad de la herencia.
En estas condiciones utilizamos herencia errónea:
Tenemos algunas clases en una jerarquía de herencia, sin extender las funcionalidades de la clase primaria. La extensión debe ser razonable y debe ser suficiente para hacer una nueva clase . Los medios razonables desde el punto de vista del observador. El observador puede ser Project Architect o Designer (u otros arquitectos y diseñadores).
Tenemos muchas clases en la jerarquía de herencia. Se llama Sobre-Especialización. Algunas razones pueden causar esto:
- Tal vez no consideramos la Nota 1 (Extendiendo las funcionalidades de los padres)
- Tal vez nuestra modularización (empaque) no sea correcta. Y pusimos muchos casos de uso del sistema en un paquete y deberíamos hacer Refactorización de diseño.
Son otras razones, pero no se relacionan exactamente con esta respuesta.
Nota 4 :
¿Qué debemos hacer? ¿Cuándo estamos usando herencia equivocada?
Solución 1: deberíamos realizar Refactorización de diseño para verificar el valor de las clases con el fin de extender la funcionalidad principal . En esta refactorización, quizás se eliminen muchas clases de sistema.
Solución 2: Debemos realizar Refactorización de Diseño a la modularización. En esta refactorización, tal vez algunas clases de nuestro paquete se transmiten a otros paquetes.
Solución 3: Uso de la composición sobre la herencia .
Podemos usar esta técnica por muchas razones. La jerarquía dinámica es una de las razones populares por las que preferimos la composición en lugar de la herencia.
Vea las notas de Tim Boudreau (de Sun) here :
Las jerarquías de objetos no escalan
Solución 4: usar instancias sobre subclases
Esta pregunta es sobre esta técnica. Permítanme nombrarlo instancias sobre subclases .
Cuando podamos usarlo:
(Consejo 1): Considere la Nota 1, cuando no extendemos exactamente las funcionalidades de la clase padre. O las extensiones no son razonables y suficientes.
(Consejo 2 :) Considere la Nota 2, si tenemos muchas subclases (clases semi o idénticas) que amplían un poco la clase principal y podemos controlar esta extensión sin herencia. Tenga en cuenta que no es fácil decir eso. Debemos probar que no está violando otros Principios Orientados a Objetos como el Principio de Abrir-Cerrar.
¿Qué debemos hacer?
Martin Fowler recomienda ( Libro 1 página 232 y Libro 2 página 251):
Reemplace la subclase con campos , cambie los métodos a los campos de superclase y elimine las subclases.
Podemos usar otras técnicas como enum
como se menciona en la pregunta.
El primer código que usted cita implica excepciones.
La herencia es un ajuste natural para los tipos de excepción porque la construcción proporcionada por el lenguaje para diferenciar las excepciones de interés en la declaración try-catch es a través del uso del sistema de tipos. Esto significa que podemos elegir fácilmente manejar solo un tipo más específico ( FileNotFound
), o el tipo más general ( IOException
).
Probar el valor de un campo, para ver si manejar una excepción, significa salir de la construcción del lenguaje estándar y escribir algún código de protección de la placa de la caldera (p. Ej., Valor (es) de prueba y cambio si no está interesado).
(Además, las excepciones deben ser extensibles a través de los límites de DLL (compilación). Cuando usamos enumeraciones podemos tener problemas para ampliar el diseño sin modificar la fuente que introduce (y otras que consumen) la enumeración).
Cuando se trata de cosas distintas a las excepciones, la sabiduría de hoy fomenta la composición sobre la herencia, ya que esto tiende a dar como resultado diseños menos complejos y más fáciles de mantener.
Su Opción 1 es más un ejemplo de composición, mientras que su Opción 2 es claramente un ejemplo de herencia.
Si está de acuerdo en que estas clases representan instancias en lugar de planos, diría que es una buena práctica crear clases que representen instancias o es totalmente incorrecto la forma en que lo veo y mi declaración "clases que representan instancias" es solo una carga de ¿disparates?
Estoy de acuerdo con usted, y no diría que esto representa una buena práctica. Estas clases como se muestran no son particularmente personalizables y no representan un valor agregado.
Una clase que no ofrece anulaciones, ningún estado nuevo, ningún método nuevo, no está particularmente diferenciada de su base. Por lo tanto, hay poco mérito en declarar tal clase, a menos que tratemos de realizar pruebas de instancia (como lo hace la construcción del lenguaje de manejo de excepciones en las portadas). Realmente no podemos decir a partir de este ejemplo, que está ideado con el propósito de hacer la pregunta, si hay algún valor agregado en estas subclases pero no lo parece.
Sin embargo, para ser claros, hay muchos ejemplos peores de herencia, como cuando una (pre) ocupación como Maestro o Estudiante hereda de la Persona. Esto significa que un Profesor no puede ser un Estudiante al mismo tiempo a menos que nos involucremos en agregar aún más clases, por ejemplo, Estudiante del Profesor, tal vez utilizando herencia múltiple.
Podríamos llamar a esta explosión de clase, ya que a veces terminamos necesitando una matriz de clases debido a relaciones inapropiadas. (Agregue una nueva clase y necesita una fila o columna completamente nueva de clases explotadas).
Trabajar con un diseño que sufre una explosión de clase en realidad crea más trabajo para los clientes que consumen estas abstracciones, por lo que es una situación suelta.
En este caso, confiamos en el lenguaje natural porque cuando decimos que alguien es un Estudiante, esto no es, desde una perspectiva lógica, la misma relación permanente "es-una" / instancia-de (de subclases), sino más bien se está jugando un rol potencialmente temporal que la Persona: uno de los muchos roles posibles que una Persona podría desempeñar al mismo tiempo. En estos casos la composición es claramente superior a la herencia.
Sin embargo, en su escenario, es poco probable que BullDog pueda ser otra cosa que BullDog, por lo que la relación permanente es una relación de subclases, y al agregar poco valor, al menos esta jerarquía no corre el riesgo de explosión de clases.
Tenga en cuenta que el principal inconveniente de este enfoque de enumeración es que la enumeración puede no ser extensible dependiendo del idioma que esté utilizando. Si necesita extensibilidad arbitraria (por ejemplo, por otros y sin alterar su código), tiene la opción de usar algo extensible pero más débilmente escrito, como cadenas (no se detectan errores tipográficos, no se detectan duplicados, etc.), o puede utilizar la herencia, ya que ofrece una extensibilidad decente con una escritura más fuerte. Las excepciones necesitan este tipo de extensibilidad por parte de otros sin modificación y recompilación de los originales y otros, ya que se usan a través de los límites de DLL.
Si controla la enumeración y puede volver a compilar el código como una unidad según sea necesario para manejar nuevos tipos de perros, entonces no necesita esta extensibilidad.
En primer lugar, al incluir la pregunta de excepciones junto con un problema general de diseño del sistema, realmente estás haciendo dos preguntas diferentes.
Las excepciones son simplemente valores complicados. Sus comportamientos son triviales: proporcionan el mensaje, proporcionan la causa, etc. Y son naturalmente jerárquicos. Hay Throwable
en la parte superior, y otras excepciones se especializan repetidamente. La jerarquía simplifica el manejo de excepciones al proporcionar un mecanismo de filtro natural: cuando dices catch (IOException...
, sabes que obtendrás todo lo malo que sucedió con respecto a la I / O. No puedo ser más claro que eso. Pruebas, que pueden ser feo para las jerarquías de objetos grandes, no hay problema para las excepciones: hay poco o nada que probar en un valor.
De ello se deduce que si está diseñando valores complejos similares con comportamientos triviales, una jerarquía de herencia alta es una opción razonable: los diferentes tipos de nodos de árbol o gráfico constituyen un buen ejemplo.
Su segundo ejemplo parece ser sobre objetos con comportamientos más complejos. Estos tienen dos aspectos:
- Los comportamientos necesitan ser probados.
- Los objetos con comportamientos complejos a menudo cambian sus relaciones entre sí a medida que los sistemas evolucionan.
Estas son las razones de la mantra a menudo escuchada "composición sobre herencia". Desde mediados de los años 90, se sabe que las grandes composiciones de objetos pequeños generalmente son más fáciles de probar, mantener y cambiar que las grandes jerarquías de herencia de objetos necesariamente grandes.
Habiendo dicho eso, las opciones que ha ofrecido para la implementación están perdiendo el punto. La pregunta que debe responder es "¿Cuáles son los comportamientos de los perros que me interesan?" Luego describe esto con una interfaz, y programa a la interfaz.
interface Dog {
Breed getBreed();
Set<Dog> getFavoritePlaymates(DayOfWeek dayOfWeek);
void emitBarkingSound(double volume);
Food getFavoriteFood(Instant asOfTime);
}
Cuando entiendes los comportamientos, las decisiones de implementación se vuelven mucho más claras.
Luego, una regla de oro para la implementación es poner comportamientos simples y comunes en una clase base abstracta:
abstract class AbstractDog implements Dog {
private Breed breed;
Dog(Breed breed) { this.breed = breed; }
@Override Breed getBreed() { return breed; }
}
Debería poder probar dichas clases base mediante la creación de versiones concretas mínimas que solo lancen la UnsupportedOperationException
para los métodos no implementados y verifiquen los implementados. La necesidad de cualquier tipo de configuración más sofisticada es un olor a código: has puesto demasiado en la base.
Las jerarquías de implementación de este tipo pueden ser útiles para reducir el modelo, pero más de 2 de profundidad es un olor a código. Si necesita 3 o más niveles, es muy probable que pueda y deba envolver fragmentos del comportamiento común de las clases de bajo nivel en clases de ayuda que serán más fáciles de evaluar y estarán disponibles para la composición en todo el sistema. Por ejemplo, en lugar de ofrecer un protected void emitSound(Mp3Stream sound);
Para que los herederos utilicen el método de la clase base, sería mucho más preferible crear una nueva class SoundEmitter {}
y agregar un miembro final
con este tipo en Dog
.
Luego haga clases concretas llenando el resto del comportamiento:
class Poodle extends AbstractDog {
Poodle() { super(Breed.POODLE); }
Set<Dog> getFavoritePlaymates(DayOfWeek dayOfWeek) { ... }
Food getFavoriteFood(Instant asOfTime) { ... }
}
Observe: la necesidad de un comportamiento (que el perro debe poder devolver su raza) y nuestra decisión de implementar el comportamiento "obtener raza" en una clase base abstracta resultó en un valor acumulado de enumeración.
Terminamos adoptando algo más cercano a su Opción 1, pero esta no fue una elección a priori . Fluyó de pensar en los comportamientos y en la forma más limpia de implementarlos.
La opción 1 tiene que enumerar todas las causas conocidas en el momento de la declaración.
La opción 2 se puede ampliar creando nuevas clases, sin tocar la declaración original.
Esto es importante cuando la declaración base / original es realizada por el marco. Si hubiera 100 motivos conocidos, fijos, de problemas de E / S, una enumeración o algo similar podría tener sentido, pero si surgen nuevas formas de comunicación que también deberían ser excepciones de E / S, entonces la jerarquía de clases tiene más sentido. Cualquier biblioteca de clase que agregue a su aplicación puede extenderse con más excepciones de E / S sin tocar la declaración original.
Esta es básicamente la O en SOLID , abierta por extensión, cerrada por modificación.
Pero esta es también la razón por la que, como ejemplo, el tipo de enumeración DayOfWeek existe en muchos marcos. Es extremadamente improbable que el mundo occidental se despierte repentinamente un día y decida ir por 14 días únicos, u 8, o 6. Por lo tanto, tener clases para ellos es probablemente una exageración. Estas cosas son más fijas en piedra (knock-on-wood).
Las dos opciones que presentas en realidad no expresan lo que creo que estás tratando de entender. Lo que intentas diferenciar es la composición y la herencia.
La composición funciona así:
class Poodle {
Legs legs;
Tail tail;
}
class Bulldog {
Legs legs;
Tail tail;
}
Ambos tienen un conjunto común de características que podemos agregar para "componer" una clase. Podemos especializar los componentes donde necesitamos, pero solo podemos esperar que las "Piernas" funcionen en su mayoría como otras piernas.
Java ha elegido la herencia en lugar de la composición para IOException
y FileNotFoundException
.
Es decir, una FileNotFoundException
es un tipo de (es decir, una IOException
) y permite el manejo solo en función de la identidad de la superclase (aunque puede especificar un manejo especial si lo desea).
Los argumentos para elegir la composición sobre la herencia están bien ensayados por otros y se pueden encontrar fácilmente buscando "composición frente a herencia".
Los siguientes comentarios se encuentran bajo la condición de que las subclases no extiendan realmente la funcionalidad de su súper clase.
Desde el documento de Oracle:
Signals that an I/O exception of some sort has occurred. This class is the general class of exceptions produced by failed or interrupted I/O operations.
Dice que la excepción IOException es una excepción general . Si tenemos una causa enumeración:
enum cause{
FileNotFound, CharacterCoding, ...;
}
No podremos lanzar una excepción IOException si la causa en nuestro código personalizado no está incluida en la enumeración. En otra palabra, hace que la excepción IOException sea más específica que general .
Suponiendo que no estamos programando una biblioteca, y la funcionalidad de la clase Dog a continuación es específica en nuestro requisito comercial:
enum DogBreed {
Bulldog, Poodle;
}
class Dog {
DogBreed dogBreed;
public Dog(DogBreed dogBreed) {
this.dogBreed = dogBreed;
}
}
Personalmente creo que es bueno usar enumeración porque simplifica la estructura de clases (menos clases).