为什么继承一个类而不添加属性?


39

我在我们的(相当大的)代码库中发现了一个继承树,如下所示:

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

public class OrderDateInfo : NamedEntity { }

据我所知,它主要用于绑定前端的东西。

对我来说,这很有意义,因为它为类提供了具体的名称,而不是依赖于generic NamedEntity。另一方面,有许多这样的类根本没有其他属性。

这种方法有什么缺点吗?


25
未提及的优势:您可以区分仅与OrderDateInfos 相关的方法与与其他NamedEntitys 相关的方法
Caleth

8
出于同样的原因,如果碰巧有一个Identifier与之无关NamedEntity但需要an IdNameproperty的类,那么您就不会使用它NamedEntity。类的上下文和正确用法不仅仅是其拥有的属性和方法。
尼尔

Answers:


15

对我来说,这很有意义,因为它为类提供了具体的名称,而不是依赖于通用的NamedEntity。另一方面,有许多这样的类根本没有其他属性。

这种方法有什么缺点吗?

这种方法还不错,但是有更好的解决方案可用。简而言之,接口将是一个更好的解决方案。这里的接口和继承不同的主要原因是因为您只能从一个类继承,但是可以实现许多接口

例如,假设您已命名实体和已审计实体。您有几个实体:

One不是审计实体,也不是命名实体。很简单:

public class One 
{ }

Two是一个命名实体,但不是审计实体。本质上就是您现在拥有的:

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

public class Two : NamedEntity 
{ }

Three既是命名条目又是经过审核的条目。这是您遇到问题的地方。您可以创建AuditedEntity基类,但不能同时Three继承 AuditedEntity NamedEntity

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

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

但是,您可能会想到通过AuditedEntity继承来解决NamedEntity。这是一个聪明的技巧,可以确保每个类仅需要(直接)从另一个类继承。

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

public class Three : AuditedEntity
{ }

