仅限构造方法的子类:这是反模式吗?


37

我当时正在与一位同事进行讨论,但最终我们对继承的目的产生了矛盾的直觉。我的直觉是,如果子类的主要功能是表达其父级的可能值的有限范围,则它可能不应该是子类。他主张相反的直觉:子类化表示对象的“特定性”更高,因此子类关系更合适。

更具体地讲,我认为如果我有一个扩展父类的子类,但是该子类覆盖的唯一代码是构造函数(是的,我知道构造函数通常不会“重写”,请耐心等待),然后真正需要的是一种辅助方法。

例如,考虑一下这个现实生活的类:

public class DataHelperBuilder
{
    public string DatabaseEngine { get; set; }
    public string ConnectionString { get; set; }

    public DataHelperBuilder(string databaseEngine, string connectionString)
    {
        DatabaseEngine = databaseEngine;
        ConnectionString = connectionString;
    }

    // Other optional "DataHelper" configuration settings omitted

    public DataHelper CreateDataHelper()
    {
        Type dataHelperType = DatabaseEngineTypeHelper.GetType(DatabaseEngine);
        DataHelper dh = (DataHelper)Activator.CreateInstance(dataHelperType);
        dh.SetConnectionString(ConnectionString);

        // Omitted some code that applies decorators to the returned object
        // based on omitted configuration settings

        return dh;
    }
}

他声称拥有这样的子类是完全合适的:

public class SystemDataHelperBuilder
{
    public SystemDataHelperBuilder()
        : base(Configuration.GetSystemDatabaseEngine(),
               Configuration.GetSystemConnectionString())
    {
    }
 }

因此,问题是:

  1. 在谈论设计模式的人们中,哪些直觉是正确的?如上所述的子类化是反模式吗?
  2. 如果是反模式,它的名字是什么?

如果这是一个易于搜索的答案,我深表歉意。我在Google上的搜索大都返回了有关伸缩构造函数反模式的信息,而不是我真正想要的信息。


2
我认为名称中的“ Helper”一词是反模式。就像阅读一篇不断引用事物和whatsit的文章一样。说什么。是工厂吗?构造函数?对我来说,它充满了懒惰的思考过程,并且鼓励违反单一责任原则,因为适当的语义名称将指导它。我同意其他选民的意见,你应该接受@lxrec的回答。为工厂方法创建子类是完全没有意义的,除非您不信任用户传递文档中指定的正确参数。在这种情况下,请获取新用户。
亚伦·霍尔

我绝对同意@Ixrec的回答。毕竟,他的回答说我一直都是对的,而我的同事是错的。;-)但是,认真的,这就是为什么我觉得很奇怪。我以为我可以被指控有偏见。但是我是程序员StackExchange的新手。如果我确信这不会违反礼节,我将愿意接受。
HardlyKnowEm 2015年

1
不,希望您接受您认为最有价值的答案。到目前为止,社区认为最有价值的一个是Ixrec,其比值超过3:1。因此,他们希望您接受它。在这个社区中,发问者接受错误答案的挫败感很少,但是从未接受答案的发问者却表现出极大的挫败感。请注意,这里没有人抱怨被接受的答案,相反,他们抱怨投票似乎正在纠正自己:stackoverflow.com/q/2052390/541136
Aaron Hall

很好,我会接受的。感谢您抽出宝贵的时间在这里教育我有关社区标准的知识。
HardlyKnowEm 2015年

Answers:


54

如果您要做的只是使用某些参数创建类X,则子类化是表达此意图的一种奇怪方法,因为您没有使用任何类和继承赋予您的功能。它并不是真正的反模式,只是很奇怪,没有意义(除非您有其他原因)。表达此意图的更自然的方法是Factory方法,在这种情况下,这是您的“帮助方法”的奇特名称。

关于一般直觉,“更具体”和“有限范围”都是潜在的有害考虑子类的方式,因为它们都暗示将Square作为Rectangle的子类是一个好主意。在不依赖诸如LSP之类的形式的情况下,我想说一个更好的直觉是,子类要么提供基本接口的实现,要么扩展接口以添加一些新功能。


