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?
object-oriented-design
inheritance
robotron
fuente
fuente
OrderDateInfo
s de aquellos que son relevantes para otrosNamedEntity
sIdentifier
clase que no tiene nada que ver conNamedEntity
, pero que requieren tanto unaId
yName
la propiedad, que no se usaráNamedEntity
en su lugar. El contexto y el uso adecuado de una clase es más que solo las propiedades y métodos que posee.Respuestas:
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:
One
no es una entidad auditada ni una entidad nombrada. Así de simple:Two
es una entidad nombrada pero no una entidad auditada. Eso es esencialmente lo que tienes ahora:Three
es una entrada con nombre y auditada. Aquí es donde te encuentras con un problema. Puede crear unaAuditedEntity
clase base, pero no puede hacer queThree
herede ambosAuditedEntity
yNamedEntity
:Sin embargo, puede pensar en una solución al
AuditedEntity
heredar deNamedEntity
. Este es un truco inteligente para garantizar que cada clase solo necesite heredar (directamente) de otra clase.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.
Four
es una entidad auditada pero no una entidad nombrada. Pero no puede dejar deFour
heredar deAuditedEntity
como lo haría entoncesNamedEntity
debido a la herencia entre AuditedEntityand
NamedEntity`.Usando la herencia, no hay forma de hacer ambas cosas
Three
yFour
trabajar a menos que comience a duplicar clases (lo que abre un nuevo conjunto de problemas).Usando interfaces, esto se puede lograr fácilmente:
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:
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.
fuente
three
en 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.Esto es algo que uso para evitar que se use el polimorfismo.
Supongamos que tiene 15 clases diferentes que tienen
NamedEntity
como clase base en algún lugar de su cadena de herencia y está escribiendo un nuevo método que solo es aplicableOrderDateInfo
.Usted "podría" simplemente escribir la firma como
void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)
Y espero y ore, nadie abusa del sistema de tipos para empujar
FooBazNamedEntity
allí.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 unaSqlException
o unaFileNotFoundException
excepción personalizada para su aplicación. Esas clases a menudo no proporcionan más datos o funcionalidad que laException
clase 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?fuente
NamedEntity
de esto, por ejemplo, aEntityName
, para que la composición tenga sentido cuando se describe.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.
fuente
En mi humilde opinión el diseño de la clase está mal. Debería ser.
OrderDateInfo
HA AName
es 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ó
NamedEntity
como un parámetro sólo debería haber sido interesado en elId
yName
propiedades, así que cualquiera de tales métodos se deben cambiar para aceptarEntityName
en 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!
fuente
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
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í.
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
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
Person
estructura de datos, el problema que estamos resolviendo no requiere un modelado perfecto de la Persona, por lo que descuidamos agregar propiedades comunes comoage
, 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
age
campo más adelante) )fuente