oop - 从全局状态转移到依赖注入(inject)等模式的好机制是什么?

标签 oop testing design-patterns refactoring

背景

我正在重新设计和重构一个庞大的代码库,该代码库的编写既没有考虑可测试性也没有考虑可维护性。有很多全局/静态状态正在进行。一个函数需要一个数据库连接,所以它只是使用一个全局静态方法来建立一个:$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 个主要步骤:

  1. 将代码提取到私有(private)方法(应该是简单的复制粘贴,因为 $这是一样的)
  2. 提取代码以分离类并拉取 依赖
  3. 推送依赖

我认为您处于第一步和第二步之间。你有一个单元测试的后门。 根据上述算法,下一步是创建一些静态工厂(讲师将其命名为 ApplicationFactory),它将用于代替在 TimeUser 中创建实例。 ApplicationFactory 是某种 ServiceLocator 模式。这样你就可以反转依赖(根据 SOLID 原则)。 如果您对此感到满意,您应该删除将 Time 实例传递给构造函数并仅使用 ServiceLocator(没有用于单元测试的后门,您应该 stub 服务定位器) 如果不是,那么您必须找到所有实例化 TimeUser 的地方并注入(inject)时间实现:

new TimeUser(ApplicationFactory::getTime());

一段时间后,您的 ApplicationFactory 将变得非常大。然后你必须做出决定:

  1. 将其拆分成更小的工厂
  2. 使用一些依赖注入(inject)容器(Symfony DI、AurynDI 或 类似的东西)

目前我的团队正在做类似的事情。我们正在提取职责以分离类并注入(inject)它们。我们有一个 ApplicationFactory,但我们将它用作尽可能高级别的服务定位器,因此下面的类会注入(inject)所有依赖项并且对 ApplicationFactory 一无所知。我们的应用程序工厂很大,现在我们准备用 SymfonyDI 替换它。

关于oop - 从全局状态转移到依赖注入(inject)等模式的好机制是什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27816397/

相关文章:

PHP oop 构建数组

java - 在 Scala 的实例变量方面需要一些帮助

.net - 您能否为 .NET Winforms 应用程序推荐低成本的自动化测试工具?

C# 我应该公开我的 windows 窗体事件相关的私有(private)函数只是为了测试目的吗?

ios - 让一个 View Controller 听另一个

oop - 领域模型和 OO 领域模型有什么区别?

ruby - 使用 rspec 测试生产特定代码 - Sinatra

c#-4.0 - 是否有用于验证的设计模式?

java - 优先组合而不是继承

android - MVP和Android处理Lifecycle事件,UI逻辑在哪里做