PHP MVC:构造函数参数的默认值

PHP MVC: Default values for the constructor parameters

开始之前:

我的问题主要基于意见而被搁置。但我已经尽力以更精确的方式重新编辑它。希望其内容将支持其重新开放。

所以我的问题又来了:

这是关于我的HMVC 项目,一个PHP 框架,其中M、V & C 组件封装在"independent" 块(模块目录)中。该项目不应包含任何静态 class 成员、静态方法、单例或服务定位器。我正在使用依赖注入容器,因此能够提供控制反转 (IoC)。

在 class AbstractTemplate 中,我将模板文件所需的根路径作为默认值分配给 class 构造函数的参数:

abstract class AbstractTemplate {

    public function __construct(
        , $appLayoutsPath = '[app-root-path]/Layouts/Default'
        , $moduleLayoutsPath = '[module-root-path]/Templates/Layouts'
        , $moduleTemplatesPath = '[module-root-path]/Templates/Templates'
    ) {
        //...
    }

}

但通过这种方式,我将 class 耦合到文件系统的硬编码表示。

因此我考虑通过使用单独的 class 来传递默认值,它又将所需的值保存为 class 常量:

class Constants {

    const APP_LAYOUTS_PATH = '[app-root-path]/Layouts/Default';
    const MODULE_LAYOUTS_PATH = '[module-root-path]/Templates/Layouts';
    const MODULE_TEMPLATES_PATH = '[module-root-path]/Templates/Templates';

}

abstract class AbstractTemplate {

    public function __construct(
        , $appLayoutsPath = Constants::APP_LAYOUTS_PATH
        , $moduleLayoutsPath = Constants::MODULE_LAYOUTS_PATH
        , $moduleTemplatesPath = Constants::MODULE_TEMPLATES_PATH
    ) {
        //...
    }

}

通过这种方式,我将抽象 class 与具体实现 Constants 相结合。

我想问你:

  1. 第二个选项可以测试没有问题吗?

  2. 是否有另一种具体的可能性来提供默认值,同时保持良好的可测试性?

感谢您的回答,感谢您抽出宝贵时间。

选项 2 更简洁,更易于维护我会选择 2,代码提示应用程序如 phpStorm 或 DW 将更好地理解选项 2。

我强烈建议不要纯粹出于设计或风格考虑而避免使用静态 class 成员等完整功能,这是有原因的。

清洁代码

我完全同意 选项 2 更简洁。不过我不太同意他的第二点:

I'd strongly recommend not to avoid entire functionality such as static class members purely for design or stylistic considerations, it's there for a reason.

存在语言功能,但这并不一定意味着它们应该用于这个特定目的。此外,问题与其说是风格,不如说是关于(去)耦合。

相互依赖

解决你的第三点:

... class Constants is introducing this interdependence/coupling ...

这里没有inter依赖,因为AbstractTemplate依赖Constants,但是Constants没有依赖。你的第二个选项可以测试,但它不是很灵活。

耦合

在你的第二点你说:

Is there a real issue, that each class using option 2 will depend on a Constants class?

问题不在于引入了依赖关系,而是何种依赖关系。从应用程序的 specificnamed 成员读取值是 tight 耦合,您应该尝试避免。取而代之的是,使默认值仅对那些读取值的 classes 保持不变:

实现 IDefaultsProvider 的对象如何获取它们的值与 AbstractTemplate class 完全无关。

可能的解决方案

为了说的透彻一点,这里我要推倒重来了

在PHP中,IDefaultsProvider的接口可以这样写:

interface IDefaultsProvider {
    /** Returns the value of a variable identified by `$name`. */
    public function read($name);
}

接口是一个契约,上面写着:“当你有一个实现 IDefaultsProvider 的对象时,你可以使用它的 read() 方法读取默认值,它将 return 默认值您请求的值。

我将在下面进一步介绍接口的具体实现。首先,让我们看看 AbstractTemplate 的代码可能是这样的:

abstract class AbstractTemplate {
    private static function getDefaultsProvider() {
        // Let "someone else" decide what object to use as the provider.
        // Not `AbstractTemplate`'s job.
        return Defaults::getProvider();
    }

    private static function readDefaultValue($name) {
        return static::getDefaultsProvider()->read($name);
    }