2
但是,工厂方法不允许您在代码库中强制执行某些行为(由参数控制)。有时,明确注释该类/方法/字段采用SystemDataHelperBuilder特定含义很有用。
Telastyn

3
啊,我认为方形与矩形参数正是我想要的。谢谢!
HardlyKnowEm 2015年

7
当然,如果正方形是不变的,那么将正方形作为矩形的子类也可以。您确实必须查看LSP。
戴维·康拉德

10
关于正方形/矩形“问题”的是几何形状是不可变的。您始终可以在矩形有意义的地方使用正方形,但是调整大小并不是正方形或矩形所做的事情。
2015年

3
@nha LSP = Liskov替代原则
Ixrec

16

以下哪种直觉是正确的?

您的同事是正确的(假设使用标准类型系统)。

考虑一下,类代表可能的法律价值。如果class A具有一个字节字段F,则您可能会倾向于认为它A具有256个合法值,但是这种直觉是不正确的。A将“每个值的永远排列”限制为“必须将字段设置F为0-255”。

如果扩展AB它有另一个字节的字段FF,即增加限制。而不是“值在F字节中”,而是“值在F字节中&& FF也是字节”。由于您保留了旧规则,因此适用于基类型的所有内容仍适用于您的子类型。但是,由于要添加规则,因此您将进一步远离“可能是任何东西”。

或者想换一种方式,假设A有一堆亚型:BC,和D。类型的变量A可以是这些子类型中的任何一个,但是类型的变量B更具体。(在大多数类型的系统中)它不能是a C或a D

这是反模式吗?

恩 具有专用对象是有用的,并且对代码没有明显的危害。通过使用子类型来实现该结果可能会有些疑问,但是取决于您拥有什么工具。即使使用更好的工具,该实现也简单,直接且可靠。

我不认为这是一种反模式,因为在任何情况下它都不是明显的错误和坏处。


5
“更多的规则意味着更少的可能值,意味着更加具体。” 我真的不遵循这个。类型byte and byte肯定比类型具有更多的价值byte; 256倍。除此之外,我认为您不能将规则与元素数量(或基数,如果您想精确的话)混为一谈。当您考虑无限集时,它会崩溃。偶数和整数一样多,但是知道一个偶数仍然是一个限制,这比仅仅知道它是整数给我更多的信息!自然数也可以这样说。
2015年

6
@Telastyn我们正在摆脱话题,但可以证明偶数整数集的基数(松散地为“大小”)与所有整数甚至所有有理数的集合相同。这真的是很酷的东西。参见math.grinnell.edu/~miletijo/museum/infinite.html
HardlyKnowEm,2015年

2
集合论非常不直观。您可以将整数和偶数一一对应(例如使用函数n -> 2n)。这并不总是可能的。您无法将整数映射到实数,因此实数集比整数“更大”。但是您可以将(实数)间隔[0,1]映射到所有实数的集合,以使它们具有相同的“大小”。子集被定义为“的每个元素A也是一个元素B”,恰恰是因为一旦您的集合变为无穷大,就可能是它们的基数相同但元素不同。
Doval 2015年

1
与手头主题更相关,您给出的示例是一个约束,就面向对象编程而言,此约束实际上是在附加字段方面扩展了功能性。我说的是不扩展功能的子类,例如圆椭圆问题。
HardlyKnowEm 2015年

1
@Doval:“更少”的可能含义不止一种-即集合上的排序关系不止一种。关系的“是子集”在集合上给出了定义明确的(部分)顺序。而且,“更多规则”可以理解为“该集合的所有元素都满足(来自某个集合)更多(即一个超集合)命题”。通过这些定义,“更多规则”确实意味着“更少的价值”。粗略地说,这是许多计算机程序语义模型(例如Scott域)所采用的方法。
psmears 2015年

12

