Muchas clases pequeñas versus herencia lógica (pero) intrincada

24

Me pregunto qué es mejor en términos de buen diseño de OOP, código limpio, flexibilidad y evitar olores de código en el futuro. Situación de imagen, donde tiene muchos objetos muy similares que necesita representar como clases. Estas clases no tienen ninguna funcionalidad específica, solo clases de datos y son diferentes solo por nombre (y contexto) Ejemplo:

Class A
{
  String name;
  string description;
}

Class B
{
  String name;
  String count;
  String description;
}

Class C
{
  String name;
  String count;
  String description;
  String imageUrl;
}

Class D
{
  String name;
  String count;
}

Class E
{
  String name;
  String count;
  String imageUrl;
  String age;
}

¿Sería mejor mantenerlos en clases separadas, para obtener una "mejor" legibilidad, pero con muchas repeticiones de código, o sería mejor usar la herencia para estar más SECO?

Al usar la herencia, terminarás con algo como esto, pero los nombres de clase perderán su significado contextual (debido a is-a, no has-a):

Class A
{
  String name;
  String description;
}

Class B : A
{
  String count;
}

Class C : B
{
  String imageUrl;
}

Class D : C
{
  String age;
}

Sé que la herencia no es y no debe usarse para la reutilización del código, pero no veo ninguna otra forma posible de reducir la repetición del código en tal caso.

usuario969153
fuente

Respuestas:

58

La regla general dice "Preferir delegación sobre herencia", no "evitar toda herencia". Si los objetos tienen una relación lógica y se puede usar una B donde se espera una A, es una buena práctica usar la herencia. Sin embargo, si los objetos tienen campos con el mismo nombre y no tienen una relación de dominio, no use la herencia.

Muy a menudo, vale la pena hacer la pregunta de "razón del cambio" en el proceso de diseño: si la clase A obtiene un campo adicional, ¿esperaría que la clase B también lo obtenga? Si eso es cierto, los objetos comparten una relación, y la herencia es una buena idea. Si no, sufra la pequeña repetición para mantener conceptos distintos en un código distinto.

thiton
fuente
Claro, la razón del cambio es un buen punto, pero ¿en qué situación, donde estas clases son y siempre serán constantes?
user969153
20
@ user969153: cuando las clases serán realmente constantes, no importa. Toda buena ingeniería de software es para el futuro mantenedor, que necesita hacer cambios. Si no habrá cambios futuros, todo funciona, pero créanme, siempre tendrán cambios a menos que programen para la papelera.
thiton
@ user969153: Dicho esto, la "razón del cambio" es heurística. Te ayuda a decidir, nada más.
thiton
2
Buena respuesta. Las razones para cambiar es S en el acrónimo SOLID del tío Bob, que ayudará a diseñar un buen software.
Klee
44
@ user969153, en mi experiencia, "¿dónde están estas clases y siempre serán constantes?" no es un caso de uso válido :) Todavía tengo que trabajar en una aplicación de tamaño significativo que no experimentó un cambio totalmente inesperado.
cdkMoose
18

Al usar la herencia, terminarás con algo como esto, pero los nombres de clase perderán su significado contextual (debido a is-a, no has-a):

Exactamente. Mira esto, fuera de contexto:

Class D : C
{
    String age;
}

¿Qué propiedades tiene una D? ¿Puedes recordar? ¿Qué pasa si complica aún más la jerarquía?

Class E : B
{
    int numberOfWheels;
}

Class F : E
{
    String favouriteColour;
}

¿Y qué pasa si, horror sobre horrores, quieres agregar un campo imageUrl a F pero no E? No se puede derivar de C y E.

He visto una situación real en la que muchos tipos de componentes diferentes tenían un nombre, un ID y una descripción. Pero esto no fue por la naturaleza de sus componentes, solo fue una coincidencia. Cada uno tenía una identificación porque estaban almacenados en una base de datos. El nombre y la descripción eran para mostrar.

Los productos también tenían una identificación, nombre y descripción, por razones similares. Pero no eran componentes, ni eran productos de componentes. Nunca verías las dos cosas juntas.

Pero, algún desarrollador había leído sobre DRY y decidió que eliminaría toda pista de duplicación del código. Así que llamó a todo producto y eliminó la necesidad de definir esos campos. Se definieron en Producto y todo lo que necesitaba esos campos podría derivarse de Producto.

No puedo decir esto con suficiente fuerza: esta no es una buena idea.

DRY no se trata de eliminar la necesidad de propiedades comunes. Si un objeto no es un Producto, no lo llame Producto. Si un Producto NO REQUIERE una Descripción, por la naturaleza misma de ser un Producto, no defina la Descripción como una Propiedad del Producto.

Sucedió, eventualmente, que teníamos Componentes sin descripciones. Entonces comenzamos a tener descripciones nulas en esos objetos. No anulable. Nulo. Siempre. Para cualquier producto de tipo X ... o posterior Y ... y N.

Esta es una violación atroz del Principio de sustitución de Liskov.

DRY se trata de nunca ponerse en una posición en la que pueda corregir un error en un lugar y olvidarse de hacerlo en otro lugar. Esto no va a suceder porque tiene dos objetos con la misma propiedad. Nunca. Así que no te preocupes por eso.

Si el contexto de las clases hace obvio que debería hacerlo, lo cual será muy raro, entonces y solo entonces debería considerar una clase principal común. Pero, si se trata de propiedades, considere por qué no está utilizando una interfaz.

pdr
fuente
4

La herencia es la forma de lograr un comportamiento polimórfico. Si sus clases no tienen ningún comportamiento, entonces la herencia es inútil. En este caso, ni siquiera los consideraría objetos / clases, sino estructuras.

Si desea poder colocar diferentes estructuras en diferentes partes de su comportamiento, considere las interfaces con métodos / propiedades get / set, como IHasName, IHasAge, etc. Esto hará que el diseño sea mucho más limpio y permita una mejor composición de estos tipo de jerarquías.

Eufórico
fuente
3

¿No es esto para lo que son las interfaces? Clases no relacionadas con diferentes funciones pero con una propiedad similar.

IName = Interface(IInterface)
  function Getname : String;
  property Name : String read Getname
end;

Class Dog(InterfacedObject, IName)
private
  FName : String;
  Function GetName : String;
Public
  Name : Read Getname;
end;

Class Movie(InterfacedObject, IName)
private
  FCount;
  FName : String;
  Function GetName : String;
Public
  Name : String Read Getname;
  Count : read FCount; 
end;
Pieter B
fuente
1

Su pregunta parece estar enmarcada en el contexto de un lenguaje que no admite herencia múltiple pero que no especifica específicamente esto. Si está trabajando con un lenguaje que admite herencia múltiple, esto se vuelve trivialmente fácil de resolver con un solo nivel de herencia.

Aquí hay un ejemplo de cómo se puede resolver esto usando la herencia múltiple.

trait Nameable { String name; }
trait Describable { String description; }
trait Countable { Integer count; }
trait HasImageUrl { String imageUrl; }
trait Living { String age; }

class A : Nameable, Describable;
class B : Nameable, Describable, Countable;
class C : Nameable, Describable, Countable, HasImageUrl;
class D : Nameable, Countable;
class E : Nameable, Countable, HasImageUrl, Living;
pgraham
fuente