简体   繁体   中英

How to inject an interface into a concrete class hierarchy

So, I have an existing class hierarchy that I can't modify. There are existing consumers of classes in that hierarchy in more than just my codebase. I have another class (in a new, but external library) that has a different contract (class prototype) with similar but improved functionality. I wish to provide that new functionality to existing consumers of the old code.

class OldBase {}

class OldSubClass extends OldBase{}

class NewCode {}

//consumers
existingMethod(OldSubClass $c) {}
alsoExistingMethod(OldBase $c) {}

I thought of using an AdapterInterface , but this seems, perhaps, inelegant.

interface NewCodeAdapterInterface
{
     //interface that mimics the contract from OldBase
}
class NewCodeImplementation implements NewCodeAdapterInterface{}

//now this code can not be used with any existing OldBase objects :-\
existingMethod(NewCodeAdapterInterface $c) {}

I'd like to ensure a backwards compatible way to allow old code to be used while allowing a clean way to use the new with as few ramifications as possible, but how?

Starting with the premise that you want to implement a unified replacement of disparate classes consumed by existing code, without modifying the existing consumers , then I have a... "solution".

Here's an example of the current problem:

class A
{
    public function test()
    {
        echo "A\n";
    }
}

class B
{
    public function test()
    {
        echo "B\n";
    }
}

class Consumer
{
    public function runTestA(A $a)
    {
        $a->test();
    }

    public function runTestB(B $b)
    {
        $b->test();
    }
}

$con = new Consumer();

$a = new A();
$b = new B();

$con->runTestA($a);
$con->runTestB($b);

You're trying to find a solution that will allow something like, without modifying anything in Consumer:

$con = new Consumer();

$c = new C();

$con->runTestA($c);
$con->runTestB($c);

I'm going to heavily advise against doing what I'm about to outline. It would be better to modify the method signatures in Consumer to allow a new class to be passed that has the joint functionality. But, I'm going to answer the question as asked...

To start with, we need a couple of classes which can pass any existing method signatures. I'll use a trait to define the joint functionality.

trait ExtensionTrait
{
    public function test()
    {
        echo "New Functionality\n";
    }
}

class ExtendedA extends A
{
    use ExtensionTrait;
}

class ExtendedB extends B
{
    use ExtensionTrait;
}

Now we have some classes with the new functionality, which can pass the method checks... if we pass the right one. So, how do we do that?

Let's first put together a quick utility class that allows easy switching between the two classes.

class ModeSwitcher
{
    private $a;
    private $b;
    public $mode;

    public function __construct($a, $b)
    {
        $this->a = $a;
        $this->b = $b;
        $this->mode = $this->a;
    }

    public function switchMode()
    {
        if ($this->mode instanceof ExtendedA)
        {
            $this->mode = $this->b;
        }
        elseif ($this->mode instanceof ExtendedB)
        {
            $this->mode = $this->a;
        }
    }

    public function __set($name, $value)
    {
        $this->a->$name = $value;
        $this->b->$name = $value;
    }

    public function __isset($name)
    {
        return isset($this->mode->$name);
    }

    public function __unset($name)
    {
        unset($this->a->$name);
        unset($this->b->$name);
    }

    public function __call($meth, $args)
    {
        return call_user_func_array([$this->mode, $meth], $args);
    }

}

This mode switcher class maintains a current mode class, which passes through gets and calls. Sets and unsets are applied to both classes, so any properties modified aren't lost upon a mode switch.

Now, if we can modify the consumer of the consumer, we can put together a translation layer that automatically switches between modes to find the correct mode.

class ConsumerTranslator
{
    private $consumer;

    public function __construct(Consumer $consumer)
    {
        $this->consumer = $consumer;
    }

    public function __get($name)
    {
        return $this->consumer->$name;
    }

    public function __set($name, $value)
    {
        $this->consumer->$name = $value;
    }

    public function __isset($name)
    {
        return isset($this->consumer->$name);
    }

    public function __unset($name)
    {
        unset($this->consumer->$name);
    }

    public function __call($methName, $arguments)
    {
        try
        {
            $tempArgs = $arguments;
            foreach ($tempArgs as $i => $arg)
            {
                if ($arg instanceof ModeSwitcher)
                {
                    $tempArgs[$i] = $arg->mode;
                }
            }
            return call_user_func_array([$this->consumer, $methName], $tempArgs);
        }
        catch (\TypeError $e)
        {
            $tempArgs = $arguments;
            foreach ($tempArgs as $i => $arg)
            {
                if ($arg instanceof ModeSwitcher)
                {
                    $arg->switchMode();
                    $tempArgs[$i] = $arg->mode;
                }
            }
            return call_user_func_array([$this->consumer, $methName], $tempArgs);
        }
    }
}

Then, we can use the combined functionality like so:

$con = new Consumer();
$t = new ConsumerTranslator($con);
$a = new ExtendedA();
$b = new ExtendedB();
$m = new ModeSwitcher($a, $b);

$t->runTestA($m);
$t->runTestB($m);

This allows you to interchangeably utilize either class tree without any modification of Consumer whatsoever, nor any major changes to the usage profile of Consumer, as the Translator is basically a passthrough wrapper.

It works by catching the TypeError thrown by a signature mismatch, switching to the paired class, and trying again.

This is... not recommended to actually implement. The constraints declared provided an interesting puzzle, though, so, here we are.

TL;DR: Don't bother with any of this mess, just modify the consuming contract and use a joint interface, like you were intending.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM