Adapter pattern with static classes
I am looking for a good way to implement the Adaptor pattern with static classes in PHP 5.x.
One of the examples where I would like to use this, is as a counterpart to Python's os.path.join()
.
I would have two adaptees, a Windows and a Linux adaptee class.
I think it is reasonable, to implement these classes as static classes, because they have no "context". They do not need to store any state and creating an instance everytime I need one seems superfluous - therefore I am looking for a clean way to implement this.
Let's consider the following bogus implementation:
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);
}
The first thing you will notice is, that it assumes an initialized static member called adaptee which would hold the necessary, OS-dependent implementation details.
This requires me to have an arbitrarily named static constructor-like function, that I would call immediately after the declaration of the class. (Another thing that bothers me with this approach).
Of course, I could initialize a local $adaptee
variable on each method c开发者_StackOverflowall, but that seems like inappropriate and I would have to replicate that in each other static function that needs the adaptee.
Now... for PHP's class implemention detail: They are not first-class objects, so I couldn't just pass the class as an argument. In the example, it requires me to create the Adaptees as non-static (what is the term for this?) classes, then instantiate it and eventually assign it to the static $adaptee
member variable of the Adapter class.
Maybe this is just this weird and completely subjective thought that I have... but I really feel that it is not appropriate to do it like this. Do you have any ideas about a better implementation?
One other idea that I've had is, to store the adaptee's class name instead, and use call_user_func
instead, but I don't feel too comfortable using this approach.
Update
I may not have described this properly, so I will try to explain this in an update:
I am not looking on how to get the underlying Operating System, but I would like to have a neat way, for a static class to act differently depending on whether the OS is Linux, Windows, FreeBSD or something else.
I thought of the adaptor pattern, but since I don't have a static constructor, I cannot really initialize the class. One way would be to initialize it at the beginning of every public static method call (or just check whether it is initialized).
The other possibility would be, to create a static constructor-like method and simply call it right after the declaration. That might do the trick, but I am just wondering what other, possibly more elgeant methods there are, to achieving this.
As for my initial example: It is supposed to be a utility function, it does not need to preserve state in any kind really, so I am not looking for a Path-Object of any sorts. What I would like, is a Path factory function, that returns a string, without having to differentiate between the different OSes every time when called. The "library"-thing led me to create a static class as pseudo-namespace for my related utility functions, and the different implementation details that need to be supported to the adaptor pattern. Now I am looking for an elegant way, to combine the two.
You'll shoot yourself in the foot when you make them static. You cannot inject static classes so you will always have coupling to the global scope and because you will hardcode static calls everywhere, maintaining them will become a nightmare. And you cannot mock them either (ok, PHPUnit can, but it only does to enable testing of code that otherwise would be untestable).
Just create an instance and use regular functions and save yourself some worries. There is no advantage in using statics. And the performance impact is utterly and totally negligible.
I'd probably create an interface for the adaptee and the adapters to implement
interface IPathAdapter
{
public function cleanPath($path);
public function isAbsolutePath($part);
// more …
}
and then do probably something like
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);
}
}
So when I want to use Path, I'd have to do
$path = new Path(new PathAdapter_Windows);
If you cannot inject the adapters, I'd probably go the route you already suggested and pass the Adapter class name as an argument to instantiate it from within Path then. Or I'd leave the detection of the appropriate adapter completely to the Path class, e.g. have it detect the OS and then instantiate what is needed.
If you want to autodetect, have a look at Does PHP have a function to detect the OS it's running on?. I'd probably write a separate class to handle the identification and then make it a dependency to the Path class, e.g.
public function __construct(IDetector $detector = NULL)
{
if($detector === NULL){
$detector = new OSDetector;
}
$this->_detector = $detector;
}
The reason I am injecting is because it will allow me to change the implementation, e.g. to mock the Detector in UnitTests but can also ignore to inject at runtime. It will use the default OSDetector then. With the detector, detect the OS and create an appropriate adapter somewhere in Path or in a dedicated Factory.
I think you can do this, you just have to put the namespace path into a global var, for example in composer autoload.php:
$GLOBALS['ADAPTED_CLASS_NAMESPACE'] = 'MyComponent\AdapterFoo\VendorBar';
I think it's a good approach in a context where you can't use dependency injection i.e in a entity for validation (we keep in mind that separated Validation classes are better).
<?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;
}
}
So anywhere:
(new Adapter())->foo($bar);
精彩评论