不,这不是反模式。我可以想到一些实际的用例:

  1. 如果要使用编译时检查来确保对象集合仅符合一个特定的子类。例如,如果你MySQLDaoSqliteDao你的系统,但出于某种原因,你要确保集合只包含一个来源的数据,如果你描述你可以让编译器验证这个特殊的正确性使用子类。另一方面,如果将数据源存储为字段,这将成为运行时检查。
  2. 我当前的应用程序在AlgorithmConfiguration+ ProductClass和之间具有一对一的关系AlgorithmInstance。换句话说,如果配置FooAlgorithmProductClass属性文件中,你只能得到一个FooAlgorithm该产品类。FooAlgorithm给定的两个值的唯一方法ProductClass是创建一个子类FooBarAlgorithm。没关系,因为它使属性文件易于使用(在属性文件的一个部分中不需要使用许多不同配置的语法),而且对于给定的产品类,不止一个实例是非常罕见的。
  3. @Telastyn的答案给出了另一个很好的例子。

底线:这样做并没有什么害处。一个反模式被定义为:

反模式(或反模式)是对重复出现的问题的常见响应,该问题通常无效,并且可能产生适得其反的风险。

这里没有产生适得其反的风险,因此它不是反模式。


2
是的,我想如果我不得不再次提这个问题,我会说“代码气味”。不足以警告下一代的年轻开发人员避免它,但是,如果您看到它,则可能表明总体设计中存在问题,但最终还是正确的。
HardlyKnowEm 2015年

点1正好在目标上。我不太了解第2点,但我敢肯定它也一样……:)
Phil

9

如果某样东西是模式或反模式,则在很大程度上取决于您要编写的语言和环境。例如,for循环汇编语言,C语言和类似语言中的模式,而Lisp是反模式。

让我告诉你一个我很久以前写的代码的故事...

它使用一种称为LPC的语言,并实现了在游戏中投放咒语的框架。您有一个法术超类,该类具有战斗法术的子类来处理一些检定,然后是针对单个目标直接伤害法术的子类,然后将其归类为单个法术-魔法导弹,闪电箭等。

LPC工作原理的本质是您拥有一个主对象,即Singleton。在您使用“ eww Singleton”之前-根本不是“ eww”。一个人没有复制这些咒语,而是调用了这些咒语中的(有效)静态方法。魔术导弹的代码如下:

inherit "spells/direct";

void init() {
  ::init();
  damage = 10;
  cost = 3;
  delay = 1.0;
  caster_message = "You cast a magic missile at ${target}";
  target_message = "You are hit with a magic missile cast by ${caster}";
}

你看到了吗?它是一个仅构造函数的类。它设置了一些在其父抽象类中的值(不,它不应该使用setter-它是不可变的),仅此而已。它工作正常。它易于编码,易于扩展且易于理解。

这种模式会转换为其他情况,在这种情况下,您有一个子类扩展了抽象类并设置了自己的一些值,但是所有功能都在其继承的抽象类中。看看Java中的StringBuilder就是一个例子-不仅是构造函数,而且我迫切希望在方法本身中找到很多逻辑(一切都在AbstractStringBuilder中)。

这不是一件坏事,而且肯定不是反模式(在某些语言中,它本身可能就是模式)。


2

我可以想到的此类子类最常见的用法是异常层次结构,尽管这是一种退化的情况,只要我们允许该语言继承构造函数,我们通常就不会在该类中定义任何内容。我们使用继承来表示,这ReadError是的特例IOError,依此类推,但ReadError不必覆盖的任何方法IOError

我们这样做是因为通过检查异常的类型来捕获异常。因此,我们需要对类型进行编码,以便某人可以只捕获ReadError而不捕获全部IOError

通常,检查事物的类型是非常糟糕的形式,我们会尽量避免这种情况。如果我们成功地避免了这种情况,那么就没有必要在类型系统中表达专业性。

但是,它可能仍然有用,例如,如果类型的名称以对象的记录字符串形式出现:这是语言,并且可能是特定于框架的。原则上,您可以在任何此类系统中用对类的方法的调用来替换类型的名称,在这种情况下,我们覆盖的不仅是构造函数SystemDataHelperBuilder,而且还将覆盖loggingname()。因此,如果您的系统中存在此类日志记录,您可以想象,尽管您的类从字面上仅覆盖了构造函数,但“道德上”它也覆盖了类名称,因此出于实用目的,它不是仅构造函数的子类。它具有理想的不同行为,它是

