【问题标题】:Static functions are bad - but what's the alternative?静态函数很糟糕 - 但有什么替代方案?
【发布时间】:2015-10-29 08:34:57
【问题描述】:

在我的示例中,我使用的是 PHP 框架 Yii2,但我认为这适用于大多数 OO 语言。

我有一个 ActiveRecord 基类,我的大部分业务对象都从它扩展而来,例如Project.

目前,如果我想要一个 Project 实例,我会调用

Project::findOne(['id' => $id]);

findOne 是ActiveRecord 的静态方法(它是Yii2 框架的一部分)。所以这是一种不好的形式,因为在编写单元测试时我不能轻易地模拟/存根这个调用的返回。

但是解决这个问题的最佳方法是什么?

我可以创建一个继承自 ActiveRecord 的类 CActiveRecord 并将静态调用包装在一个非静态调用中并在任何地方使用它 - 但是我必须按顺序实例化一个丢弃的 Project 对象获取实际实例。如果Project 对象需要一些繁重的配置来实例化怎么办 - 我会将随机的废话传递给构造函数只是为了获取一个实例。

总结: 简单地将静态变量更改为非静态变量似乎是错误的 - 我不应该将函数也移动到其他地方吗?如果有,在哪里?

【问题讨论】:

  • 尝试搜索 PHP LSB(Late Static Bindings)。
  • 谷歌全局状态。也使您的代码难以测试。另一种方法是实例化您的对象并通过依赖注入传递它们。只有架构较差的代码库才需要静态。忽略那些说什么都有位置的人,比如goto,因为少了一个操作码。
  • 此外,活动记录是一种非常幼稚的模式,它将您的域与其持久性相结合。看看 DataMapper。
  • 虽然这确实可能是一个不好的做法,但听起来你选择了框架,然后你应该选择框架(或找到更好的库)。
  • @Gerry 问题是许多框架强加给你的结构非常糟糕,最终会在为时已晚时咬你一口。你至少必须真正理解你在“使用框架”时同意的权衡并决定它是否值得(例如,因为项目现在并且将保持“小”,框架完全符合预期用途并减少了几周到几个小时的工作来生成你的模型等等)。

标签: php oop static yii2


【解决方案1】:

静态调用的问题是 硬耦合 到特定的其他代码段。只是将其包装在“动态”调用中并不能使它变得更好:

$c = new CProject;
$c->findOne(); // Calls Project::findOne()

这真是太没有意义了。问题不在于->:: 的语法,问题在于此特定代码引用了特定的其他类,并且您无法轻松地将此类替换为其他类。你在你的类/对象之间建立了严格的、硬编码的依赖关系,这使得它们很难分开,这使得你的代码难以测试,也使得代码更难适应不同的情况。

替代方案是依赖注入

function foo(Project $project) {
    $p = $project->findOne();
}

此函数不与任何一个特定 Project 类耦合,而是与仅提供类似于Project 的接口的类耦合。事实上,Project 甚至可以只是一个interface。然后在这里调用哪个特定的类和方法,然后在完全不同的地方决定,比如你的依赖注入容器;或者只是这段代码的调用者。

这使得拆开这段代码并根据当前情况以不同方式重新组合起来变得容易得多。这并不是说它不能工作并且你根本不应该使用静态调用,但你真的需要知道你正在与每个硬编码的类名建立什么交叉依赖关系,以及这可能会或可能不会导致一个问题。即使对于中等复杂和/或增长的软件项目,它几乎肯定会最终导致某种形式的摩擦。

请参阅How Not To Kill Your Testability Using Statics 以获得更长的深入文章。

【讨论】:

  • 谢谢你,我最近读了很多同意你所说的文章——我也同意。我不太明白的是最好的做法。我得到了 DI,但在你的例子中,调用 foo() 的东西必须实例化一个丢弃的 Project 对象——这似乎不对?你只是创建一个“ActiveRecordHelper”还是轻量级的实例化?
  • ActiveRecord 本身就是一个问题。如果Project 既是代表数据实例 的对象,又是数据库连接器本身,则几乎没有好方法来处理这个问题。至少你应该使用 Factory$projectFactory->findOne() 返回一个 Project 实例。 ProjectFactory 本身会像 new ProjectFactory($database) 一样被实例化。 $databasePDO 或其他的一个实例。您将只实例化一次ProjectFactory,并将其传递给需要检索Project 对象的每个函数/类。你看出区别了吗?
  • 是的,没错,你明白了。这就是我所说的额外复杂性。根据您的特定对象,“父”对象了解其依赖关系是可管理和/或可取的。但同样,这也是 DI 容器或“服务定位器”的用武之地。$diContainer->get('ProjectFactory'),其中 $diContainer 被配置为知道 ProjectFactory 需要哪些依赖项,并且可以构造具有以去重方式解析的所有依赖项的对象。见symfony.com/doc/current/components/dependency_injection/…
  • “覆盖”diContainer 并没有真正进入画面。如果您有一个想要实例化另一个对象的类,并且该对象需要两个依赖项,而这两个依赖项可能每个都需要另外两个依赖项......那么您的类很难拥有所有这些递归依赖项并实例化对象。这就是 diContainer 解决的问题。该类只有一个 diContainer(作为依赖项注入)并要求 diContainer 完成解决依赖项的所有工作。如果需要自定义实例化,则重新配置 diContainer。
  • 这样想:你写的每个类都完全独立于其他类。您永远不会直接引用任何其他特定类。最多你在做一些类型提示;最好这些类型提示仅用于接口,而不是特定的其他类。现在你有一堆完全独立的类,它们可以像乐高积木一样放在一起。您现在需要非常具体和仔细地选择这些乐高积木在哪些点组合在一起。 DI 容器就是这样一个集中配置的位置,可以轻松更改或更换。
猜你喜欢
  • 1970-01-01
  • 2011-09-22
  • 1970-01-01
  • 1970-01-01
  • 2011-03-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-09-25
相关资源
最近更新 更多