¿Por qué heredar una clase y no agregar propiedades?


39

Encontré un árbol de herencia en nuestra base de código (bastante grande) que se parece a esto:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class OrderDateInfo : NamedEntity { }

Por lo que pude reunir, esto se usa principalmente para unir cosas en el front-end.

Para mí, esto tiene sentido ya que le da un nombre concreto a la clase, en lugar de confiar en el genérico NamedEntity. Por otro lado, hay una serie de tales clases que simplemente no tienen propiedades adicionales.

¿Hay alguna desventaja en este enfoque?


25
Al revés no se menciona: puede distinguir métodos que solo son relevantes para OrderDateInfos de aquellos que son relevantes para otros NamedEntitys
Caleth

8
Por la misma razón que si se pasó a tener, digamos, una Identifierclase que no tiene nada que ver con NamedEntity, pero que requieren tanto una Idy Namela propiedad, que no se usará NamedEntityen su lugar. El contexto y el uso adecuado de una clase es más que solo las propiedades y métodos que posee.
Neil

Respuestas:


15

Para mí, esto tiene sentido ya que le da un nombre concreto a la clase, en lugar de confiar en la NamedEntity genérica. Por otro lado, hay una serie de tales clases que simplemente no tienen propiedades adicionales.

¿Hay alguna desventaja en este enfoque?

El enfoque no es malo, pero hay mejores soluciones disponibles. En resumen, una interfaz sería una solución mucho mejor para esto. La razón principal por la cual las interfaces y la herencia son diferentes aquí es porque solo puede heredar de una clase, pero puede implementar muchas interfaces .

Por ejemplo, considere que ha nombrado entidades y entidades auditadas. Tienes varias entidades:

Oneno es una entidad auditada ni una entidad nombrada. Así de simple:

public class One 
{ }

Twoes una entidad nombrada pero no una entidad auditada. Eso es esencialmente lo que tienes ahora:

public class NamedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Two : NamedEntity 
{ }

Threees una entrada con nombre y auditada. Aquí es donde te encuentras con un problema. Puede crear una AuditedEntityclase base, pero no puede hacer que Threeherede ambos AuditedEntity y NamedEntity :

public class AuditedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : NamedEntity, AuditedEntity  // <-- Compiler error!
{ }

Sin embargo, puede pensar en una solución al AuditedEntityheredar de NamedEntity. Este es un truco inteligente para garantizar que cada clase solo necesite heredar (directamente) de otra clase.

public class AuditedEntity : NamedEntity
{
    public DateTime CreatedOn { get; set; }
    public DateTime UpdatedOn { get; set; }
}

public class Three : AuditedEntity
{ }

