宣告孩子输入基类;是不是坏了?

时间:2013-11-16 12:08:48

标签: c# oop object-oriented-analysis

最近我遇到了一些代码,它们将子类型声明为基类中的枚举。这是一个简单的例子:

public enum EmployeeType
{
    Manager,
    Secretary
}

public class Employee
{
    public string Code { get; set; }

    public EmployeeType Type { get; set; }
}

public class Manager : Employee
{
    public void Manage()
    {
        // Managing
    }
}

public class Secretary : Employee
{
    public void SetMeeting()
    {
        // Setting meeting
    }
}

根据我的开发经验,我写了关于它的an article,声明这是一个不好的做法/设计。我认为这很糟糕,因为基类应该与其子类无关。它应该没有关于其子类的信息,这至少有两个原因:

  1. 可扩展性:此设计不可扩展,因为如果您想要定义另一个派生类,例如Developer,您还应该更新EmployeeType枚举,您可能没有访问。
  2. 矛盾的定义:现在你可以编写这段代码:

    Secretary secretary = new Secretary();
    secretary.EmployeeType = EmployeeType.Manager;
    /*
    This is absurd semantically. But syntactically, it's possible.
    */
    
  3. 但是,当我阅读Wikipedia's article about inheritance时,我找不到任何问题的答案。

    虽然乍一看可能听起来有争议,但我相信继承应该足够成熟,以便为这种困境找到可靠的答案。这段代码不好,smelly code?或者它是否可以接受并且合理?为什么呢?

6 个答案:

答案 0 :(得分:7)

我认为当我们回到何时使用继承的哲学时,问题就可以得到解决。
“人们认为”将继承视为是一个关系

当你在两个类之间建立继承关系时,你可以利用动态绑定和多态。
动态绑定意味着在运行时调用哪个方法实现,基于对象的类。
多态性意味着您可以使用超类类型的变量来保存对其类是超类或其任何子类的对象的引用。

动态绑定和多态的一个主要好处是它们可以帮助使代码更容易更改。如果您有一个使用超类类型变量的代码片段(例如Employee),您可以稍后创建一个全新的子类,例如Manager,并且旧代码片段可以在不更改新子类的实例的情况下工作。如果Manager覆盖由代码片段调用的任何Employee方法,则动态绑定将确保执行这些方法的Managers的实现。即使编写和编译代码片段时类管理器不存在,也是如此 现在回到问题,为什么我们需要员工中的EmployeeType?
我们希望在Employee,AKA CalculateSalary()

