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
相结合。
我想问你:
第二个选项可以测试没有问题吗?
是否有另一种具体的可能性来提供默认值,同时保持良好的可测试性?
感谢您的回答,感谢您抽出宝贵时间。
选项 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?
问题不在于引入了依赖关系,而是何种依赖关系。从应用程序的 specific、named 成员读取值是 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.
另外,这个:
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 框架:
我可能会选择选项 #2。我喜欢把事情分开(使用关注点分离原则)和代码重用(不要重复自己原则)。如果您打算在多个 classes 中重用默认值,我并不能立即从问题中清楚地知道。如果这样做,选项 #2 会更好,因为您只需在一处更改实际的字符串默认值。
不是真的。您正在以某种方式创建具有默认参数的类型。假设你的 Constants
class 是 int
的一种。您的 class 取决于整数类型是否存在真正的问题?有时您必须在 class 中包含一些变量,而 Constants
就是这些变量之一。
您的 class 将始终取决于 Constants
,因此您将无法轻松地换入和换出不同的常量。如果您想要一组不同的常量用于测试或其他环境(开发、生产、测试等),这可能是个问题
我个人认为我会将默认值卸载到配置文本文件中,这在不同的环境中可能会有所不同
配置文件方式
文件名为 '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 一起使用,您可能会争论是否有任何默认值是个好主意,如果它不会更好从容器中注入所有东西...
开始之前:
我的问题主要基于意见而被搁置。但我已经尽力以更精确的方式重新编辑它。希望其内容将支持其重新开放。
所以我的问题又来了:
这是关于我的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
相结合。
我想问你:
第二个选项可以测试没有问题吗?
是否有另一种具体的可能性来提供默认值,同时保持良好的可测试性?
感谢您的回答,感谢您抽出宝贵时间。
选项 2 更简洁,更易于维护我会选择 2,代码提示应用程序如 phpStorm 或 DW 将更好地理解选项 2。
我强烈建议不要纯粹出于设计或风格考虑而避免使用静态 class 成员等完整功能,这是有原因的。
清洁代码
我完全同意
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?
问题不在于引入了依赖关系,而是何种依赖关系。从应用程序的 specific、named 成员读取值是 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.
另外,这个:
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 框架:
我可能会选择选项 #2。我喜欢把事情分开(使用关注点分离原则)和代码重用(不要重复自己原则)。如果您打算在多个 classes 中重用默认值,我并不能立即从问题中清楚地知道。如果这样做,选项 #2 会更好,因为您只需在一处更改实际的字符串默认值。
不是真的。您正在以某种方式创建具有默认参数的类型。假设你的
Constants
class 是int
的一种。您的 class 取决于整数类型是否存在真正的问题?有时您必须在 class 中包含一些变量,而Constants
就是这些变量之一。您的 class 将始终取决于
Constants
,因此您将无法轻松地换入和换出不同的常量。如果您想要一组不同的常量用于测试或其他环境(开发、生产、测试等),这可能是个问题我个人认为我会将默认值卸载到配置文本文件中,这在不同的环境中可能会有所不同
配置文件方式
文件名为 '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 一起使用,您可能会争论是否有任何默认值是个好主意,如果它不会更好从容器中注入所有东西...