背景
我正在重新设计和重构一个庞大的代码库,该代码库的编写既没有考虑可测试性也没有考虑可维护性。有很多全局/静态状态正在进行。一个函数需要一个数据库连接,所以它只是使用一个全局静态方法来建立一个:$conn = DatabaseManager::getConnection($connName);
。或者它想要加载一个文件,所以它使用 $fileContents = file_get_contents($hardCodedFilename);
来完成。
大部分代码都没有进行适当的测试,并且只在生产中直接进行过测试。所以我打算做的第一件事是编写单元测试,以确保重构后功能正确。遗憾的是,像上面示例这样的代码几乎无法进行单元测试,因为没有任何外部依赖项(数据库连接、文件句柄...)可以被正确模拟。
抽象
为了解决这个问题,我创建了非常薄的包装器,例如系统函数,可以在以前使用不可模拟函数调用的地方使用。 (我在 PHP 中给出了这些示例,但我假设它们也适用于任何其他 OOP 语言。这也是一个高度缩短的示例,实际上我正在处理更大的类。)
interface Time {
/**
* Returns the current time in seconds since the epoch.
* @return int for example: 1380872620
*/
public function current();
}
class SystemTime implements Time {
public function current() {
return time();
}
}
这些可以像这样在代码中使用:
class TimeUser {
/**
* @var Time
*/
private $time;
/**
* Prints out the current time.
*/
public function tellsTime() {
// before:
echo time();
// now:
echo $this->time->current();
}
}
由于应用程序只依赖于接口(interface),我可以在测试中用模拟的 Time
实例替换它,例如,它允许预定义下一次调用返回的值 当前()
。
注入(inject)
到目前为止都是基本的。我的实际问题是如何将正确的实例放入依赖于它们的类中。从我对Dependency injection的理解,服务应该由应用程序传递到需要它们的组件中。通常,这些服务将在 {{main()}} 方法或其他起点创建,然后串联起来,直到它们到达需要它们的组件。
从头开始创建新应用程序时,此模型可能效果很好,但就我的情况而言,它不太理想,因为我想逐渐转向更好的设计。所以我想出了以下模式,它自动提供旧功能,同时让我可以灵活地替换服务。
class TimeUser {
/**
* @var Time
*/
private $time;
public function __construct(Time $time = null) {
if ($time === null) {
$time = new SystemTime();
}
$this->time = $time;
}
}
可以将服务传递到构造函数中,允许在测试中模拟服务,但在“常规”操作期间,该类知道如何创建自己的协作者,提供默认功能,与之前需要的功能相同.
问题
有人告诉我这种方法是不干净的并且颠覆了依赖注入(inject)的想法。我知道真正的方法是传递依赖关系,就像上面概述的那样,但我认为这种更简单的方法没有任何问题。还要记住,这是一个庞大的系统,可能需要预先创建数百个服务(服务定位器是一个替代方案,但现在我正在尝试朝另一个方向发展)。
有人可以阐明这个问题并提供一些见解,以了解在我的案例中什么是实现重构的更好方法吗?
最佳答案
我认为您已经迈出了良好的第一步。 去年我在 DutchPHP 上有一个关于重构的讲座,讲师描述了从神类中提取责任的 3 个主要步骤:
- 将代码提取到私有(private)方法(应该是简单的复制粘贴,因为 $这是一样的)
- 提取代码以分离类并拉取 依赖
- 推送依赖
我认为您处于第一步和第二步之间。你有一个单元测试的后门。 根据上述算法,下一步是创建一些静态工厂(讲师将其命名为 ApplicationFactory),它将用于代替在 TimeUser 中创建实例。 ApplicationFactory 是某种 ServiceLocator 模式。这样你就可以反转依赖(根据 SOLID 原则)。 如果您对此感到满意,您应该删除将 Time 实例传递给构造函数并仅使用 ServiceLocator(没有用于单元测试的后门,您应该 stub 服务定位器) 如果不是,那么您必须找到所有实例化 TimeUser 的地方并注入(inject)时间实现:
new TimeUser(ApplicationFactory::getTime());
一段时间后,您的 ApplicationFactory 将变得非常大。然后你必须做出决定:
- 将其拆分成更小的工厂
- 使用一些依赖注入(inject)容器(Symfony DI、AurynDI 或 类似的东西)
目前我的团队正在做类似的事情。我们正在提取职责以分离类并注入(inject)它们。我们有一个 ApplicationFactory,但我们将它用作尽可能高级别的服务定位器,因此下面的类会注入(inject)所有依赖项并且对 ApplicationFactory 一无所知。我们的应用程序工厂很大,现在我们准备用 SymfonyDI 替换它。
关于oop - 从全局状态转移到依赖注入(inject)等模式的好机制是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27816397/