清理代码
我完全同意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 框架: