我应该将对象传递给构造函数,还是在类中实例化?


10

考虑以下两个示例:

将对象传递给构造函数

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

实例化课程

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

处理将对象添加为属性的正确方法是哪种?我什么时候应该使用另一个?单元测试会影响我应该使用的内容吗?



9
@FrustratedWithFormsDesigner-这不适用于代码审查。
克里斯·弗雷德

Answers:


14

我认为第一个对象将使您能够在config其他地方创建对象并将其传递给ExampleA。如果需要依赖注入,这可能是一件好事,因为您可以确保所有实例共享同一对象。

另一方面,也许您ExampleA 需要一个新的干净config对象,所以在某些情况下第二个示例更合适,例如每个实例可能具有不同的配置。


1
因此,A有利于单元测试,B有利于其他情况……为什么不同时具有两者?我经常有两个构造函数,一个用于手动DI,一个用于常规用法(我使用Unity for DI,它将生成满足其DI要求的构造函数)。
Ed James

3
@EdWoodcock与单元测试和其他情况无关。该对象要么需要外部的依赖关系,要么在内部对其进行管理。永不兼而有之。
MattDavey 2012年

9

不要忘记可测试性!

通常,如果Example类的行为取决于配置,则您希望能够在不保存/修改类实例的内部状态的情况下对其进行测试(即,您希望在不修改属性/ Example班级成员)。

因此,我会选择 ExampleA


那就是为什么我提到测试-感谢您添加:)
囚犯

5

如果对象负责管理依赖项的生存期,则可以在构造函数中创建对象(并将其放置在析构函数中)。*

如果对象不负责管理依赖项的生存期,则应将其传递到构造函数中并从外部进行管理(例如,通过IoC容器)。

在这种情况下,我认为您的ClassA不应负责创建$ config,除非它也负责处理$ config,或者如果该配置对于ClassA的每个实例都是唯一的。

*为了有助于可测试性,构造函数可以引用工厂类/方法来在其构造函数中构造依赖关系,从而提高内聚性和可测试性。

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

2

我最近与我们的架构团队进行了完全相同的讨论,有一些微妙的理由以一种或另一种方式进行。它主要取决于依赖注入(如其他人所指出的)以及您是否真正控制了在构造函数中新建的对象的创建。

在您的示例中,如果您的Config类:

a)具有非平凡的分配,例如来自池或工厂方法。

b)可能无法分配。巧妙地将其传递给构造函数可以避免此问题。

c)实际上是Config的子类。

将对象传递给构造函数可提供最大的灵活性。


1

以下答案是错误的,但我会将其保留,以供其他人学习(请参阅下文)

在中ExampleA,您可以Config在多个类中使用同一实例。但是,如果Config整个应用程序中只有一个实例,请考虑应用Singleton模式Config以避免避免多个实例Config。如果Config是Singleton,则可以执行以下操作:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

ExampleB,在另一方面,你永远得到一个单独的实例Config为每个实例ExampleB

您应该应用哪个版本实际上取决于应用程序如何处理以下实例Config

  • 如果每个实例ExampleX都应有一个单独的实例Config,请配合ExampleB;
  • 如果每个实例ExampleX将共享一个(并且只有一个)实例Config,请使用ExampleA with Config Singleton
  • 如果的实例ExampleX可能使用的不同实例Config,请坚持使用ExampleA

为什么转换Config单例是错误的:

我必须承认,我昨天才了解到Singleton模式(阅读Head First设计模式书)。我天真地将它应用于本示例,但正如许多人指出的,一种方式是另一种方式(有些方式比较隐秘,只说“您做错了!”),这不是一个好主意。因此,为防止其他人犯与我刚才相同的错误,以下总结了Singleton模式为何有害的原因(基于评论和我发现的谷歌搜索结果):

  1. 如果ExampleA检索到自己对Config实例的引用,则这些类将紧密耦合。没有办法让实例ExampleA使用其他版本的Config(例如某些子类)。如果您想ExampleA使用的模拟实例进行测试,这将很可怕,Config因为无法将其提供给ExampleA

  2. 那里的前提将是一个,只有一个,实例Config也许认为现在,但你不能总是肯定的是,同样的将持有的未来。如果在以后的某个时刻发现需要多个实例Config,则没有办法无需重写代码就无法实现。

  3. 即使一个Config永恒的实例对于所有永恒都是正确的,但您可能仍然希望能够使用的某个子类Config(而仍然只有一个实例)。但是,由于代码是通过方法直接获取实例getInstance()Config,因此static无法获取子类。同样,必须重写代码。

  4. 至少在仅查看的API时,ExampleA使用的事实Config将被隐藏ExampleA。这可能不是一件坏事,但就我个人而言,这感觉像是一种缺点。例如,在进行维护时,没有简单的方法来找出哪些类将受到更改的影响,而Config无需研究其他所有类的实现。

  5. 即使ExampleA使用Singleton Config本身并不是问题,但从测试的角度来看,它仍然可能成为问题。单例对象将带有状态,该状态将一直持续到应用程序终止。当您运行单元测试时,这可能是一个问题,因为您希望将一个测试与另一个测试隔离开(即执行一个测试不会影响另一个测试的结果)。要解决此问题,必须在每次测试运行之间销毁Singleton对象(可能必须重新启动整个应用程序),这可能很耗时(更不用说乏味和烦人了)。

话虽如此,我很高兴自己在这里而不是在实际应用程序的实现中犯了这个错误。实际上,我实际上是在考虑重写我的最新代码,以对某些类使用Singleton模式。尽管我可以轻松地还原更改(所有内容都存储在SVN中),但是我仍然会浪费时间。


4
我不建议您这样做。.这样您会紧密地结合课堂ExampleAConfig-这不是一件好事。
2012年

@Paul:是的。好的收获,没想到。
gablin 2012年

3
由于可测试性,我总是建议不要使用Singletons。它们本质上是全局变量,不可能模拟依赖关系。
MattDavey 2012年

4
我总是建议再次使用Singletons,因为这样做的原因。
雷诺斯2012年

1

做最简单的事情是夫妇ExampleAConfig。除非有令人信服的理由要做更复杂的事情,否则您应该做最简单的事情。

原因之一脱钩ExampleA,并Config会改善的可测试性ExampleAExampleA如果Config使用缓慢,复杂或快速发展的方法,直接耦合将降低可测试性。对于测试而言,如果方法运行超过几微秒,则它会很慢。如果所有方法Config都简单快速,那么我将采用简单方法并直接耦合ExampleAConfig


1

您的第一个示例是“依赖项注入”模式的示例。具有外部依赖项的类由构造函数,setter等传递给依赖项。

这种方法导致代码松耦合。大多数人认为,松散耦合是一件好事,因为在一个对象的一个​​特定实例与另一个实例的配置需要不同的情况下,您可以轻松地替换配置,可以传递一个模拟配置对象进行测试,等等。上。

第二种方法更接近GRASP创建者模式。在这种情况下,对象创建自己的依赖关系。这导致代码紧密耦合,这可能会限制类的灵活性,并使测试更加困难。如果您需要一个类的实例与其他实例具有不同的依赖关系,则唯一的选择是对其进行子类化。

但是,如果依赖对象的寿命由依赖对象的生存期决定,并且在依赖对象的对象外部未使用依赖对象的情况下,它可能是合适的模式。通常,我建议将DI设置为默认位置,但是只要您知道其他方法的后果,就不必完全排除其他方法。


0

如果您的类没有将暴露$config给外部类,那么我将在构造函数中创建它。这样,您将内部状态保持为私有。

如果$config需求在使用前要求正确设置自己的内部状态(例如,需要初始化数据库连接或某些内部字段),则可以将初始化推迟到某些外部代码(可能是工厂类)并将其注入构造函数。或者,正如其他人指出的那样,是否需要在其他对象之间共享它。


0

ExampleA与良好的具体类Config 分离,如果不是Config类型而是Config抽象超类的类型,则提供接收到的对象。

ExampleB坏的具体类Config 紧密耦合。

实例化一个对象会在类之间建立牢固的耦合。应该在Factory类中完成。

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.