因此,我想说他的代码是个好主意,如果其他地方有一些代码以某种方式检查的实例SystemDataHelperBuilder,要么显式地带有一个分支(例如基于类型的异常捕获分支),要么因为有一些通用代码会最终SystemDataHelperBuilder以一种有用的方式使用该名称(即使只是记录日志)。

但是,在特殊情况下使用类型确实会引起一些混乱,因为如果ImmutableRectangle(1,1)在任何方面都ImmutableSquare(1)表现不同则最终有人会想知道为什么,也许希望它没有。与异常层次结构不同,您可以通过检查height == width,而不是来检查矩形是否是正方形instanceof。在您的示例中,使用完全相同的参数创建的SystemDataHelperBuilder和之间存在差异DataHelperBuilder,这有可能引起问题。因此,对于我来说,返回使用“正确”参数创建的对象的函数通常看起来是正确的事情:子类导致“同一”事物的两种不同表示形式,并且仅在您正在执行某项操作时才有用通常会尽量不要这样做。

请注意,我不是在谈论设计模式和反模式,因为我不认为有可能对程序员曾经想过的所有好与坏想法进行分类。因此,并不是每个主意都是模式或反模式,只有当有人识别出它在不同的上下文中反复出现并为其命名时,它才会变成;-)我不知道这种子类的任何特定名称。


我可以看到for的用途ImmutableSquareMatrix:ImmutableMatrix,只要有一种有效的方法,可以要求an ImmutableMatrix尽可能呈现其内容ImmutableSquareMatrix。即使ImmutableSquareMatrix基本上是一个ImmutableMatrix其构造函数要求高度和宽度匹配,并且其AsSquareMatrix方法仅会返回自身的方法(而不是例如构造一个ImmutableSquareMatrix包装相同的后勤数组的方法),这种设计也允许对需要平方的方法进行编译时参数验证矩阵
超级猫

@supercat:很好,在发问者的示例中,如果必须传递一个函数SystemDataHelperBuilder,而不仅仅是任何函数都可以传递DataHelperBuilder,那么为此定义一个类型(和一个接口,假设您以这种方式处理DI)是有意义的。 。在您的示例中,存在一些操作,方阵可能具有非方阵,例如行列式,但是您的观点很好,即使系统不需要那些您仍然希望按类型检查的操作。
史蒂夫·杰索普

……所以我想我所说的并不是完全正确的,“您通过检查是否height == width不是来检查矩形是否是正方形instanceof”。您可能更喜欢可以静态声明的内容。
史蒂夫·杰索普

我希望有一种机制可以让类似构造函数的语法调用静态工厂方法。表达式的类型foo=new ImmutableMatrix(someReadableMatrix)someReadableMatrix.AsImmutable()甚至的类型更清晰ImmutableMatrix.Create(someReadableMatrix),但是后者的形式提供了前者所缺乏的有用的语义可能性。如果构造函数可以告知矩阵是方形的,则能够使其类型反映比要求下游代码(需要方形矩阵制作新的对象实例)更干净。
supercat

@supercat:我喜欢Python的一件事是缺少new运算符。一个类只是一个可调用类,被调用时恰巧返回(默认情况下)其自身的实例。因此,如果要保持接口一致性,完全可以编写一个工厂函数,其名称以大写字母开头,因此看起来就像构造函数调用。不是说我经常使用工厂功能执行此操作,而是在需要时使用它。
史蒂夫·杰索普

2

子类关系的最严格的面向对象定义被称为“ is-a”。使用正方形和矩形的传统示例,即正方形“是”矩形。

这样,您应该在任何认为“ is-a”对代码有意义的情况下使用子类化。

对于子类只有构造函数的情况,应该从“是”角度进行分析。很显然,SystemDataHelperBuilder«是-»DataHelperBuilder,因此第一遍建议它是有效的用法。