这仍然有效。但是,您在这里所做的工作表明,每个审核实体在本质上也是一个命名实体。这使我想到了最后一个例子。Four是受审核实体,但不是命名实体。但是你不能让Four继承AuditedEntity因为你会也将使其成为一个NamedEntity因AuditedEntity之间的继承andNamedEntity`。

使用继承,除非您开始复制类(否则会带来一系列新的问题),否则无法同时Three进行Four工作。

使用接口,可以轻松实现:

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; }
}

这里唯一的次要缺点是您仍然必须实现接口。但是,拥有通用的可重用类型会带来所有好处,而对于给定实体需要多种通用类型的变体时,不会出现任何弊端。

但是您的多态性保持不变:

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

另一方面,有许多这样的类根本没有其他属性。

这是标记接口模式的一种变体,您在其中实现了一个空接口,纯粹是为了能够使用接口类型来检查给定的类是否已被该接口“标记”。
您使用继承的类而不是实现的接口,但是目标是相同的,因此我将其称为“标记类”。

从表面上看,标记接口/类没有错。它们在语法上和技术上都是有效的,并且只要标记是普遍正确的(在编译时)并且不是有条件的,使用它们就不会存在固有的缺点。

这就是您应该如何区分不同的异常,即使与基础方法相比,这些异常实际上没有任何其他属性/方法。

因此,这样做本质上并没有错,但是我建议您谨慎使用,以确保您不只是试图通过设计不良的多态性掩盖现有的体系结构错误。


4
“您只能从一个类继承,但可以实现许多接口。” 但是,并非每种语言都适用。某些语言确实允许多类继承。
肯尼斯·K

就像肯尼斯(Kenneth)所说的那样,两种非常流行的语言具有多重继承的能力,即C ++和Python。three例如,您的示例中的类不使用接口的陷阱实际上在C ++中是有效的,实际上,这对于所有四个示例都适用。实际上,这一切似乎都是C#语言的局限性,而不是一个警告性的故事,当您应该使用接口时不使用继承。
WHN

63

这是我用来防止使用多态性的东西。

假设您NamedEntity在继承链中某处有15个不同的类作为基类,并且您正在编写仅适用于的新方法OrderDateInfo

您“可以”将签名写为

void MyMethodThatShouldOnlyTakeOrderDateInfos(NamedEntity foo)

并希望并祈祷没有人滥用类型系统将其推FooBazNamedEntity入其中。

或者您“可以”只是写void MyMethod(OrderDateInfo foo)。现在由编译器强制执行。简单,优雅,不依赖别人没有犯错。

另外,正如@candied_orange指出的,例外是一个很好的例子。您很少(而且我的意思是非常非常非常少)想要抓住一切catch (Exception e)。您更有可能想捕获应用程序SqlExceptionFileNotFoundException或或自定义异常。这些类通常不提供比基Exception类更多的数据或功能,但是它们使您能够区分它们所代表的内容,而不必检查它们,检查类型字段或搜索特定文本。

总的来说,这是使类型系统比使用基类时允许使用更窄的类型集的一个技巧。我的意思是,您可以将所有变量和参数定义为具有type Object,但这只会使您的工作更加困难,不是吗?


4
请原谅我,但我很好奇为什么将OrderDateInfo具有NamedEntity对象作为属性不是更好的方法?
亚当B

9
@AdamB这是一个有效的设计选择。它是否更好取决于其他用途。在例外情况下,我无法创建一个具有exception属性的新类,因为那样的话(对我来说是C#)不会让我抛出它。可能还有其他设计考虑因素会阻止使用合成而不是继承。很多时候成分会更好。这仅取决于您的系统。
Becuzz

17
@AdamB出于同样的原因,当您继承基类并添加缺少基类的方法或属性时,无需创建新类并将基类作为属性。人类成为哺乳动物与拥有哺乳动物之间是有区别的。
Logarr

8
@Logarr是的,我是哺乳动物和拥有哺乳动物之间是有区别的。但是,如果我忠于内在的哺乳动物,您将永远不会知道区别。您可能想知道为什么我可以随心所欲地给这么多种牛奶。
candied_orange

5
@AdamB您可以做到这一点,这就是所谓的“继承于继承”。但是您可以为此重命名NamedEntity,例如EntityName,以便在描述时可以理解该组成。
塞巴斯蒂安·雷德尔

28

这是我最喜欢的继承用法。我主要将其用于可能使用更好,更具体的名称的异常

导致长链并导致溜溜球问题的常见继承问题在这里不适用,因为没有什么可以激发您进行链合作的。


1
嗯,好点是例外。我要说这是一件可怕的事情。但是您用一个很好的用例说服了我。
David Arno

何友友和一瓶朗姆酒。罕见的奥尔洛是亚尔。由于在木板之间缺少适当的橡木,经常会漏水。戴维·琼斯(Davey Jones)的储物柜带着土地来建造它。翻译:很少有继承链是如此之好,以至于可以抽象地/有效地忽略基类。
Radarbob

我认为值得指出的是,从另一个继承的新类确实添加了新属性-新类的名称。这正是创建具有更好名称的异常时所使用的。
Logan Pickup

1

恕我直言,班级设计是错误的。它应该是。

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

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

OrderDateInfoHAS A Name是一种更自然的关系,它创建了两个不容易引起原始问题的易于理解的类。

接受NamedEntity为参数的任何方法应该只对IdName属性感兴趣,因此应将此类方法更改为接受EntityName

我接受原始设计的唯一技术原因是属性绑定,OP提到了这一点。废话框架无法处理额外的属性并绑定到object.Name.Id。但是,如果您的绑定框架无法解决该问题,那么您还有更多的技术债务需要添加到列表中。

我会同意@Flater的回答,但是即使包含C#的可爱的自动属性,使用包含属性的许多接口,最终也要编写大量样板代码。想象一下用Java做到这一点!


1

类公开行为,数据结构公开数据。

我看到了class关键字,但是没有看到任何行为。这意味着我将开始将此类视为数据结构。以这种方式,我将把你的问题改写为

为什么具有通用的顶层数据结构?

因此,您可以使用顶级数据类型。这允许利用类型系统通过确保所有属性都存在而确保跨大量不同数据结构的策略。

为什么要有一个包含顶层但不添加任何内容的数据结构?

因此,您可以使用较低级别的数据类型。这允许在类型系统中添加提示以表达变量的目的

Top level data structure - Named
   property: name;

Bottom level data structure - Person

在上面的层次结构中,我们发现方便地指定一个人的名字,以便人们可以获取和更改其名字。虽然在Person数据结构中添加额外的属性可能是合理的,但我们要解决的问题并不需要对Person进行完美建模,因此我们忽略了添加诸如age等的公共属性。

因此,这是利用打字系统来表达此命名项目的意图的一种方式,该意图不会随着更新而中断(例如文档),并且可以在以后的某个日期轻松扩展(如果您age以后确实需要该字段)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.