为特定实例创建子类是一种不好的做法吗?


37

考虑以下设计

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

您认为这里有问题吗?对我来说,Karl和John类应该仅仅是实例而不是类,因为它们与以下内容完全相同:

Person karl = new Person("Karl");
Person john = new Person("John");

当实例足够时,为什么要创建新类?这些类不会向实例添加任何内容。


17
不幸的是,您会在许多生产代码中找到它。尽可能清理干净。
亚当·祖克曼

14
这是一个很棒的设计-如果开发人员希望使自己对于业务数据的每次更改都是必不可少的,并且没有问题可用,则是24/7 ;-)
Doc Brown

1
这是一个相关的问题,OP似乎认为这与传统观点相反。programmers.stackexchange.com/questions/253612/...
扣篮

11
根据行为而非数据来设计类。对我来说,子类没有向层次结构添加任何新行为。
Songo 2014年

2
在lambda进入Java 8之前,这已广泛用于Java中的匿名子类(例如事件处理程序)(这是​​同一语法,但语法不同)。
marczellm 2014年

Answers:


77

不需要每个人都有特定的子类。

没错,这些应该是实例。

子类的目标:扩展父类

子类用于扩展父类提供的功能。例如,您可能有:

  • 父类中Battery哪些可以Power()的东西,可以有一个Voltage特性,

  • 还有一个子类RechargeableBattery,它可以继承Power()Voltage,但也可以是Recharge()d。

请注意,您可以将RechargeableBatteryclass 的实例作为参数传递给任何接受a Battery作为参数的方法。这称为Liskov替代原理,是SOLID五个原理之一。同样,在现实生活中,如果我的MP3播放器接受两节AA电池,我可以用两节可充电AA电池代替它们。

请注意,有时很难确定是否需要使用字段或子类来表示某些内容之间的差异。例如,如果您必须处理AA,AAA和9伏电池,您将创建三个子类还是使用枚举?Martin Fowler在第232页中的“ 重构中的用字段替换子类” 可能会给您一些想法以及如何从一个迁移到另一个。

在你的榜样,KarlJohn没有扩展任何东西,也没有提供任何额外的价值:你可以有完全一样的使用功能Person直接类。没有更多的价值而拥有更多的代码行永远都不是一件好事。

商业案例的例子

为特定人员创建子类实际上有意义的商业案例是什么?

假设我们构建了一个管理公司员工的应用程序。该应用程序也管理权限,因此会计Helen无法访问SVN存储库,但是两位程序员Thomas和Mary无法访问与会计相关的文档。

大老板吉米(公司的创始人兼首席执行官)没有人享有非常特殊的特权。例如,他可以关闭整个系统或解雇一个人。你明白了。

这种应用程序最差的模型是具有以下类:

          类图显示了Helen,Thomas,Mary和Jimmy类以及它们的共同父类:抽象的Person类。

因为代码重复将很快出现。即使在最基本的四个员工示例中,您也将在Thomas和Mary类之间重复代码。这将促使您创建一个公共的父类Programmer。由于您可能还拥有多个会计师,因此您可能也会创建Accountant类。

          类图显示了一个具有三个孩子的抽象人:抽象会计师,抽象程序员和吉米。 会计有一个子类Helen,程序员有两个子类:Thomas和Mary。

现在,您注意到拥有该类Helen并没有什么用处,并且保留ThomasMary:大部分代码无论如何仍在较高级别上工作-在会计师,程序员和Jimmy级别。SVN服务器不在乎是托马斯(Thomas)还是玛丽(Mary)需要访问日志,它只需要知道它是程序员还是会计师即可。

if (person is Programmer)
{
    this.AccessGranted = true;
}

因此,您最终删除了不使用的类:

          该类图包含Accountant,Programmer和Jimmy子类,其共同的父类是:抽象Person。

您认为:“但我可以保持吉米的原样,因为总会有一位首席执行官,一位大老板吉米”。而且,Jimmy在您的代码中使用了很多,实际上看起来更像这样,而不是前面的示例:

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

这种方法的问题在于,吉米仍然会受到公交车的袭击,并且会有新的首席执行官。否则董事会可能会认为Mary很棒,因此她应该成为新的CEO,而Jimmy将会被降级为销售人员,因此,现在,您需要遍历所有代码并进行所有更改。


6
@DocBrown:我经常感到惊讶的是,简短的答案没有错,但不够深入,得分远高于深入解释该主题的答案。可能有太多人不喜欢阅读很多东西,因此简短的答案对他们来说更有吸引力。不过,我还是编辑了答案,至少提供了一些解释。
2014年

