init()方法有代码味道吗?


20

声明init()类型的方法是否有目的?

我不是问我们是否应该优先init()于构造函数,或者如何避免声明init()

我问的是在声明方法(查看它的普遍性)后是否有任何理论依据,init()或者它是否是一种代码味道,应避免使用。


这个init()习语很普遍,但是我还没有看到任何真正的好处。

我说的是鼓励通过方法初始化的类型:

class Demo {
    public void init() {
        //...
    }
}

什么时候可以在生产代码中使用它?


我觉得这可能是代码的味道,因为它表明构造函数未完全初始化对象,从而导致部分创建了对象。如果未设置状态,则该对象不应存在。

从企业应用的意义上讲,这使我相信它可能是某种用于加速生产的技术的一部分。这是我可以想到的惯用法,这是唯一合乎逻辑的原因,但我不确定是否会有这样的习惯。


1
要“ ...看到它有多普遍...”:普遍吗?你能举一些例子吗?也许您正在处理一个需要初始化和构造分开的框架。

该方法是在基类或派生类上还是在两者上找到的?(或者:方法是在属于继承层次结构的类上找到的吗?基类是否在init()派生类上调用,反之亦然?)如果这样,它是让基类执行“后构造函数”的一个示例。 “只有在最派生的类完成构造后才能执行。这是多阶段初始化的示例。
rwong

如果您不想在实例化时进行初始化,则将两者分开是有意义的。
JᴀʏMᴇᴇ


Answers:


39

是的,这是代码气味。代码气味不一定总是需要消除的。这会让您重新审视。

在这里,您有一个处于两种根本不同状态的对象:初始化前和初始化后。这些州有不同的责任,允许使用的不同方法以及不同的行为。实际上是两个不同的类。

如果您将它们物理上划分为两个单独的类,则将静态删除整个类的潜在错误,其代价可能是使您的模型与“现实世界模型”非常不匹配。您通常会命名第一个ConfigSetup类似名称。

因此,下次尝试将您的Construct-init惯用法重构为两类模型,然后看看结果如何。


6
您建议尝试两类模型是好的。提出解决代码气味的具体步骤很有用。
伊万

