【问题标题】:PHP MVC: Default values for the constructor parameters [closed]PHP MVC:构造函数参数的默认值[关闭]
【发布时间】:2017-06-07 10:26:44
【问题描述】:

开始之前:

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

这又是我的问题:

这是关于我的 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. 是否有另一种具体的可能性来提供默认值并同时保持良好的可测试性?

感谢您的回答并感谢您的宝贵时间。

【问题讨论】:

  • 花时间考虑这个问题没有任何好处。使用选项 1。它已经过试验和测试,可以满足您的需求。 如果需要一个 Constant 类(我认为这不会发生),你仍然可以稍后重构它。
  • 是时候停止使用abstractextends 关键字了!更喜欢组合而不是继承。定义干净简单的接口,让客户端来休息。
  • 不知道您为什么决定编辑您的问题以表达感谢/谈论对您的问题的反馈等。这只会使您的问题变得混乱。让你的问题“切中要害”。
  • @DavidMakogon 嗨。感谢您的建议。我重新编辑了。回答你:这个问题之前有一个赏金。在它被“搁置”的那一刻,赏金变得无效(至少我认为是这样,因为声誉被重新分配到我的帐户)。但是所有花时间回答我的问题的用户都应该知道,他们的努力没有白费:我试图重新提出问题和赏金。

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


【解决方案1】:

您可以同时支持两者,而且您不会感到厌烦,因为它只是从任何“真实”代码中分离出来的示例。但在我看来,这应该是您的起始代码

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

}

所以你有你的常量,你可以重用它(希望在同一个类或任何使它“真实”的类中,而不是在它之外!)

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

单元测试或依赖注入和控制反转在这里也不会改变任何东西。但是,如果您提供的代码是您“知道”且与 IoC 一起使用的,您可能会争论是否有任何默认值是个好主意,以及从容器中注入所有内容是否更好...

【讨论】:

  • 嗨。首先:感谢您的回答!我感谢你的努力。如您所知,该问题自 6 月 9 日起被搁置。无论我做什么,无论我重新编辑了多少次,我都无法重新打开它,因此,重新激活了 100 个声誉的赏金 :-( 但我发现你的答案适合我的情况,例如我的项目的星座。其他用户也有很大的贡献!
  • 人们认为积分并不重要,所以不用担心;D
  • 您的一般方法 - 关于(打破)OOP 原则和关于强制注入依赖项而不需要默认值的方法,让我决定您的答案应该被接受一。现在,当然,我想用你应得的赏金来奖励你对这个主题的远见。但是,由于这个问题被搁置了,明天可能会关闭,我不知道我该如何实现这一点。请问有这个...困境的答案吗?再次感谢您,我正在等待您的想法。
  • 哦,嗨 :-) 我刚刚同时给你写信。
  • @smentek,你是说没有单独的常量类对 OOP 更好吗?我认为关注点分离将说明有一个单独的常量类。我理解你不应该将常量分开,而是嵌入到 AbstractTemplate 类和任何其他扩展 AbstractTemplate 的类中。正确的?如果是这样,那么 SOC 呢?
【解决方案2】:
  1. 我可能会选择选项 #2。我喜欢将事情分开(使用关注点分离原则)和代码重用(不要重复自己的原则)。如果您打算在多个类中重用默认值,我并不清楚这个问题。如果这样做,选项 #2 会更好,因为您只需在一处更改实际的字符串默认值。

  2. 并非如此。您在某种程度上创建了一个具有默认参数的类型。想象一下您的Constants 类是int 的一种类型。是否存在您的类依赖于整数类型的真正问题?有时你的类中必须有一些变量,Constants 就是这些变量之一。

  3. 您的类将始终依赖于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;

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

还记得使用 Keep It Simple 原则。您可以随时重构您的设计。当然,请考虑一下您的设计在未来将如何发挥作用,但不要让它过于复杂,除非您必须这样做。