3
简短答案通常会首先发布。较早的帖子获得更多的投票。
Tim B

1
@TimB:我通常从中等大小的答案开始,然后将它们编辑得非常完整(这是一个示例,假设可能有大约15个修订,仅显示了4个)。我注意到随着时间的流逝,答案越长,得到的投票就越少;一个类似的问题仍然很简短,吸引了更多的支持。
2014年

同意@DocBrown。例如,一个很好的答案将说出类似“ 行为的子类,而不是内容的子类”之类的东西,并引用一些我在此注释中不会做的引用。
user949300 2014年

2
如果上一段还不清楚:即使您只有1个人具有不受限制的访问权限,您仍应将该人作为实现无限制访问权限的单独类的实例。检查人员本身会导致代码-数据交互,并可能打开一整串蠕虫。例如,如果董事会决定任命一个三人制为首席执行官怎么办?如果您没有“不受限制”的类,则不仅需要更新现有CEO,还需要为其他成员添加2张支票。如果确实有,只需将它们设置为“不受限制”即可。
Nzall 2014年

15

使用这种类结构只是为了改变细节,这显然是实例上的可设置字段,这是很愚蠢的。但这是您的示例所特有的。

设置不同Person的不同类几乎肯定是个坏主意-每当新人进入您的域时,您都必须更改程序并重新编译。但是,这并不意味着从现有的类继承,以便在某个地方返回不同的字符串有时不是有用的。

不同之处在于您期望该类表示的实体如何变化。人们几乎总是假定人们来去去去,并且几乎每一个与用户打交道的严肃应用程序都有望在运行时添加和删除用户。但是,如果您对诸如不同的加密算法之类的模型进行建模,则无论如何支持新的加密算法可能都是一个重大更改,并且发明一个新的类,其myName()方法返回不同的字符串(并且其perform()方法可能执行某些操作)是有意义的。


12

当实例足够时,为什么要创建新类?

在大多数情况下,您不会。您的示例确实是一个很好的例子,其中这种行为不会增加实际价值。

它也违反了Open Closed原则,因为子类基本上不是扩展,而是修改了父级的内部工作原理。此外,父级的公共构造函数现在在子类中令人不快,因此API的可理解性降低了。

但是,有时,如果您只有一个或两个在整个代码中经常使用的特殊配置,则仅子类化具有复杂构造函数的父级有时会更方便且耗时更少。在这种特殊情况下,我看不到这种方法有什么问题。我们称它为构造函数currying j / k


我不确定我是否同意OCP段落。如果一个类被暴露属性setter其后代则-假定它遵循良好的设计principles-的属性应该成为其内部运作的一部分
本·阿伦森

@BenAaronson只要不更改属性本身的行为,您就不会违反OCP,但是在这里,它们会违反。当然,您可以在其内部使用该属性。但是您不应该改变现有的行为!您应该仅扩展行为,而不能通过继承对其进行更改。当然,每条规则都有例外。
猎鹰

1
那么,使用虚拟成员是否违反了OCP?
Ben Aaronson

3
@Falcon不必派生新类只是为了避免重复的复杂构造函数调用。只需为常见情况创建工厂并参数化小的变化即可。
Keen 2014年

@BenAaronson我要说的是,具有实际实现的虚拟成员是一种违反行为。抽象成员或说什么都不做的方法将是有效的扩展点。基本上,当您查看基类的代码时,您知道它将按原样运行,而不是将每个非最终方法都替换为完全不同的方法。或者换句话说:继承类可以影响基类的行为的方式将是明确的,并且被限制为“加性”。
millimoose 2014年

8

如果这是惯例的程度,那么我同意这是一个不好的惯例。

如果约翰和卡尔的行为不同,那么情况会有所改变。可能person有一种方法cleanRoom(Room room)可以证明约翰是一个很棒的室友,而且打扫非常有效,但卡尔却不是,也不打扫房间。

在这种情况下,有意义的是让它们成为定义了行为的自己的子类。有更好的方法可以实现同一件事(例如,一个CleaningBehavior类),但是至少这种方法并不是对OO原则的可怕违反。


没有不同的行为,只是不同的数据。谢谢。
伊格纳西奥·索勒·加西亚

6

在某些情况下,您知道只会有一定数量的实例,然后使用这种方法就可以了(虽然我觉得很丑)。如果语言允许,最好使用枚举。

Java示例:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

已经有很多人回答了。以为我会发表自己的个人看法。


曾几何时,我致力于(现在仍在)创作音乐的应用程序。

该应用程序有一个抽象Scale类与几个子类:CMajorDMinor,等Scale看上去像这样:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