中实现一些功能
CalculateSalary(){
if (EmployeeType == EmployeeType.Manager) // Add management percent }

诱人的原因,但如果员工既是经理又是主管怎么办? 它会变得复杂!我们只需将CalculateSalary()实现为抽象方法,但我们将丢失目标!
这是Coad的规则:
子类表示是一种特殊的表示不是所扮演的角色。
Liskov Substitution原则是对“我是否应该继承这种类型?”的测试 拇指在上面的作用是:

  • TypeB是否要公开完整的界面(所有公共方法 同样,TypeA可以在TypeA所在的地方使用TypeB 预期?表示继承。
    例如塞斯纳双翼飞机将暴露出来 飞机的完整界面,如果不是更多。这就是它 适合从飞机派生。
  • TypeB是否只需要TypeA公开的部分/部分行为? 表示需要合成。
    例如鸟可能只需要苍蝇 飞机的行为。在这种情况下,提取它是有意义的 out作为接口/ class / both并使其成为两者的成员 类。

来自javaworld 的脚注:
确保继承模型是is-a关系。
不要仅仅为了获得代码重用而使用继承 不要仅仅为了获得多态性而使用继承 赞成组合超过继承。

我认为在这样的情况下,作为经理是员工扮演的角色,角色可能会随着时间的推移而改变,角色将作为一个组合来实现。

作为域模型中视图的另一个方面,并将其映射到关系数据库。将继承映射到SQL模型真的很难(最终会创建不常用的列,使用视图等)。一些ORM试图解决这个问题,但它总是很快变得复杂。
例如Hibernate的 One Table Per Class Hierarchy 方法,违反2NF并且可能导致不一致

的样本
Secretary secretary = new Secretary();
secretary.EmployeeType = EmployeeType.Manager;


答案 1 :(得分:5)

这绝对是不好的做法。暂且不谈使用继承(这本身被广泛视为不良实践,与使用组合相比),它通过在父类和子类之间引入令人讨厌的循环依赖来打破Dependency inversion principle

在重新设计代码时,我希望解决三个问题:

  1. 我将删除循环依赖项,方法是从基类中删除枚举。
  2. 我让员工成为一个界面。
  3. 我会考虑删除公共设置器,它允许应用程序的任何方面修改员工记录的核心方面,使测试和跟踪错误远比他们需要的更困难。

答案 2 :(得分:4)

听起来很糟糕“代码味道”,我会说糟糕的设计。但我通常做这样的事情,但我认为这是可以接受的。

public enum EmployeeType
{
    UnKnown,//this can be used for extensiblity
    Manager,
    Secretary,    
}

public abstract class Employee//<--1
{
    public string Code { get; set; }

    public abstract EmployeeType Type { get; } //<--2
}

public class Manager : Employee
{
    public override EmployeeType Type { get { EmployeeType.Manager; } }//<--3
    public void Manage()
    {
        // Managing
    }
}

public class Secretary : Employee
{
    public override EmployeeType Type { get { EmployeeType.Secretary; } }//<--4
    public void SetMeeting()
    {
        // Setting meeting
    }
}

然后是它的工厂..

EmployeeFactory.Create(employeeType);

我在原始代码中进行了“4”更改,现在它看起来有点像“工厂模式”。可能是代码的作者理解“工厂模式”错误。但是谁知道呢?

答案 3 :(得分:2)

我同意这是不好的做法,但在某些情况下这可能是必需的或有用的,如果是这样,你至少可以通过使基类的属性只读并通过ctor的设置来防止发生不良事件。子类。

public enum EmployeeType
{
    Manager,
    Secretary
}

public class Employee
{
    public Employee(EmployeeType type)
    {
        this.Type = type;
    }

    public string Code { get; set; }

    public EmployeeType Type { get; private set; }
}

public class Manager : Employee
{
    public Manager()
        : base(EmployeeType.Manager)
    {
    }

    public void Manage()
    {
        // Managing
    }
}

public class Secretary : Employee
{
    public Secretary()
        : base(EmployeeType.Secretary)
    {
    }

    public void SetMeeting()
    {
        // Setting meeting
    }
}

要防止员工使用错误的类型进行实例化,您可以使其受到保护。

一般来说,这个问题非常广泛,这意味着它实际上取决于整体设计,我会尝试通过重构/重新设计期望Type属性的类结构和/或逻辑来防止这种情况发生。

答案 4 :(得分:1)

从问题看来,EmployeeType匹配一个班级。在这种情况下,简单来说不是答案,那种类型的员工是班级的类型。它根本不应该是一个属性,并且拥有类型的枚举似乎毫无意义。列表或数组可能,但不是枚举。确切的细节完全取决于用例和整体设计。或者,在第二个想法中,可能应该有一个类型的只读属性,它给出了公共的,可能是抽象的类型,即使真实类型的雇员实例是私有的。

然后工厂方法应该采用员工类类型。请注意,API中可见的类型可以是抽象的,工厂方法可以在内部选择确切的具体类型。此外,采用字符串类型的工厂方法版本可能很有用。


但是,如果EmployeeType与具有相同名称的类不对应,那么答案会略有不同。首先,你应该使用不同的命名,这样就不会有与枚举和类混淆的风险。 2,将EmployeeType转换为类或接口可能是值得的。然后,员工拥有类型,而不是 类型。构成而不是继承。

答案 5 :(得分:0)

我认为这对于某些场景来说是一个很好的设计,它肯定是在.NET基类库的各个部分中看到的反复出现的模式。

我认为暗示它可能是糟糕设计的误解是类层次结构必须是可扩展的。但是,首先,继承关系只是表示某个子类型是超类型的特化,并且子类型与超类型不同,但与超类型的(接口)兼容。

通过继承相关的一系列类型 not 必然为第三方提供添加任何其他类型的任何可能性。当这种类型族表示标准中定义的数据结构时,这通常是但不是唯一的。

至于表达具有enum值的当前类的类型,如果继承层次结构仅限于第三方的扩展,则没有任何说法。严格地说,这样的属性不应该是必要的,但是像C#这样的语言不提供切换类型的任何可能性,提供enum值可以提高必须处理的代码的可读性不同类型的类型系列因此构成了良好的API设计。在这方面,它还提高了可维护性,因为同一类型系列的未来版本可能会重构类型层次结构。专门针对enum值进行检查的代码将与此类更改无关(并且容易出错的解决方法,例如检查(string)类型名称而不是Type对象本身不会首先介绍。)

.NET BCL在各个地方使用此模式,例如: