我正在寻找一种在PHP 5.x中使用静态类实现Adapter模式的好方法。
我想使用它的一个例子是Python os.path.join()
的对应物。
我会有两个适应器,一个Windows和一个Linux适配器类。
我认为将这些类实现为静态类是合理的,因为它们没有“上下文”。他们不需要存储任何状态并且每次我需要时创建一个实例似乎是多余的 - 因此我正在寻找一种干净的方式来实现它。
让我们考虑以下虚假实施:
static public function join(){
$parts = func_get_args();
$joined = array(MY_MAGICALLY_PASSED_DEFAULT_PATH_PREFIX);
foreach($parts as $part){
$part = self::$adaptee->cleanPath($path);
if(self::$adaptee->isAbsolute($part)){
$joined = array($part);
}
else{
$joined[] = $part;
}
}
return implode(PATH_SEPARATOR, $joined);
}
首先要注意的是,它假定一个名为adaptee的初始化静态成员,该成员将保留必要的,依赖于操作系统的实现细节。
这要求我有一个任意命名的类似静态构造函数的函数,我会在声明类之后立即调用它。 (用这种方法困扰我的另一件事)。
当然,我可以在每个方法调用上初始化一个本地$adaptee
变量,但这似乎是不合适的,我将不得不在需要适配器的每个其他静态函数中复制它。
现在......对于PHP的类实现细节:它们不是第一类对象,所以我不能只将类作为参数传递。在该示例中,它要求我将Adaptees创建为非静态(这是什么术语?)类,然后实例化它并最终将其分配给静态$adaptee
成员变量适配器类。
也许这就是我所拥有的这种奇怪且完全主观的想法......但我真的觉得这样做是不合适的。您对更好的实施有什么想法吗?
我所拥有的另一个想法是,存储适配器的类名,而是使用call_user_func
,但我觉得使用这种方法感觉不太舒服。
我可能没有正确描述,所以我会尝试在更新中解释这个:
我不是在寻找如何获得底层操作系统,但我想有一个简洁的方法,因为静态类的行为会有所不同,具体取决于操作系统是Linux,Windows,FreeBSD还是其他。
我想到了适配器模式,但由于我没有静态构造函数,我无法真正初始化类。一种方法是在每次公共静态方法调用开始时初始化它(或者只是检查它是否被初始化)。
另一种可能性是,创建一个类似静态构造函数的方法,并在声明后立即调用它。这可能会有所帮助,但我只是想知道还有什么其他的,可能更为重要的方法来实现这一目标。
至于我的初步例子: 它应该是一个实用函数,它不需要真正保留任何类型的状态,所以我不是在寻找任何类型的Path-Object。我想要的是一个Path工厂函数,它返回一个字符串,而不必在每次调用时区分不同的操作系统。 “library”-thing导致我为我的相关实用程序函数创建一个静态类作为伪命名空间,以及需要支持适配器模式的不同实现细节。现在我正在寻找一种优雅的方式,将两者结合起来。
答案 0 :(得分:3)
当你让它们静止时,你会用脚射击自己。你不能注入静态类,所以你总是会耦合到全局范围,因为你会在任何地方硬编码静态调用,维护它们将成为一场噩梦。你也不能模仿它们(好吧,PHPUnit可以,但它只能启用测试代码,否则将是不可测试的)。
只需创建一个实例并使用常规功能即可省去一些后顾之忧。使用静力学没有任何优势。而且性能影响完全可以忽略不计。
我可能会为适配器和适配器创建一个接口来实现
interface IPathAdapter
{
public function cleanPath($path);
public function isAbsolutePath($part);
// more …
}
然后做类似
的事情class Path implements IPathAdapter
{
protected $_adapter;
public function __construct(IPathAdapter $adapter)
{
$this->_adapter = $adapter;
}
public function cleanPath($path)
{
$this->_adapter->cleanPath($part);
}
public function isAbsolutePath($part)
{
$this->_adapter->isAbsolutePath($part);
}
// more …
public function join(){
$parts = func_get_args();
$joined = array($this->getScriptPath());
foreach($parts as $part){
$part = $this->cleanPath($path);
if ($this->isAbsolutePath($part)){
$joined = array($part);
} else{
$joined[] = $part;
}
}
return implode($this->getPathSeparator(), $joined);
}
}
所以当我想使用Path时,我必须做
$path = new Path(new PathAdapter_Windows);
如果你不能注入适配器,我可能会去你已经建议的路由,并传递Adapter类名作为参数,然后从Path中实例化它。或者我将适当的适配器的检测完全留给Path类,例如让它检测操作系统,然后实例化所需的内容。
如果您想自动检测,请查看Does PHP have a function to detect the OS it's running on?。我可能会编写一个单独的类来处理标识,然后使它成为Path类的依赖项,例如
public function __construct(IDetector $detector = NULL)
{
if($detector === NULL){
$detector = new OSDetector;
}
$this->_detector = $detector;
}
我注射的原因是因为它允许我改变实施,例如在UnitTests中模拟Detector,但也可以忽略在运行时注入。然后它将使用默认的OSDetector。使用检测器,检测操作系统并在Path或专用工厂中的某处创建适当的适配器。
答案 1 :(得分:0)
我认为您可以这样做,您只需将命名空间路径放入全局变量中,例如在composer autoload.php中:
$ GLOBALS ['ADAPTED_CLASS_NAMESPACE'] ='MyComponent \ AdapterFoo \ VendorBar';
我认为在不能使用依赖注入的上下文中这是一种很好的方法,即在实体中进行验证(我们记住,分离的验证类更好)。
<?php
namespace MyComponent;
use MyComponent\AdaptedInterface;
use ReflectionClass;
class Adapter
{
/**
* @var AdaptedInterface
*/
protected $adaptedClass;
public function __construct(AdaptedInterface $validator = null)
{
if (null == $validator && $this->validateClassPath($GLOBALS['ADAPTED_CLASS_NAMESPACE'])) {
$this->adaptedClass = new $GLOBALS['ADAPTED_CLASS_NAMESPACE'];
} else {
$this->adaptedClass = $validator;
}
}
protected function validateClassPath($classPath)
{
$reflection = new ReflectionClass($classPath);
if (!$reflection->implementsInterface(
'MyComponent\AdaptedInterface'
)) {
throw new \Exception('Your adapted class have not a valid class path :' . $classPath . 'given');
}
return true;
}
}
所以任何地方:
(new Adapter())->foo($bar);