【问题标题】:Declaring children type in base class; Is it bad or not?在基类中声明子类型;是坏还是不好?
【发布时间】:2013-11-29 20:39:24
【问题描述】:

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

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.
    */
    

但是,当我阅读Wikipedia's article about inheritance 时,我找不到任何问题的答案。

虽然乍一看可能有争议,但我相信继承应该足够成熟,可以为这个困境提供可靠的答案。这段代码不好吗,smelly code?还是可以接受和合理的?为什么?

【问题讨论】:

  • 我认为关键问题是它在这里是做什么用的?如果我看到 Java 中的等效代码,我会假设程序员不理解 OOP...
  • 通常是糟糕的设计。但在某些情况下(通常如果子类型集有限),它可能是一种有用的模式,类似于 F# 提供的区分联合。
  • Type 上的公共设置者几乎总是一个坏主意。我上面的评论假设一个没有 setter 的虚拟 getter,或者至少是一个受保护的 setter。
  • 我做了一个更新可能值得一看。

标签: c# oop object-oriented-analysis


【解决方案1】:

我认为当我们回到什么时候使用继承的哲学时问题就可以解决了。
据说“把继承看成一个是一个 关系

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

动态绑定和多态性的主要好处之一是它们可以帮助使代码更容易更改。如果您的代码片段使用了超类类型的变量,例如 Employee,您可以稍后创建一个全新的子类,例如 Manager,并且旧代码片段将在不更改新子类实例的情况下工作。如果 Manager 覆盖了代码片段调用的任何 Employee 方法,动态绑定将确保 Managers 对这些方法的实现得到执行。即使在编写和编译代码片段时类管理器不存在也是如此。

现在回到问题,为什么我们需要 Employee 中的 EmployeeType?
我们可能想在 Employee 中实现一些函数,AKA CalculateSalary()

CalculateSalary(){
if (EmployeeType == EmployeeType.Manager) // Add management percent }

诱人的理由,但如果员工既是经理又是主管怎么办?
它会很复杂!简单地说,我们可以将 CalculateSalary() 实现为抽象方法,但我们将失去目标!
这是 Coad 的规则:
子类表示是一种特殊的,而不是一种所扮演的角色。
Liskov Substitution 原则是测试“我是否应该从这种类型继承” ?'

以上拇指的作用是:

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

作为javaworld的脚注
确保继承对 is-a 关系建模。
不要仅仅为了代码重用而使用继承。
不要仅仅为了获得多态性而使用继承。
优先考虑组合而不是继承。

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

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

Secretary secretary = new Secretary();
secretary.EmployeeType = EmployeeType.Manager;

【讨论】:

    【解决方案2】:

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

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

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

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

    .NET BCL 在很多地方使用这种模式,例如:

    【讨论】:

    • 请注意,它们都是只读属性。这不是这里的情况。
    • @SriramSakthivel:确实,显示的代码不是很干净——我个人也会明确地将Employee 声明为abstract,并给它一个protected(或者可能是internal,以防止任何第三方继承)构造函数,以及可能使ManagerSecretarysealed。但是,我理解 OP 主要关注以 enum 类型反映子类的做法,这是 IMO 比您正确指出的 API 清洁度问题更重要的设计决策。
    • BCL 充满了糟糕的设计,例如在这种情况下以及在许多集合类中破坏了 Liskov 替换原则。不能仅仅因为 BCL 设计的不好就为自己的代码设计得不好。
    • @O.R.Mapper 每个设计良好的软件背后的核心原则是松散耦合。通过使基类依赖于其子类,可以将这些类耦合在一起。好的软件设计不是只有在方便时才应该打开的东西;它应该是您生成的每一行代码的核心。
    • @DavidArno:在我看来,软件开发的核心原则是为手头的任务选择正确的工具。有时,松散耦合是正确的工具,而在其他时候则不是。我个人的看法是,不适应实际情况,盲目遵循原则是没有意义的。
    【解决方案3】:

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

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

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

    【讨论】:

    • 在我看来,这并不能真正回答这个问题,只是“在需要依赖倒置的情况下这是不好的做法吗?”
    • @O.R.Mapper 依赖倒置是可取的总是,因为它减少了耦合。问题是,打破依赖倒置原则是糟糕代码设计的一个例子。正确答案是“是”。
    • @DavidArno,我不同意。基于Project Management Triangle,有时在现实、实际的世界中你没有足够的资源来使用它。我同意理论上总是好的。但在实践中,有时您应该为了敏捷和降低成本而忽略它。
    • @DavidArno:所以,正如您所说,依赖倒置仅在需要低耦合度的情况下才可取,因此并非在所有情况下都适用。这个问题实际上比“打破依赖倒置原则 [是] 糟糕代码设计的一个例子”是否更具体,但即使是这样,它也可能过于宽泛,无法用明确的“是”或“否”来回答。
    • @DavidArno:当然,处理任务有多种方法。一般来说,我更喜欢仔细选择合适的技术,并知道我选择它们的原因,而不是盲目地遵循它们。这既可以防止产品中不幸的设计决策,也可以防止在我没有具体论据支持我的观点时诉诸一般指控。
    【解决方案4】:

    我同意这是不好的做法,但在某些情况下,这可能是必要的或有用的,如果是这样,您至少可以通过将基类的属性设置为只读并通过子类。

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

    为防止 Employee 被错误的类型实例化,您可以将其设置为受保护的。

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

    【讨论】:

    • "但在某些情况下..." 不。如果这是不好的做法,请重写它。不要开始使用受保护的方法等来破解它。
    • @DavidArno 我不同意,这个问题太广泛了,无法正确回答。你的评论只是一个意见。例如,您可能必须使用另一个需要类似内容的组件/库,而您无法阻止这种情况的发生?那又怎样......一般来说,如果它是你的所有代码,你根本不应该做这样的事情并重构/重新设计你的代码(就像我写的那样)......
    【解决方案5】:

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

    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”次更改,现在它看起来有点“工厂模式”。可能是代码作者理解“工厂模式”错误。但是谁知道呢?

    【讨论】:

    • @Downvoter 请发表评论,以便我知道哪里错了。
    • 您添加的“未知”并不能解决您在此处创建的模式破坏了良好 OO 设计的核心方面的事实:依赖倒置原则。
    • 我认为为了防止出现不良影响,您仍然应该创建子类的Type 属性,或者子类本身,sealed。顺便说一句,我通常觉得Type 太容易与对象的(编程-).NET 类型而不是(业务逻辑)类型混淆,所以我通常将这种属性称为*Kind
    • @HenriqueBarcelos 你就在那里,另一方面,我们不能依赖CLRType 来处理所有事情。考虑EmployeeFactory.Create(employeeType); 这里employeeType 是一个枚举,它告诉Create 方法创建哪种类型的员工来创建“经理”或“劳工”等。这可以通过字符串EmployeeFactory.Create("Manager"); 轻松实现,但它看起来很丑而不是typesafe..明白我的意思了吗?
    • 是的,我明白了...我更习惯 Java 和 PHP,所以通常我会坚持使用字符串版本,如果出现任何问题,我会抛出 Exception...
    猜你喜欢
    • 2011-04-11
    • 2011-04-07
    • 2012-06-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-05
    • 2011-10-08
    相关资源
    最近更新 更多