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

时间:2017-06-07 10:26:44

标签: php oop design-patterns model-view-controller solid-principles

开始之前:

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

所以这是我的问题:

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

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

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 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
    ) {
        //...
    }

}

这样我就可以将抽象类与具体实现Constants结合起来。

我想问你:

  1. 可以毫无问题地测试第二个选项吗?

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

  3. 感谢您的回答并感谢您的时间。

4 个答案:

答案 0 :(得分:0)

清洁代码

我完全同意George Dryser,因为选项2更清晰。我对他的第二点并不完全赞同:

  

我强烈建议不要完全避免整个功能,例如静态类成员纯粹出于设计或风格考虑,这是有原因的。

存在语言功能,但这并不一定意味着它们应该用于特定用途。而且,问题不仅仅是关于(去)耦合的风格。

依存

解决你的第三点:

  

... class Constants引入了这种相互依赖/耦合......

此处没有 inter 依赖关系,因为AbstractTemplate取决于Constants,但Constants没有依赖关系。你的第二个选项可以测试,但它不是很灵活。

联轴器

在你的第二点,你说:

  

是否存在真正的问题,使用选项2的每个类都将依赖于Constants类?

问题不是 引入了依赖关系,而是依赖关系的 。从应用程序的特定名称成员读取值是紧密耦合,您应该尽量避免。相反,使默认值仅对那些读取值的类保持不变:

Default Value Provider

实现IDefaultsProvider的对象如何获取它们的值根本不涉及AbstractTemplate类。

可能的解决方案

为了彻底,我将在这里重新发明轮子。

在PHP中,IDefaultsProvider的界面可以这样写:

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

接口是一个合同,上面写着:“当你有一个实现IDefaultsProvider的对象时,你可以使用它的read()方法读取默认值,它将返回你请求的默认值”。 / p>

我将在下面进一步介绍该接口的具体实现。首先,让我们看看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 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数据并在请求时返回默认值。为了这个例子,我会选择懒惰的阅读和阅读。解析。

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);
    }
}

这是整个shebang:

_

结论

  

是否有更好的替代方法来提供默认值?

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

如果您的应用程序中只有几个默认值,则反转控制可能过度。如果您的应用程序中有大量默认值,或者您不能指望将来默认值,配置变量等的数量保持很低,您可能需要查看依赖注入

  

控制倒置是一个过于笼统的术语,因此人们会发现它令人困惑。因此,经过与各种IoC倡导者的大量讨论,我们确定了名称Dependency Injection。

     

- Martin Fowler

另外,这个:

  

基本上,不是让对象创建依赖项或要求工厂对象为它们创建一个依赖项,而是将所需的依赖项传递给外部对象,并使其成为别人的问题。

     

- SO Answer to "What is Dependency Injection?" wds

好消息是有很多DI框架:

答案 1 :(得分:0)

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

  2. 不是真的。您正在创建一个具有默认参数的类型。想象一下,您的Constants类是int类型。您的类依赖于整数类型是否存在真正的问题?有时你的班级必须有一些变量,而Constants就是其中一个变量。

  3. 您的课程将始终取决于Constants,因此您无法轻松交换和输出不同的常量。如果你想为测试或其他环境(开发,生产,测试等)提供一组不同的常量,这可能是一个问题

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

  5. 配置文件方式

    名为&#39; config / my-config.php&#39;;

    的文件
    /**
     * 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的电子邮件/ IP配置。但对于像操作系统默认字体文件夹放置的东西,我使用if / then / else。 if (OS == WIN) $path = X; else $path = Y;

    还记得使用Keep It Simple原则。您可以随时重构您的设计。当然,想想你的设计将来会如何发挥作用,但在你不得不这样做之前,不要让它变得过于复杂。

答案 2 :(得分:0)

你可以争论两个,但你不会因为它只是一个从任何&#34;真实&#34;中脱离出来的例子。码。但在我看来,这应该是你的起始代码

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
    ) {
        //...
    }

}

所以你有你的常数,你可以重复使用它(希望在同一个班级或任何班级中使用它&#34;真实&#34;,而不是在它之外!)

你可以争辩说&#34;类常数&#34;如果要在许多不同的类中重用其中的常量,会更好。这种方法的问题在于它违背了非常基本的OOP原则。那就是:你应该只有一个对象可以为你做这个问题。如果你需要做不同的事情,你只需要以许多不同的方式在许多不同的对象中重用这个对象......

此外,单元测试或依赖注入和控制反转不会改变任何内容。但是,如果您提供的代码是您所知道的,那就是#34;只有 才能与IoC一起使用,你可以争论是否有任何默认设置是好主意,如果从容器注入所有东西不是更好......

答案 3 :(得分:-1)

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

我强烈建议不要完全避免整个功能,例如静态类成员纯粹出于设计或风格方面的考虑,这是有原因的。