您应该回答的问题是,其他任何代码是否都利用了这种“是”关系。是否有任何其他代码试图将SystemDataHelperBuilders与一般的DataHelperBuilders区分?如果是这样,则关系是非常合理的,您应该保留子类。

但是,如果没有其他代码执行此操作,那么您所拥有的是一个可能是“是”关系的关系,或者可以通过工厂来实现。在这种情况下,您可以选择任何一种方式,但我建议您使用“工厂”而不是“是”关系。在分析另一个人编写的面向对象的代码时,类层次结构是信息的主要来源。它提供了他们理解代码所需要的“是”关系。因此,将这种行为放入子类中可以使行为从“有人会发现他们是否研究超类的方法”到“每个人甚至在开始研究代码之前都会仔细研究”。引用吉多·范·罗苏姆(Guido van Rossum)的话说,“代码读取比写入更频繁” 因此,对于即将到来的开发人员而言,降低代码的可读性是一个沉重的代价。如果我可以改用工厂,我会选择不付款。


1

只要您始终可以用其子类型的实例替换其超类型的实例,并使一切仍然正常运行,则子类型永远不会“错误”。只要子类从不试图削弱任何超类所做的保证,就可以进行检查。它可以提供更强(更具体)的保证,因此从某种意义上说,您的同事的直觉是正确的。

鉴于此,您制作的子类可能没有错(因为我不知道它们到底在做什么,所以我不能肯定地说。)您需要警惕脆弱的基类问题,并且还必须考虑一下是否interface会使您受益,但这对继承的所有用法都是如此。


1

我认为回答这个问题的关键是看一下这个特定的使用场景。

继承用于强制执行数据库配置选项,例如连接字符串。

这是一种反模式,因为该类违反了单一职责主体-它使用特定的配置来源来配置自己,而不是“做一件事情,做好一件事情”。类的配置不应引入类本身。对于设置数据库连接字符串,大多数情况下,我在应用程序启动时会在某种事件中看到这种情况。


1

我经常使用构造函数的子类来表达不同的概念。

例如,考虑以下类:

public class Outputter
{
    private readonly IOutputMethod outputMethod;
    private readonly IDataMassager dataMassager;

    public Outputter(IOutputMethod outputMethod, IDataMassager dataMassager)
    {
        this.outputMethod = outputMethod;
        this.dataMassager = dataMassager;
    }

    public MassageDataAndOutput(string data)
    {
        this.outputMethod.Output(this.dataMassager.Massage(data));
    }
}

然后,我们可以创建一个子类:

public class CapitalizeAndPrintOutputter : Outputter
{
    public CapitalizeAndPrintOutputter()
        : base(new PrintOutputMethod(), new Capitalizer())
    {
    }
}

然后,您可以在代码中的任何位置使用CapitalizeAndPrintOutputter,并且确切知道它是什么类型的输出器。此外,此模式实际上使使用DI容器创建此类非常容易。如果DI容器了解PrintOutputMethod和Capitalizer,它甚至可以为您自动创建CapitalizeAndPrintOutputter。

使用此模式确实非常灵活且有用。是的,您可以使用工厂方法执行相同的操作,但是(至少在C#中)您失去了类型系统的某些功能,并且使用DI容器当然变得更加麻烦。


1

首先让我注意,在编写代码时,子类实际上并没有比父类更具体,因为初始化的两个字段都可以由客户端代码设置。

子类的实例只能使用向下转换来区分。

现在,如果您有一个设计,子类可以重新实现DatabaseEngineConnectionString属性,而子类可以重新实现该属性Configuration,那么约束将具有使子类更有意义的约束。

话虽这么说,“反模式”对我的口味来说太强了。这里真的没有错。


0

在您给出的示例中,您的伴侣是正确的。这两个数据帮助程序构建器是策略。一个比另一个更具体,但是一旦初始化它们就可以互换。

基本上,如果datahelperbuilder的客户端要接收systemdatahelperbuilder,则不会中断任何操作。另一个选择是让systemdatahelperbuilder是datahelperbuilder类的静态实例。

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.