音乐生成器与特定Scale实例一起生成音乐。用户可以从列表中选择一个音阶来生成音乐。

有一天,我想到一个很酷的主意:为什么不让用户创建自己的秤?用户将从列表中选择注释,按一个按钮,新的音阶将被添加到列表中可用的音阶中。

但是我无法做到这一点。那是因为所有的比例尺已经在编译时设置了-因为它们表示为类。然后打我:

考虑“超类和子类”通常很直观。几乎所有内容都可以通过该系统表示:超类Person和子类John以及Mary; 超类Car和子类Volvo以及Mazda; 超类Missile和子类SpeedRockedLandMineTrippleExplodingThingy

用这种方式思考是很自然的,尤其是对于OO相对较新的人。

但是我们应该永远记住,类是模板对象是被充入这些模板的内容。您可以将所需的任何内容倒入模板中,从而创造了无数种可能性。

填充模板不是子类的工作。这是对象的工作。子类的工作是添加实际功能扩展模板

这就是为什么我应该创建一个Scale带有Note[]字段的具体类,并让对象填充此模板的原因;可能通过构造函数之类的东西。最终,我做到了。

每次在类中设计模板时(例如,Note[]需要填充一个空成员,或者String name需要为一个字段分配值),请记住,填写模板是该类对象的工作(或可能创建这些对象的对象)。子类用于添加功能,而不是填充模板。


您可能会像您一样尝试创建“超类Person,子类JohnMary”类系统,因为您喜欢这样的形式性。

这样,您可以只说Person p = new Mary()而不是Person p = new Person("Mary", 57, Sex.FEMALE)。它使事情更有条理,更有条理。但是正如我们所说,为每种数据组合创建一个新类并不是一个好方法,因为它会使代码变得毫无用处并限制了运行时能力。

因此,这是一个解决方案:使用基本工厂,甚至可能是静态工厂。像这样:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

这样,您可以轻松地使用“预置”和“程序附带”,如下所示:Person mary = PersonFactory.createMary(),但是您还保留动态设计新人员的权利,例如,在您希望允许用户这样做的情况下。例如:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

甚至更好:做这样的事情:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

我刚刚分心了。我想你应该已经明白了。


子类无意填写其超类设置的模板。子类用于添加功能对象是用来填充模板的,这就是它们的用途。

您不应该为每种可能的数据组合创建一个新类。(就像我不应该ScaleNotes的每种可能的组合创建一个新的子类一样)。

她的指导方针是:每当您创建一个新的子类时,都要考虑它是否添加了超类中不存在的任何新功能。如果该问题的答案为“否”,则可能是您试图“填充”超类的模板,在这种情况下,只需创建一个对象即可。(也许还有一个带有“预设”的工厂,以使生活更轻松)。

希望能有所帮助。


这是一个很好的例子,非常感谢。
Ignacio Soler Garcia

@SoMoS很高兴我可以提供帮助。
阿维夫·科恩

2

这是此处“应为实例”的例外-尽管有些极端。

如果希望编译器根据是Karl还是John(在此示例中)来强制访问方法的权限,而不是要求方法实现检查已传递哪个实例,则可以使用不同的类。通过强制执行不同的类而不是实例化,可以在编译时(而不是运行时)对“ Karl”和“ John”进行区分检查,这可能很有用-特别是在安全性上下文中,编译器可能比运行时更受信任(例如)外部库的代码检查。


2

复制代码被认为是不好的做法

这至少部分是因为代码行以及代码库的大小与维护成本和缺陷相关

这是我针对特定主题的相关常识逻辑:

1)如果我需要更改此设置,我需要参观几个地方?少即是好。

2)是否有这样做的理由?在您的示例中-不可能-但是在某些示例中,这样做可能有某种原因。我可以想到的是某些框架,例如早期的Grails,在这些框架中,继承不一定与GORM的某些插件很好地兼容。几乎没有。

3)这样更容易理解吗?再一次,在您的示例中不是这样-但是有些继承模式可能更多,实际上,分离的类实际上更易于维护(想到的是命令或策略模式的某些用法)。


1
不管是不是坏事,都不会一成不变。例如,在某些情况下,对象创建和方法调用的额外开销可能会对性能造成不利影响,以致应用程序不再符合规范。
jwenting 2014年

参见第2点。当然有原因,偶尔。这就是为什么您需要在删除某人的代码之前先询问一下。
dcgregorya 2014年

0

有一个用例:形成对函数或方法的引用作为常规对象。

您可以在Java GUI代码中看到很多:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

编译器通过创建新的匿名子类为您提供帮助。

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.