从全局状态转向依赖注入等模式的好机制是什么?

时间:2015-01-07 09:39:55

标签: 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();
    }
}

由于应用程序仅依赖于接口,因此我可以在测试中使用模拟的Time实例替换它,例如,允许预定义值以便在下次调用current()时返回。

注射

到目前为止基本如此。我的实际问题是如何将正确的实例放入依赖于它们的类中。根据我对Dependency injection的理解,服务应该由应用程序传递到需要它们的组件中。通常这些服务将在{{main()}}方法或其他起点创建,然后串联,直到它们到达需要它们的组件。

从头开始创建新应用程序时,此模型可能效果很好,但对于我的情况,它不太理想,因为我想逐渐转向更好的设计。所以我提出了以下模式,它自动提供旧功能,同时让我可以灵活地替换服务。

class TimeUser {
    /**
     * @var Time
     */
    private $time;

    public function __construct(Time $time = null) {
        if ($time === null) {
            $time = new SystemTime();
        }

        $this->time = $time;
    }
}

服务可以传递到构造函数中,允许在测试中模拟服务,但在“常规”操作期间,类知道如何创建自己的协作者,提供默认功能,与之前需要的功能相同

问题

我被告知这种做法是不洁净的,并且颠覆了依赖注入的想法。我确实理解真正的方法是传递依赖关系,如上所述,但我没有看到这种更简单的方法有什么问题。还要记住,这是一个庞大的系统,可能需要预先创建数百个服务(服务定位器将是另一种选择,但现在我正试图转向另一个方向)。

有人可以对这个问题有所了解,并提供一些有关在我的案例中实现重构的更好方法的见解吗?

2 个答案:

答案 0 :(得分:2)

我认为你迈出了第一步。 去年我参加了DutchPHP并且有一个关于重构的讲座,讲师描述了从上帝阶级中提取责任的三个主要步骤:

  1. 将代码提取到私有方法(因为它应该是简单的复制粘贴 $这是相同的)
  2. 提取代码以分离类并拉取 依赖
  3. 推送依赖
  4. 我认为你介于第1步和第2步之间。你有一个后门进行单元测试。 根据上述算法,下一步是创建一些静态工厂(讲师命名为ApplicationFactory),它将用于代替在TimeUser中创建实例。 ApplicationFactory是某种ServiceLocator模式。这样你就会反向依赖(根据SOLID原则)。 如果你对此感到满意,你应该删除传递Time实例到构造函数并仅使用ServiceLocator(没有后门进行单元测试,你应该存根服务定位器) 如果你不是,那么你必须找到实例化TimeUser的所有地方并注入Time实现:

    new TimeUser(ApplicationFactory::getTime());
    

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

    1. 将其拆分为较小的工厂
    2. 使用一些依赖注入容器(Symfony DI,AurynDI或 类似的东西)
    3. 目前我的团队正在做类似的事情。我们正在提取职责以分离课程并注入它们。我们有一个ApplicationFactory,但是我们在尽可能高的层次上使用它作为服务定位器,因此下面的类会注入所有依赖项,并且对ApplicationFactory一无所知。我们的应用工厂很大,现在我们正准备用SymfonyDI替换它。

答案 1 :(得分:0)

你要求一个好的机制来做这件事。

您已经描述了可能会强制程序完成此任务的某些阶段,但您仍然计划以非常高的成本手动执行此操作。

如果您真的希望在庞大的代码库上完成此任务,您可以考虑使用程序转换引擎自动执行这些步骤:http://en.wikipedia.org/wiki/Program_transformation

这样的工具可以让你编写修改代码的明确规则。如果做得好,这可以使代码可靠地更改。这并不能最大限度地减少您的测试需求,但可以让您花更多的时间编写测试,花更少的时间手动更改代码(错误地)。