【讨论】:

  • 丹尼斯,非常感谢您的回答。我肯定从中吸取了教训。我想让你知道,我真的很感谢你的努力!
【解决方案3】:

清理代码

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

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

语言特性存在,但这并不一定意味着它们应该用于这个特定目的。此外,问题不在于风格,而在于(去)耦合。

相互依赖

解决你的第三点:

...类常量正在引入这种相互依赖/耦合...

这里没有相互依赖,因为AbstractTemplate 依赖于Constants,但Constants 没有依赖关系。可以测试您的第二个选项,但它不是很灵活。

耦合

在第二点你说:

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

问题不在于 引入了依赖关系,而在于 什么样的 依赖关系。从应用程序的特定命名 成员读取值是紧密 耦合,您应该尽量避免这种情况。相反,只为那些读取值的类设置默认值:

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

可能的解决方案

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

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

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

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

我将在下面进一步介绍该接口的具体实现。首先,让我们看看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() 获取默认提供程序。接下来让我们看看引导。在这里,我们可以开始处理不同的场景,例如测试、开发和生产。

对于测试,我们可能有一个名为 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 应用于配置数据、复杂的对象依赖关系,或者在您的情况下,应用于默认值。

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

控制反转是一个过于笼统的术语,因此人们会感到困惑。经过与各种 IoC 倡导者的大量讨论,我们确定了依赖注入这个名称。

——Martin Fowler

还有,这个:

基本上,不是让您的对象创建依赖项或要求工厂对象为它们创建依赖项,而是将所需的依赖项从外部传递给对象,然后将其变成其他人的问题。

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

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

【讨论】:

  • 为什么投反对票?
  • 我可以同意将默认值移动到配置文件中,但建议的实现完全是矫枉过正,并增加了不必要的复杂性。我的意思是认真的?所有用于指定默认值的代码?选项 1 简单明了。
  • @Gordon 谢谢你的澄清 :-) 我同意你的观点,只使用几个默认值就过分了,但我在回答中写道:如果你只有几个默认值应用程序中的值...。我说得不够清楚,对此感到抱歉;我更新了我的答案。我的信息是,如果不只是几个默认值,DI 可能值得研究。
  • 是的,我见过。但它在所有其他文本中都丢失了。我认为当原始问题如此简单并且OP在这种情况下不应该使用所有代码时,解释过度​​杀伤解决方案几乎没有价值。但我也为此感到内疚。我将收回 dv,因为答案是非常彻底的努力。
  • @Gordon 你是对的,确实如此。感谢您的好评,非常感谢:-)
【解决方案4】:

选项 2 是更简洁、更易于维护的代码示例,我会使用 2,像 phpStorm 或 DW 这样的代码提示应用程序会更好地理解选项 2。

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

【讨论】:

  • 与在使用它们的类中保持默认值相比,op2 如何更简洁或更易于维护?你能扩展一下这个观点吗?持有各种常量的随机混合的 Constant 类不是有效地注册了所有相关的气味吗?
  • 自从我回答后,这个问题被大量编辑,答案通常与类常量与硬编码数据数组有关
  • @Gordon 是的,George Dryser 完全正确。从第一个版本开始,我大量重新编辑了原始问题,因为它被搁置了。我明白了,仍然没有效果。无论如何,乔治的回答是针对第一个版本给出的,并且完全“在主题上”。
  • 乔治,谢谢你的回答!我感谢你的努力!你是对的:第二种选择是更清洁的选择。但我意识到,如果我在多个类(不相关的类)中使用它,我就会破坏一些基本的 OOP 原则。所以,我决定使用“修改”选项 1,正如@smentek 所介绍的那样。再次感谢你! :-)
  • 对于“搁置的问题”和我必须进行的许多编辑的误解,我深表歉意,希望问题能够重新打开,因此赏金将重新生效再次。
猜你喜欢
  • 2016-07-06
  • 2010-09-16
  • 1970-01-01
  • 2014-05-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-07-09
  • 1970-01-01
相关资源
最近更新 更多