Esto aun funciona. Pero lo que ha hecho aquí se afirma que cada entidad auditada es inherentemente también una entidad con nombre . Lo que me lleva a mi último ejemplo. Foures una entidad auditada pero no una entidad nombrada. Pero no puede dejar de Fourheredar de AuditedEntitycomo lo haría entonces NamedEntitydebido a la herencia entre AuditedEntity andNamedEntity`.

Usando la herencia, no hay forma de hacer ambas cosas Threey Fourtrabajar a menos que comience a duplicar clases (lo que abre un nuevo conjunto de problemas).

Usando interfaces, esto se puede lograr fácilmente:

public interface INamedEntity
{
    int Id { get; set; }
    string Name { get; set; }
}

public interface IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class One 
{ }

public class Two : INamedEntity 
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Three : INamedEntity, IAuditedEntity
{
    public int Id { get; set; }
    public string Name { get; set; }
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

public class Four : IAuditedEntity
{
    DateTime CreatedOn { get; set; }
    DateTime UpdatedOn { get; set; }
}

El único inconveniente menor aquí es que aún tiene que implementar la interfaz. Pero obtiene todos los beneficios de tener un tipo reutilizable común, sin ninguno de los inconvenientes que surgen cuando necesita variaciones en múltiples tipos comunes para una entidad determinada.

Pero tu polimorfismo permanece intacto:

var one = new One();
var two = new Two();
var three = new Three();
var four = new Four();

public void HandleNamedEntity(INamedEntity namedEntity) {}
public void HandleAuditedEntity(IAuditedEntity auditedEntity) {}

HandleNamedEntity(one);    //Error - not a named entity
HandleNamedEntity(two);
HandleNamedEntity(three);  
HandleNamedEntity(four);   //Error - not a named entity

HandleAuditedEntity(one);    //Error - not an audited entity
HandleAuditedEntity(two);    //Error - not an audited entity
HandleAuditedEntity(three);  
HandleAuditedEntity(four);

Por otro lado, hay una serie de tales clases que simplemente no tienen propiedades adicionales.

Esta es una variación en el patrón de la interfaz del marcador , donde implementa una interfaz vacía únicamente para poder usar el tipo de interfaz para verificar si una clase determinada está "marcada" con esta interfaz.
Está utilizando clases heredadas en lugar de interfaces implementadas, pero el objetivo es el mismo, por lo que me referiré a ella como una "clase marcada".

A primera vista, no hay nada malo con las interfaces / clases de marcadores. Son sintácticamente y técnicamente válidos, y no existen inconvenientes inherentes a su uso siempre que el marcador sea universalmente verdadero (en tiempo de compilación) y no condicional .

Así es exactamente cómo debe diferenciar entre diferentes excepciones, incluso cuando esas excepciones no tienen propiedades / métodos adicionales en comparación con el método base.

Por lo tanto, no hay nada inherentemente malo en hacerlo, pero recomendaría usar esto con precaución, asegurándome de que no solo estés tratando de ocultar un error arquitectónico existente con polimorfismo mal diseñado.


44
"solo puede heredar de una clase, pero puede implementar muchas interfaces". Sin embargo, eso no es cierto en todos los idiomas. Algunos idiomas permiten la herencia de clases múltiples.
Kenneth K.

Como dijo Kenneth, dos lenguajes bastante populares tienen la capacidad de herencia múltiple, C ++ y Python. La clase threeen su ejemplo de las dificultades de no usar interfaces es realmente válida en C ++, por ejemplo, de hecho, esto funciona correctamente para los cuatro ejemplos. De hecho, todo esto parece ser una limitación de C # como lenguaje en lugar de una historia de advertencia de no usar la herencia cuando debería usar interfaces.
cuando

63

Esto es algo que uso para evitar que se use el polimorfismo.

Supongamos que tiene 15 clases diferentes que tienen NamedEntitycomo clase base en algún lugar de su cadena de herencia y está escribiendo un nuevo método que solo es aplicable OrderDateInfo.

Usted "podría" simplemente escribir la firma como

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

Y espero y ore, nadie abusa del sistema de tipos para empujar FooBazNamedEntityallí.

O "podrías" simplemente escribir void MyMethod(OrderDateInfo foo). Ahora eso es impuesto por el compilador. Simple, elegante y no se basa en que las personas no cometan errores.

Además, como señaló @candied_orange , las excepciones son un gran caso de esto. Muy raramente (y quiero decir muy, muy, muy raramente) alguna vez quieres atrapar todo catch (Exception e). Es más probable que desee detectar una SqlExceptiono una FileNotFoundExceptionexcepción personalizada para su aplicación. Esas clases a menudo no proporcionan más datos o funcionalidad que la Exceptionclase base , pero le permiten diferenciar lo que representan sin tener que inspeccionarlos y verificar un campo de tipo o buscar texto específico.

En general, es un truco obtener el sistema de tipos que le permita utilizar un conjunto de tipos más estrecho que si utilizara una clase base. Quiero decir, podrías definir todas tus variables y argumentos como teniendo el tipo Object, pero eso solo haría tu trabajo más difícil, ¿no?


44
Disculpe mi novedad, pero tengo curiosidad por qué no sería un mejor enfoque hacer que OrderDateInfo TENGA un objeto NamedEntity como una propiedad.
Adam B

9
@AdamB Esa es una opción de diseño válida. Si es mejor o no depende de para qué se utilizará, entre otras cosas. En el caso de excepción, no puedo crear una nueva clase con una propiedad de excepción porque entonces el lenguaje (C # para mí) no me deja lanzarla. Puede haber otras consideraciones de diseño que impidan el uso de la composición en lugar de la herencia. Muchas veces la composición es mejor. Solo depende de tu sistema.
Becuzz

17
@AdamB Por la misma razón que cuando heredas de una clase base y agregas métodos o propiedades que la base carece, no haces una nueva clase y pones la base como una propiedad. Hay una diferencia entre un ser humano un mamífero y tener un mamífero.
Logarr

8
@Logarr es cierto que hay una diferencia entre ser un mamífero y tener un mamífero. Pero si me mantengo fiel a mi mamífero interno, nunca sabrás la diferencia. Quizás se pregunte por qué puedo darle tantos tipos diferentes de leche por capricho.
candied_orange

55
@ AdamB Puedes hacer eso, y esto se conoce como composición sobre herencia. Pero cambiaría el nombre NamedEntityde esto, por ejemplo, a EntityName, para que la composición tenga sentido cuando se describe.
Sebastian Redl

28

Este es mi uso favorito de la herencia. Lo uso principalmente para excepciones que podrían usar nombres mejores y más específicos.

El problema habitual de la herencia que conduce a largas cadenas y causa el problema del yoyo no se aplica aquí ya que no hay nada que lo motive a encadenar.


1
Hmm, buen punto re excepciones. Iba a decir que era algo horrible de hacer. Pero me has convencido de lo contrario con un excelente caso de uso.
David Arno

Yo-Yo Ho y una botella de ron. Es raro que el orlop sea yar. A menudo se filtra por falta de roble adecuado entre las tablas. Al casillero de Davey Jones con los lubbers que lo construyeron. TRADUCCIÓN: Es raro que una cadena de herencia sea tan buena que la clase base pueda ser ignorada de manera abstracta / efectiva.
radarbob

Creo que es importante señalar que una nueva clase que hereda de otra no añadir una nueva propiedad - el nombre de la nueva clase. Esto es exactamente lo que usa al crear excepciones con mejores nombres.
Recogida de Logan el

1

En mi humilde opinión el diseño de la clase está mal. Debería ser.

public class EntityName
{
    public int Id { get; set; }
    public string Text { get; set; }
}

public class OrderDateInfo
{
    public EntityName Name { get; set; }
}

OrderDateInfoHA A Namees una relación más natural y crea dos clases fáciles de entender que no habrían provocado la pregunta original.

Cualquier método que aceptó NamedEntitycomo un parámetro sólo debería haber sido interesado en el Idy Namepropiedades, así que cualquiera de tales métodos se deben cambiar para aceptar EntityNameen su lugar.

La única razón técnica que aceptaría para el diseño original es para vincular la propiedad, que el OP mencionó. Un marco de basura no podría hacer frente a la propiedad adicional y unirse object.Name.Id. Pero si su marco vinculante no puede hacer frente a eso, entonces tiene que agregar más deuda tecnológica a la lista.

Yo estaría de acuerdo con la respuesta de @ Flater, pero con muchas interfaces que contienen propiedades, terminas escribiendo mucho código repetitivo, incluso con las encantadoras propiedades automáticas de C #. ¡Imagínate hacerlo en Java!


1

Las clases exponen el comportamiento y las estructuras de datos exponen los datos.

Veo las palabras clave de la clase, pero no veo ningún comportamiento. Esto significa que comenzaría a ver esta clase como una estructura de datos. En este sentido, voy a reformular su pregunta como

¿Por qué tener una estructura de datos de nivel superior común?

Entonces puede usar el tipo de datos de nivel superior. Esto permite aprovechar el sistema de tipos para garantizar una política en grandes conjuntos de diferentes estructuras de datos al garantizar que todas las propiedades estén allí.

¿Por qué tener una estructura de datos que incluye el nivel superior, pero no agrega nada?

Entonces puede usar el tipo de datos de nivel inferior. Esto permite poner pistas en el sistema de mecanografía para expresar el propósito de la variable

Top level data structure - Named
   property: name;

Bottom level data structure - Person

En la jerarquía anterior, nos parece conveniente especificar que se nombra a una Persona, para que las personas puedan obtener y modificar su nombre. Si bien puede ser razonable agregar propiedades adicionales a la Personestructura de datos, el problema que estamos resolviendo no requiere un modelado perfecto de la Persona, por lo que descuidamos agregar propiedades comunes como age, etc.

Por lo tanto, es un aprovechamiento del sistema de tipeo para expresar la intención de este elemento con nombre de una manera que no rompa con las actualizaciones (como la documentación) y pueda extenderse en una fecha posterior con facilidad (si encuentra que realmente necesita el agecampo más adelante) )

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.