    public function __construct(
        , $appLayoutsPath = static::readDefaultValue('app_layouts_path')
        , $moduleLayoutsPath = static::readDefaultValue('module_layouts_path')
        , $moduleTemplatesPath = static::readDefaultValue('module_templates_path')
    ) {
        //...
    }
}

我们已经删除了 Constants 及其成员(const APP_LAYOUTS_PATH 等)。 AbstractTemplate 现在幸福地不知道默认值的来源。 现在,AbstractTemplate 和默认值是 松散的 耦合。

AbstractTemplate的实现只知道如何得到一个IDefaultsProvider对象(见方法getDefaultsProvider())。在我的示例中,我为此使用了以下 class:

class Defaults {
    /** @var IDefaultsProvider $provider */
    private $provider;

    /** @returns IDefaultsProvider */
    public static function getProvider() {
        return static::$provider;
    }

    /**
     * Changes the defaults provider instance that is returned by `getProvider()`.
     */
    public static function useInstance(IDefaultsProvider $instance) {
        static::$instance = $instance;
    }
}

至此,阅读 部分完成,因为AbstractTemplate 可以使用Defaults::getProvider() 获得默认提供程序。接下来我们看看bootstrapping。这是我们可以开始处理不同场景的地方,例如测试、开发和生产。

为了测试,我们可能有一个名为 bootstrap.test.php 的文件,只有在 运行 进行测试时才会包含该文件。需要在AbstractTemplate之前包含,当然:

<?php
// bootsrap.test.php
include_once('Defaults.php');
include_once('TestingDefaultsProvider.php');
Defaults::useInstance(new TestingDefaultsProvider());

其他场景也需要自己的引导程序。

<?php
// bootsrap.production.php
include_once('Defaults.php');
include_once('ProductionDefaultsProvider.php');
Defaults::useInstance(new ProductionDefaultsProvider());

...等等。

还有待完成的是 IDefaultProvider 的实现。让我们从 TestingDefaultsProvider:

开始
class TestingDefaultsProvider implements IDefaultsProvider {
    public function read($name) {
        return $this->values[$name];
    }

    private $values = [
        'app_layouts_path' => '[app-root-path]/Layouts/Default',
        'module_layouts_path' => '[module-root-path]/Templates/Layouts',
        'module_templates_path' => '[module-root-path]/Templates/Templates',
        // ... more defaults ...
    ];
}

实际上可能就这么简单。

假设在生产环境中,我们希望配置数据驻留在配置文件中:

// defaults.json
{
    "app_layouts_path": "[app-root-path]/Layouts/Default",
    "module_layouts_path": "[module-root-path]/Templates/Layouts",
    "module_templates_path": "[module-root-path]/Templates/Templates",
    // ... more defaults ...
}

为了获得文件中的默认值,我们需要做的就是读取一次,解析 JSON 数据并在请求时 return 默认值。为了这个例子,我将采用惰性读取和解析。

class ProductionDefaultsProvider implements IDefaultsProvider {
    public function read($name) {
        $parsedContent = $this->getAllDefaults();
        return $parsedContent[$name];
    }

    private static $parsedContent = NULL;

    private static function getAllDefaults() {
        // only read & parse file content once:
        if (static::$parsedContent == NULL) {
            static::$parsedContent = static::readAndParseDefaults();
        }
        return static::$parsedContent;
    }

    private static readAndParseDefaults() {
        // just an example path:
        $content = file_get_contents('./config/defaults.json');
        return json_decode($content, true);
    }
}

这是全部内容:

结论

Is there a better alternative to provide default values?

是的,前提是值得付出努力。关键原理是inversion of control(也就是IoC)。我的示例的目的是展示如何实现 IoC。您可以将 IoC 应用于配置数据、复杂的对象依赖项,或者在您的情况下,应用于默认值。

如果您的应用程序中只有几个默认值,则反转控制可能会过大。如果您的应用程序中有大量默认值,或者如果您不能期望默认值、配置变量等的数量在未来保持非常低,您可能需要研究 依赖注入 .

Inversion of Control is too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.

Martin Fowler

另外,这个:

