Considere el siguiente diseño
public class Person
{
public virtual string Name { get; }
public Person (string name)
{
this.Name = name;
}
}
public class Karl : Person
{
public override string Name
{
get
{
return "Karl";
}
}
}
public class John : Person
{
public override string Name
{
get
{
return "John";
}
}
}
¿Crees que hay algo mal aquí? Para mí, las clases de Karl y John deberían ser solo instancias en lugar de clases, ya que son exactamente lo mismo que:
Person karl = new Person("Karl");
Person john = new Person("John");
¿Por qué crearía nuevas clases cuando las instancias son suficientes? Las clases no agregan nada a la instancia.
programming-practices
inheritance
class-design
anti-patterns
Ignacio Soler García
fuente
fuente
Respuestas:
No es necesario tener subclases específicas para cada persona.
Tienes razón, esas deberían ser instancias en su lugar.
Objetivo de las subclases: ampliar las clases para padres
Las subclases se utilizan para ampliar la funcionalidad proporcionada por la clase principal. Por ejemplo, puede tener:
Una clase padre
Battery
que puede tenerPower()
algo y puede tener unaVoltage
propiedad,Y una subclase
RechargeableBattery
, que puede heredarPower()
yVoltage
, pero también puede serRecharge()
d.Tenga en cuenta que puede pasar una instancia de
RechargeableBattery
clase como parámetro a cualquier método que acepte aBattery
como argumento. Esto se llama principio de sustitución de Liskov , uno de los cinco principios SÓLIDOS. Del mismo modo, en la vida real, si mi reproductor de MP3 acepta dos pilas AA, puedo sustituirlas por dos pilas AA recargables.Tenga en cuenta que a veces es difícil determinar si necesita usar un campo o una subclase para representar una diferencia entre algo. Por ejemplo, si tiene que manejar baterías AA, AAA y de 9 voltios, ¿crearía tres subclases o usaría una enumeración? “Reemplazar subclase con campos” en Refactorización por Martin Fowler, página 232, puede darle algunas ideas y cómo pasar de una a otra.
En su ejemplo,
Karl
yJohn
no extienda nada, ni proporcionan ningún valor adicional: puede tener exactamente la misma funcionalidad usando laPerson
clase directamente. Tener más líneas de código sin valor adicional nunca es bueno.Un ejemplo de un caso de negocios.
¿Cuál podría ser un caso de negocios en el que tendría sentido crear una subclase para una persona específica?
Digamos que creamos una aplicación que administra a las personas que trabajan en una empresa. La aplicación también gestiona los permisos, por lo que Helen, la contadora, no puede acceder al repositorio SVN, pero Thomas y Mary, dos programadores, no pueden acceder a los documentos relacionados con la contabilidad.
Jimmy, el gran jefe (fundador y CEO de la compañía) tiene privilegios muy específicos que nadie tiene. Puede, por ejemplo, apagar todo el sistema o despedir a una persona. Tienes la idea.
El modelo más pobre para dicha aplicación es tener clases como:
porque la duplicación de código surgirá muy rápidamente. Incluso en el ejemplo muy básico de cuatro empleados, duplicará el código entre las clases de Thomas y Mary. Esto lo empujará a crear una clase principal común
Programmer
. Dado que también puede tener varios contadores, probablemente también creará unaAccountant
clase.Ahora, se da cuenta de que tener la clase
Helen
no es muy útil, así como mantenerThomas
yMary
: la mayor parte de su código funciona en el nivel superior de todos modos, a nivel de contadores, programadores y Jimmy. Al servidor SVN no le importa si son Thomas o Mary quienes necesitan acceder al registro; solo necesita saber si es un programador o un contador.Entonces terminas eliminando las clases que no usas:
"Pero puedo mantener a Jimmy tal como está, ya que siempre habría un solo CEO, un gran jefe, Jimmy", piensas. Además, Jimmy se usa mucho en su código, que en realidad se parece más a esto, y no como en el ejemplo anterior:
El problema con ese enfoque es que Jimmy aún puede ser atropellado por un autobús, y habría un nuevo CEO. O la junta directiva puede decidir que Mary es tan genial que debería ser un nuevo CEO, y Jimmy sería degradado a un puesto de vendedor, por lo que ahora debe revisar todo su código y cambiarlo todo.
fuente
Es una tontería usar este tipo de estructura de clases solo para variar detalles que obviamente deberían ser campos configurables en una instancia. Pero eso es particular a su ejemplo.
Hacer diferentes
Person
clases diferentes es ciertamente una mala idea: tendría que cambiar su programa y volver a compilar cada vez que una Persona nueva ingrese a su dominio. Sin embargo, eso no significa que heredar de una clase existente para que una cadena diferente se devuelva en algún lugar a veces no sea útil.La diferencia es cómo espera que varíen las entidades representadas por esa clase. Casi siempre se supone que las personas van y vienen, y se espera que casi todas las aplicaciones serias que tratan con usuarios puedan agregar y eliminar usuarios en tiempo de ejecución. Pero si modela cosas como diferentes algoritmos de cifrado, admitir uno nuevo probablemente sea un cambio importante de todos modos, y tiene sentido inventar una nueva clase cuyo
myName()
método devuelva una cadena diferente (y cuyoperform()
método presumiblemente hace algo diferente).fuente
En la mayoría de los casos, no lo harías. Su ejemplo es realmente un buen caso donde este comportamiento no agrega valor real.
También viola el principio Open Closed , ya que las subclases básicamente no son una extensión, sino que modifican el funcionamiento interno de los padres. Además, el constructor público del padre ahora es irritante en las subclases y, por lo tanto, la API se volvió menos comprensible.
Sin embargo, a veces, si solo tendrá una o dos configuraciones especiales que se usan a menudo en todo el código, a veces es más conveniente y lleva menos tiempo subclasificar un padre que tiene un constructor complejo. En casos tan especiales, no puedo ver nada malo con este enfoque. Llamémoslo constructor curry j / k
fuente
Si este es el alcance de la práctica, entonces estoy de acuerdo en que es una mala práctica.
Si John y Karl tienen comportamientos diferentes, entonces las cosas cambian un poco. Podría ser que
person
tiene un método paracleanRoom(Room room)
donde resulta que John es un gran compañero de cuarto y limpia muy eficazmente, pero Karl no lo hace y no limpia mucho la habitación.En este caso, tendría sentido que sean su propia subclase con el comportamiento definido. Hay mejores formas de lograr lo mismo (por ejemplo, una
CleaningBehavior
clase), pero al menos de esta manera no es una violación terrible de los principios de OO.fuente
En algunos casos, usted sabe que solo habrá un número determinado de instancias y entonces estaría bien (aunque feo en mi opinión) usar este enfoque. Es mejor usar una enumeración si el idioma lo permite.
Ejemplo de Java:
fuente
Mucha gente ya respondió. Pensé que daría mi propia perspectiva personal.
Érase una vez que trabajé en una aplicación (y todavía lo hago) que crea música.
La aplicación tuvo un resumen
Scale
clase con varias subclases:CMajor
,DMinor
, etc.Scale
parecía algo así:Los generadores de música trabajaron con una
Scale
instancia específica para generar música. El usuario seleccionaría una escala de una lista para generar música.Un día, se me ocurrió una idea genial: ¿por qué no permitir que el usuario cree sus propias escalas? El usuario seleccionaría notas de una lista, presionaría un botón y se agregaría una nueva escala a la lista de escalas disponibles.
Pero no pude hacer esto. Esto se debió a que todas las escalas ya están configuradas en tiempo de compilación, ya que se expresan como clases. Entonces me golpeó:
A menudo es intuitivo pensar en términos de 'superclases y subclases'. Casi todo se puede expresar a través de este sistema: superclase
Person
y subclasesJohn
yMary
; superclaseCar
y subclasesVolvo
yMazda
; superclaseMissile
y subclasesSpeedRocked
,LandMine
yTrippleExplodingThingy
.Es muy natural pensar de esta manera, especialmente para la persona relativamente nueva en OO.
Pero siempre debemos recordar que las clases son plantillas , y los objetos son contenido vertido en estas plantillas . Puede verter el contenido que desee en la plantilla, creando innumerables posibilidades.
No es el trabajo de la subclase llenar la plantilla. Es el trabajo del objeto. El trabajo de la subclase es agregar funcionalidad real o expandir la plantilla .
Y es por eso que debería haber creado una
Scale
clase concreta , con unNote[]
campo, y dejar que los objetos completen esta plantilla ; posiblemente a través del constructor o algo así. Y eventualmente, así lo hice.Cada vez que diseñe una plantilla en una clase (p. Ej., Un
Note[]
miembro vacío que debe llenarse o unString name
campo al que se le debe asignar un valor), recuerde que es el trabajo de los objetos de esta clase completar la plantilla ( o posiblemente aquellos que crean estos objetos). Las subclases están destinadas a agregar funcionalidad, no a completar plantillas.Es posible que sienta la tentación de crear un tipo de sistema de "superclase
Person
, subclasesJohn
yMary
", como lo hizo, porque le gusta la formalidad que esto le da.De esta manera, solo puedes decir
Person p = new Mary()
, en lugar dePerson p = new Person("Mary", 57, Sex.FEMALE)
. Hace las cosas más organizadas y más estructuradas. Pero como dijimos, crear una nueva clase para cada combinación de datos no es un buen enfoque, ya que hincha el código para nada y lo limita en términos de habilidades de tiempo de ejecución.Así que aquí hay una solución: usar una fábrica básica, incluso podría ser estática. Al igual que:
De esta manera, puede usar fácilmente los 'ajustes preestablecidos' y el 'viene con el programa', así:
Person mary = PersonFactory.createMary()
pero también se reserva el derecho de diseñar dinámicamente nuevas personas, por ejemplo, en el caso de que desee permitir que el usuario lo haga. . P.ej:O incluso mejor: haz algo así:
Me dejé llevar. Creo que entiendes la idea.
Las subclases no están destinadas a completar las plantillas establecidas por sus superclases. Las subclases están destinadas a agregar funcionalidad . Los objetos están destinados a completar las plantillas, para eso están.
No debería crear una nueva clase para cada combinación posible de datos. (Al igual que no debería haber creado una nueva
Scale
subclase para cada combinación posible deNote
s).Es una pauta: cada vez que cree una nueva subclase, considere si agrega alguna funcionalidad nueva que no existe en la superclase. Si la respuesta a esa pregunta es "no", entonces podría estar tratando de 'completar la plantilla' de la superclase, en cuyo caso simplemente cree un objeto. (Y posiblemente una Fábrica con 'presets', para hacer la vida más fácil).
Espero que ayude.
fuente
Esta es una excepción a los 'deberían ser instancias' aquí, aunque ligeramente extrema.
Si desea que el compilador imponga permisos para acceder a los métodos en función de si es Karl o John (en este ejemplo) en lugar de pedirle a la implementación del método que verifique qué instancia se ha pasado, puede usar diferentes clases. Al imponer diferentes clases en lugar de la creación de instancias, puede realizar comprobaciones de diferenciación entre 'Karl' y 'John' en el momento de la compilación en lugar del tiempo de ejecución y esto podría ser útil, particularmente en un contexto de seguridad donde el compilador puede ser más confiable que el tiempo de ejecución comprobaciones de código de (por ejemplo) bibliotecas externas.
fuente
Se considera una mala práctica tener código duplicado .
Esto se debe al menos en parte a que las líneas de códigos y, por lo tanto, el tamaño de la base de código se correlaciona con los costos de mantenimiento y los defectos .
Aquí está la lógica relevante del sentido común para mí sobre este tema en particular:
1) Si tuviera que cambiar esto, ¿cuántos lugares necesitaría visitar? Menos es mejor aquí.
2) ¿Hay alguna razón por la que se hace de esta manera? En su ejemplo, no podría haberlo, pero en algunos ejemplos podría haber algún tipo de razón para hacerlo. Uno que se me ocurre en la parte superior de mi cabeza es en algunos marcos como los primeros Grails, donde la herencia no necesariamente funcionaba bien con algunos de los complementos para GORM. Pocos y distantes entre sí.
3) ¿Es más limpio y fácil de entender de esta manera? Nuevamente, no está en su ejemplo, pero hay algunos patrones de herencia que pueden ser más exagerados donde las clases separadas son en realidad más fáciles de mantener (ciertos usos del comando o los patrones de estrategia me vienen a la mente).
fuente
Hay un caso de uso: formar una referencia a una función o método como un objeto regular.
Puedes ver esto en el código Java GUI mucho:
El compilador lo ayuda aquí creando una nueva subclase anónima.
fuente