1
到目前为止,这是最好的答案。不过,有一件事困扰着我:“ 那些州有不同的责任,允许调用不同的方法,并且有不同的行为 ”-不分离这些责任会违反SRP。如果该软件的目的是在各个方面复制真实的场景,那将是有道理的。但是在生产中,开发人员通常会编写鼓励易于管理的代码,并在需要时修改模型以更好地适应基于软件的环境(续下
一条

1
现实世界是不同的环境。试图复制它的程序似乎是特定于域的,因为在大多数项目中您不会/不应该考虑它。当涉及到特定于领域的项目时,很多令人不满意的事情都会被接受,因此我试图将其尽可能地笼统(例如,this构造函数中的调用是一种代码味道,并可能导致易于出错的代码,并建议您避免使用它,无论您的项目属于哪个领域。
文斯·埃米格

14

这取决于。

一个init方法是一个代码的气味时,它是没有必要具有从构造分离的对象初始化。在某些情况下,将这些步骤分开是有意义的。

快速的Google搜索为我提供了这个示例。我可以轻松想象更多情况下,在对象分配(构造函数)期间执行的代码最好与初始化本身分开。也许您有一个分级的系统,并且分配/构造在X层进行,但仅在Y层进行初始化,因为只有Y可以提供必要的参数。也许“ init”是昂贵的,并且只能对已分配对象的一个​​子集运行,并且只能在Y级上确定该子集。或者您想在派生对象中覆盖(虚拟)“ init”方法构造函数无法完成的类。也许X级别为您提供了从继承树分配的对象,但是Y级别并不知道具体的派生,仅关于公共接口(其中init 也许定义)。

当然,以我的经验来看,这些情况仅是标准情况的一小部分,在标准情况下,所有初始化都可以直接在构造函数中完成,每当您看到单独的init方法时,最好质疑一下它的必要性。


2
该链接中的答案说,这对于减少构造函数中的工作量很有用,但它鼓励部分创建对象。较小的构造函数可以通过分解实现,因此对我来说,答案似乎产生了一个新问题(可能忘记调用所有必需的初始化方法,而使对象容易出错),并且属于代码气味类别
Vince Emigh

@VinceEmigh:好吧,这是我能找到这里的SE platfform,也许不是最好的一个第一个例子,但也合法的使用个案单独的init方法。但是,每当您看到这样的方法时,请随时质疑它的必要性。
布朗

我在质疑/挑战每个用例,因为我认为没有必要这样做。对我来说,创建对象的时机很差,应该避免这种情况,因为它是通过适当设计可以避免的错误的候选对象。如果正确使用一种init()方法,那么我肯定会从学习其目的中受益。对我的无知表示歉意,我只是为找到它的用途而感到惊讶,这使我无法考虑应避免的事情
Vince Emigh

1
@VinceEmigh:当您无法想到这种情况时,您需要发挥您的想象力;-)。或再次阅读我的答案,不要仅仅将其简化为“分配”。或者使用来自不同供应商的更多框架。
布朗

1
@DocBrown使用想象力而不是经过实践验证的做法会导致某些时髦的代码,例如双花括号初始化:聪明但效率低下,应避免使用。我知道供应商会使用它,但这并不意味着使用合理。如果您有这种感觉,请告诉我原因,这就是我一直在努力解决的问题。您似乎对它有一些有益的目的陷于僵局,但听起来您很难举一个例子来鼓励良好的设计。我知道在什么情况下可以使用它,但是应该使用吗?
文斯·艾米

5

我的经验分为两类:

  1. 实际需要init()的代码。当超类或框架阻止类的构造函数在构造过程中获取其所有依赖关系时,可能会发生这种情况。
  2. 使用了init()但可以避免的代码。

以我的个人经验,我仅看到(1)的几个实例,但是看到了(2)的更多实例。因此,我通常假定init()是代码气味,但并非总是如此。有时,您只是无法解决它。

我发现使用构建器模式通常可以帮助消除需要init()的需求。


1
如果超类或框架不允许类型通过构造函数获取所需的依赖关系,那么添加init()方法将如何解决呢?该init()方法可能需要参数来接受依赖关系,或者必须实例化方法的依赖关系init(),也可以使用构造函数来实现。你能举个例子吗?
文斯·埃米格

1
@VinceEmigh:init()有时可用于从外部源加载配置文件,打开数据库连接或类似的东西。DoFn.initialize()方法(来自Apache Crunch框架)以这种方式使用。它也可以用于加载不可序列化的内部字段(DoFns必须可序列化)。这里的两个问题是(1)需要确保初始化方法被调用的某些东西,以及(2)对象需要知道它将在哪里获得(或如何构建)这些资源。
伊万

1

使用Init方法很有用的一种典型情况是,您拥有要更改的配置文件,并且无需重新启动应用程序即可将更改考虑在内。当然,这并不意味着必须与构造函数分开调用Init方法。您可以从构造函数中调用Init方法,然后在/如果配置参数更改的情况下调用它。

总结:至于大多数困境,这是否是代码异味,取决于情况和情况。


根据该配置,确定配置更新,这需要对象重置/改变它的状态,你不认为这将是最好有对象充当观察者走向Config
文斯·埃米格

@Vince Emigh不是必须的。如果我知道配置更改的确切时间,观察者将正常工作。但是,如果将配置数据保存在可以在应用程序外部进行更改的文件中,则没有真正的优雅方法。例如,如果我有一个程序可以解析某些文件并将数据转换为内部模型,并且一个单独的配置文件包含丢失数据的默认值,那么如果更改默认值,则在下次运行时会再次读取它们解析。在这种情况下,在我的应用程序中使用Init方法将非常方便。
弗拉基米尔·斯托基奇'16

如果配置文件是在运行时从外部修改的,则无法在没有某种通知的情况下重新加载这些变量,而无需通知您的应用程序需要调用init(update/ reload可能更能描述这种行为)来实际注册这些更改。 。在那种情况下,该通知将导致配置的值在您的应用程序内部发生更改,我认为可以通过使您的配置可观察来观察到,并在通知您更改配置的值时通知观察者。还是我误解了您的榜样?
文斯·埃米格

应用程序获取一些外部文件(从某些GIS或其他系统导出),解析这些文件并将其转换为应用程序所使用的系统所使用的某些内部模型。在该过程中,可以找到一些数据空白,并且可以通过默认值来填补这些数据空白。这些默认值可以存储在配置文件中,该文件可以在转换过程开始时读取,只要有新文件要转换,就会调用该默认文件。那就是初始化方法可能派上用场的地方。
弗拉基米尔·斯托基奇'16

1

取决于您如何使用它们。

当我不想一直在堆上重新分配对象时(例如当我制作视频游戏并需要保持高性能时,垃圾收集器会降低性能),我在Java / C#等垃圾收集语言中使用了这种模式。我使用构造函数进行所需的其他堆分配,并且init在每次要重用之前创建基本的有用状态。这与对象池的概念有关。

如果您有几个共享相同初始化指令子集的构造函数,但在这种情况下init将是私有的,这也很有用。这样,我可以尽可能地减少每个构造函数,因此每个构造函数仅包含其唯一的指令和对init执行其余。

通常,这是一种代码气味。


一种reset()方法对您的第一个陈述是否更具描述性?至于第二个(许多构造函数),具有许多构造函数是一种代码味道。它假定对象具有多个目的/职责,表明违反了SRP。一个对象应该承担一个责任,而构造函数应该为该责任定义所需的依赖关系。如果由于可选值而具有多个构造函数,则应进行伸缩(这也是代码的味道,应改用构建器)。
文斯·埃米格

@VinceEmigh可以使用init或reset,毕竟这只是一个名称。在我倾向于使用它的上下文中,Init更有意义,因为重置一个我第一次使用从未设置过的对象几乎没有意义。至于构造函数问题,我尝试避免有很多构造函数,但有时会有所帮助。查看任何语言的string构造函数列表,以及大量的选择。对我来说,通常最多可以有3个构造函数,但是当它们共享任何代码但以任何方式有所不同时,作为初始化的公共指令子集才有意义。
科迪

名称非常重要。它们描述了所执行的行为,而没有强迫客户阅读合同。错误的命名可能导致错误的假设。对于中的多个构造函数String,可以通过解耦字符串的创建来解决。最后,a String是a String,并且它的构造函数只应接受执行它所需的内容。这些构造函数中的大多数都出于转换目的而公开,这是对构造函数的滥用。构造函数应该执行逻辑,或他们的风险初始化失败,留给你一个无用的对象。
文斯·艾米

JDK充满了可怕的设计,我可以列举大约10个。自从许多语言的核心方面被公开曝光以来,软件设计已经发生了演变,并且由于要在近代进行重新设计,可能会破坏代码,因此它们会持续存在。
文斯·艾米

1

init()当您拥有需要外部资源(例如,网络连接)并由其他对象同时使用的对象时,这些方法在某种意义上是有意义的。您可能不想/不需要在对象的生存期内占用资源。在这种情况下,当资源分配可能失败时,您可能不想在构造函数中分配资源。

尤其是在嵌入式编程中,您希望有确定的内存占用量,因此通常的做法(好吗?)是提早调用构造函数,甚至是静态调用构造函数,并且仅在满足某些条件时才进行初始化。

除了这种情况之外,我认为所有内容都应该放入构造函数中。


如果类型需要连接,则应使用DI。如果创建连接时出现问题,则不应创建需要连接的对象。如果您要在类内部创建连接,则将创建一个对象,该对象将实例化其依赖关系(连接)。如果依赖关系的实例化失败,您将得到一个无法使用的对象,从而浪费了资源。
文斯·埃米格

不必要。您最终得到一个暂时不能用于所有目的的对象。在这种情况下,对象可以仅充当队列或代理,直到资源可用为止。init恕我直言,完全谴责方法过于严格。
tofro

0

通常,我更喜欢一个可以接收功能实例所需的所有参数的构造函数。这清楚了该对象的所有依赖关系。

另一方面,我使用一个简单的配置框架,该框架需要一个无参数的公共构造函数,以及用于注入依赖项和配置值的接口。完成之后,配置框架将调用init对象的方法:现在您已经收到了我为您准备的所有东西,请执行最后的步骤以准备工作。但是请注意:配置框架会自动调用init方法,因此您不会忘记调用它。


0

如果init()方法在语义上嵌入在对象的状态生命周期中,则没有代码异味。

如果您需要调用init()将对象置于一致状态,则这是一种代码异味。

存在这种结构有多种技术原因:

  1. 框架挂钩
  2. 将对象重置为初始状态(避免冗余)
  3. 测试期间可能会覆盖

1
但是,为什么软件工程师会将其嵌入到生命周期中呢?是否有任何合理的目的(考虑到它鼓励使用部分构造的对象),并且无法被更有效的替代方法击落?我觉得将其嵌入到生命周期中将是一种代码味道,应该用更好的对象创建时机来代替它(如果您当时不打算使用它,为什么要为一个对象分配内存?为什么要创建一个当您可以等到实际需要该对象时才部分构造该对象吗?)
Vince Emigh '16

关键是在调用init方法之前,该对象必须是可用的。也许以不同于被调用之后的方式。请参阅状态模式。从部分对象构造的意义上讲,这是一种代码气味。
oopexpert

-4

名称init有时可能是不透明的。让我们坐汽车和引擎。要启动汽车(仅加电,收听广播),您需要验证所有系统是否已准备就绪。

因此,您构造了一个引擎,一个门,一个轮子等。您的屏幕上显示engine = off。

无需开始监视引擎等,因为它们都很昂贵。然后,当您转动钥匙点火时,请调用engine-> start。它开始运行所有昂贵的进程。

现在,您看到engine = on。然后开始点火过程。

如果没有发动机,汽车将无法启动。

您可以将引擎替换为复杂的计算。就像Excel单元格一样。并非所有单元都需要始终与所有事件处理程序一起处于活动状态。当您专注于一个单元格时,您可以启动它。这样可以提高性能。

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.