Basically, instead of having your objects creating a dependency or asking a factory object to make one for them, you pass the needed dependencies in to the object externally, and you make it somebody else's problem.

 — SO Answer to "What is Dependency Injection?" by wds

好消息是周围有很多 DI 框架:

  1. 我可能会选择选项 #2。我喜欢把事情分开(使用关注点分离原则)和代码重用(不要重复自己原则)。如果您打算在多个 classes 中重用默认值,我并不能立即从问题中清楚地知道。如果这样做,选项 #2 会更好,因为您只需在一处更改实际的字符串默认值。

  2. 不是真的。您正在以某种方式创建具有默认参数的类型。假设你的 Constants class 是 int 的一种。您的 class 取决于整数类型是否存在真正的问题?有时您必须在 class 中包含一些变量,而 Constants 就是这些变量之一。

  3. 您的 class 将始终取决于 Constants,因此您将无法轻松地换入和换出不同的常量。如果您想要一组不同的常量用于测试或其他环境(开发、生产、测试​​等),这可能是个问题

  4. 我个人认为我会将默认值卸载到配置文本文件中,这在不同的环境中可能会有所不同

配置文件方式

文件名为 'config/my-config.php';

/**
 * Config for default
 */
return array(
    'APP_LAYOUTS_PATH' => '[app-root-path]/Layouts/Default'
);

在您的申请中:

$config = require 'config/my-config.php';
echo $config['APP_LAYOUTS_PATH'];  //'[app-root-path]/Layouts/Default';

If-then-else方式(可以结合配置文件)

if ($mySpecificCondition)
    $appLayoutsPath = '[app-root-path]/Layouts/Default';
else
    $appLayoutsPath = '[app-root-path]/Layouts/NonDefault';

switch ($mySpecificCondition)
{
    case 'prod':
        $configFile= 'config_path/production.config.php';
        break;

    case 'devel':
        $configFile= 'config_path/development.config.php';
        break;

    case 'test':
        $configFile= 'config_path/test.config.php';
        break;
}

$config = require $configFile;

澄清一下,您可能会遇到这样的情况,即您在不同环境中拥有相同的文件名但内容不同。或者您可能希望根据条件使用不同的参数。以上给出了一些思路。 就我而言,我将这两种方法用于不同的事情。即,对于 prod/development 的 email/IP 配置,我有相同的文件名和不同的内容。但是对于操作系统默认字体文件夹放置之类的东西,我使用 if/then/else。 if (OS == WIN) $path = X; else $path = Y;

还要记得使用Keep It Simple原则。您以后总是可以重构您的设计。当然,考虑一下您的设计在未来会如何发挥作用,但在不得已之前不要让它过于复杂。

你可以为两者争论,你不会被磨损,因为它只是一个从任何 "real" 代码中分离出来的例子。但在我看来,这应该是您的 起始代码:

abstract class AbstractTemplate {
    const DEFAULT_APP_LAYOUTS_PATH = '[app-root-path]/Layouts/Default';
    const DEFAULT_MODULE_LAYOUTS_PATH = '[module-root-path]/Templates/Layouts';
    const DEFAULT_MODULE_TEMPLATES_PATH = '[module-root-path]/Templates/Templates';

    public function __construct(
        $appLayoutsPath = AbstractTemplate::DEFAULT_APP_LAYOUTS_PATH, 
        $moduleLayoutsPath = AbstractTemplate::DEFAULT_MODULE_LAYOUTS_PATH, 
        $moduleTemplatesPath = AbstractTemplate::DEFAULT_MODULE_TEMPLATES_PATH
    ) {
        //...
    }

}

所以你有自己的常量,你可以重用它(希望在同一个 class 中或任何 class 使它成为 "real",而不是在它之外!)

如果您要在许多不同的 classes 中重用它的常量,您可能会争辩说 "class Constants" 会更好 。这种方法的问题在于它违背了非常基本的 OOP 原则。那就是:你应该只有一个对象用这些路径为你做某事。如果您需要完成不同的事情,您只需以许多不同的方式在许多不同的对象中重用这个对象...

此外,单元测试或依赖注入和控制反转不会在此处改变任何内容。但是,如果您提供的代码 "know" 将 与 IoC 一起使用,您可能会争论是否有任何默认值是个好主意,如果它不会更好从容器中注入所有东西...