¿Crear subclases para instancias específicas es una mala práctica?

37

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.

Ignacio Soler García
fuente
17
Desafortunadamente, encontrará esto en muchos códigos de producción. Límpialo cuando puedas.
Adam Zuckerman
14
Este es un gran diseño, si un desarrollador quiere hacerse indispensable para cada cambio en los datos comerciales, y no tiene problemas para estar disponible las 24 horas, los 7 días de la semana ;-)
Doc Brown
1
Aquí hay una especie de pregunta relacionada donde el OP parece creer lo contrario de la sabiduría convencional. programmers.stackexchange.com/questions/253612/…
Dunk
11
Diseñe sus clases según el comportamiento, no los datos. Para mí, las subclases no agregan ningún comportamiento nuevo a la jerarquía.
Songo
2
Esto se usa ampliamente con subclases anónimas (como los controladores de eventos) en Java antes de que lambdas llegara a Java 8 (que es lo mismo con una sintaxis diferente).
marczellm

Respuestas:

77

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 Batteryque puede tener Power()algo y puede tener una Voltagepropiedad,

  • Y una subclase RechargeableBattery, que puede heredar Power()y Voltage, pero también puede ser Recharge()d.

Tenga en cuenta que puede pasar una instancia de RechargeableBatteryclase como parámetro a cualquier método que acepte a Batterycomo 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, Karly Johnno extienda nada, ni proporcionan ningún valor adicional: puede tener exactamente la misma funcionalidad usando la Personclase 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:

          El diagrama de clases muestra las clases de Helen, Thomas, Mary y Jimmy y sus padres comunes: la clase abstracta de Persona.

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á una Accountantclase.

          El diagrama de la clase muestra una Persona abstracta con tres hijos: Contador abstracto, Programador abstracto y Jimmy.  El contador tiene una subclase de Helen, y el programador tiene dos subclases: Thomas y Mary.

Ahora, se da cuenta de que tener la clase Helenno es muy útil, así como mantener Thomasy Mary: 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.

if (person is Programmer)
{
    this.AccessGranted = true;
}

Entonces terminas eliminando las clases que no usas:

          El diagrama de clase contiene las subclases de Contador, Programador y Jimmy con su clase principal común: Persona abstracta.

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

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

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.

Arseni Mourzenko
fuente
66
@DocBrown: a menudo me sorprende ver que las respuestas cortas que no son incorrectas pero no lo suficientemente profundas tienen una puntuación mucho más alta que las respuestas que explican en profundidad el tema. Probablemente a muchas personas no les gusta leer mucho, por lo que las respuestas muy cortas les parecen más atractivas. Aún así, edité mi respuesta para proporcionar al menos alguna explicación.
Arseni Mourzenko
3
Las respuestas cortas suelen publicarse primero. Las publicaciones anteriores obtienen más votos.
Tim B
1
@TimB: generalmente comienzo con respuestas de tamaño mediano y luego las edito para que sean muy completas (aquí hay un ejemplo , dado que probablemente hubo alrededor de quince revisiones, solo se muestran cuatro). Noté que cuanto más tiempo se convierte en la respuesta con el tiempo, menos votos positivos recibe; Una pregunta similar que sigue siendo corta atrae más votos a favor.
Arseni Mourzenko
De acuerdo con @DocBrown. Por ejemplo, una buena respuesta diría algo así como "subclase por comportamiento , no por contenido", y se referiría a alguna referencia que no haré en este comentario.
user949300
2
En caso de que no estuviera claro en el último párrafo: incluso si solo tiene 1 persona con acceso sin restricciones, debe hacer que esa persona sea una instancia de una clase separada que implemente acceso sin restricciones. La búsqueda de la persona en sí da como resultado una interacción de datos de código y puede abrir una lata completa de gusanos. Por ejemplo, ¿qué pasa si la Junta Directiva decide nombrar a un triunvirato como CEO? Si no tuviera una clase "Sin restricciones", no solo tendría que actualizar el CEO existente, sino también agregar 2 comprobaciones más para los otros miembros. Si lo tenía, simplemente configúrelos como "Sin restricciones".
Nzall
15

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 Personclases 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 cuyo perform()método presumiblemente hace algo diferente).

Kilian Foth
fuente
12

¿Por qué crearía nuevas clases cuando las instancias son suficientes?

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

Halcón
fuente
No estoy seguro de estar de acuerdo con el párrafo OCP. Si una clase está exponiendo un establecedor de propiedades a sus descendientes, suponiendo que siga buenos principios de diseño, la propiedad debería no ser parte de su funcionamiento interno
Ben Aaronson
@BenAaronson Siempre y cuando el comportamiento de la propiedad en sí no se altere, usted no viola OCP, pero aquí lo hacen. Por supuesto, puede usar esa Propiedad en su funcionamiento interno. ¡Pero no debes alterar el comportamiento existente! Solo debe extender el comportamiento, no cambiarlo con la herencia. Por supuesto, hay excepciones a cada regla.
Falcon
1
Entonces, ¿cualquier uso de miembros virtuales es una violación de OCP?
Ben Aaronson
3
@Falcon No es necesario derivar una nueva clase solo para evitar llamadas repetitivas a constructores complejos. Simplemente cree fábricas para los casos comunes y parametrice las pequeñas variaciones.
Keen
@BenAaronson, diría que tener miembros virtuales que tienen una implementación real es una violación. Los miembros abstractos, o dicen métodos que no hacen nada, serían puntos de extensión válidos. Básicamente, cuando observa el código de una clase base, sabe que se ejecutará tal cual, en lugar de que cada método no final sea candidato para ser reemplazado por algo completamente diferente. O para decirlo de otra manera: las formas en que una clase heredadora puede afectar el comportamiento de la clase base serán explícitas y limitadas a ser "aditivas".
millimoose
8

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 persontiene un método para cleanRoom(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 CleaningBehaviorclase), pero al menos de esta manera no es una violación terrible de los principios de OO.

corsiKa
fuente
No hay comportamiento diferente, solo datos diferentes. Gracias.
Ignacio Soler Garcia
6

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:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}
Adán
fuente
6

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 Scaleclase con varias subclases: CMajor, DMinor, etc. Scaleparecía algo así:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Los generadores de música trabajaron con una Scaleinstancia 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 Persony subclases Johny Mary; superclase Cary subclases Volvoy Mazda; superclase Missiley subclases SpeedRocked, LandMineyTrippleExplodingThingy .

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 Scaleclase concreta , con un Note[]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 un String namecampo 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, subclases Johny Mary", como lo hizo, porque le gusta la formalidad que esto le da.

De esta manera, solo puedes decir Person p = new Mary(), en lugar de Person 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:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

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:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

O incluso mejor: haz algo así:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

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 Scalesubclase para cada combinación posible de Notes).

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.

Aviv Cohn
fuente
Este es un excelente ejemplo, muchas gracias.
Ignacio Soler Garcia
@SoMoS Me alegra haber podido ayudar.
Aviv Cohn
2

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.

David Scholefield
fuente
2

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

dcgregorya
fuente
1
si es malo o no, no está escrito en piedra. Hay, por ejemplo, escenarios en los que la sobrecarga adicional de creación de objetos y llamadas a métodos puede ser tan perjudicial para el rendimiento que la aplicación ya no cumple con las especificaciones.
Jwenting
Ver punto # 2. Claro que hay razones por las cuales, ocasionalmente. Es por eso que debes preguntar antes de descifrar el código de alguien.
dcgregorya
0

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:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

El compilador lo ayuda aquí creando una nueva subclase anónima.

Simon Richter
fuente