Liskov替换原则和使用继承类的正确方法

时间:2015-12-07 13:03:51

标签: php oop inheritance architecture liskov-substitution-principle

我有一些处理程序(“控制器”)类,他们可以用某种方式处理项目:

interface IHandler
{
    public function execute(Item $item);
}

class FirstHandler implements IHandler
{
    public function execute(Item $item) { echo $item->getTitle(); }
}

class SecondHandler implements IHandler
{
    public function execute(Item $item) { echo $item->getId() . $item->getTitle(); }
}

class Item
{
    public function getId() { return rand(); }
    public function getTitle() { return 'title at ' . time(); }
}

但是我需要在子Item类中添加一些新功能:

class NewItem extends Item
{
    public function getAuthor() { return 'author ' . rand(); }
}

并在SecondHandler

中使用它
class SecondHandler implements IHandler
{
    public function execute(Item $item) { printf('%d %s, author %s', $item->getId(), $item->getTitle(), $item->getAuthor()); }
}

但是Item类实际上没有getAuthor方法。并且,如果我尝试在SecondHandler类中更改accept方法的签名,我将捕获有关声明兼容性的E_STRICT错误。当然,这是一种LSP违规行为。

如何解决此问题?我是否需要两个接口,例如INewHandlerIHandler,具有execute方法的不同签名?但它是某种代码重复。

另外,我不能在处理程序中使用__constructor(Item $item)__construct(NewItem $item)(并且在没有参数的情况下使用execute方法),这将被视为更好的解决方案:它们必须是不可变的且只有单个实例应用程序生命周期中允许的每个策略。

6 个答案:

答案 0 :(得分:2)

正如您自己发现的那样,PHP的类型提示实现有很多限制,使得场景(如您所描述的场景)比它们应该更难。在其他类型的语言中,如Java和Swift,您的实现绝对是合法的。

在对你的问题进行一些思考之后,我找到了Félix提出的解决方案,但是我认为与问题相比它的设计太多了。

我对你的问题的回答不是解决方案,而是我在用PHP多年开发后给你的建议:

放弃PHP中的类型提示,并以动态的方式开发......应该是

PHP与Ruby / Python / JavaScript更类似于Java / C ++,尝试从静态类型语言中复制1对1转换为强制和卷积实现。

解决您的实施问题很容易,所以不要过于复杂,并保持原样(KISS原则)。

声明方法'没有类型的参数,并在你真正需要的地方实现检查(例如抛出异常)。

interface IStrategy
{
    public function execute($item);
}

class FirstStrategy implements IStrategy
{
    public function execute($item) {
        echo $item->getTitle();
    }
}

class SecondStrategy implements IStrategy
{
    public function execute($item) {
        // execute(NewItem $item) is identical to this check.
        if (! $item instanceof NewItem) {
            throw new Exception('$item must be an instance of NewItem');
        }
        echo $item->getAuthor();
    }
}

class Item
{
    public function getId() { return rand(); }
    public function getTitle() { return 'title at ' . time(); }
}

class NewItem extends Item
{
    public function getAuthor() { return 'author ' . rand(); }
}

再一次,不要用Java思考,而是尽量遵循鸭子打字的方式。

如果可能的话,尽量不要严格强制参数的类型,但要根据可用的界面调整代码的行为(Duck Typing)。

class SecondStrategy implements IStrategy
{
    public function execute($item) {
        $message = $item->getTitle();

        // PHP 5 interface availability check.
        if (is_callable([$item, 'getAuthor'])) {
            $message .= ' ' . $item->getAuthor();
        }

        // With PHP 7 is even better.
        // try {
        //     $message .= ' ' . $item->getAuthor();
        // } catch (Error $e) {}

        echo $message;
    }
}

我希望能帮到你。 ^ _ ^

答案 1 :(得分:1)

@ daniele-orlando和@ ihor-burlachenko都提出了有效的观点。 考虑以下方法进行方法重载,这是一种妥协,应该可以很好地扩展:

interface IHandler
{
    /**
     * @param $item Item|NewItem
     */
    public function execute($item);

    // protected function executeItem(Item $item);
    // protected function executeNewItem(NewItem $item);    
}

trait IHandlerTrait
{   
    public function execute($item) 
    {
        switch(true) {
            case $item instanceof Item:
                return $this->executeItem($item);
            case $item instanceof NewItem:
                return $this->executeNewItem($item);
            default:
                throw new \InvalidArgumentException("Unsupported parameter type " . get_class($item));
        }
    }

    protected function executeItem(Item $item)
    {
        throw new \LogicException(__CLASS__ . " cannot handle execute() for type Item");
    }

    protected function executeNewItem(NewItem $item)
    {
        throw new \LogicException(__CLASS__ . " cannot handle execute() for type NewItem");
    }
}

class FirstHandler implements IHandler
{
    use IIHandlerTrait;

    protected function executeItem(Item $item) { echo $item->getTitle(); }
}

class SecondHandler implements IHandler
{
    use IIHandlerTrait;

    // only if SecondHandler still need to support `Item` for backward compatibility
    protected function executeItem(Item $item) { echo $item->getId() . $item->  getTitle(); }

    protected function executeNewItem(NewItem $item) { printf('%d %s, author    %s', $item->getId(), $item->getTitle(), $item->getAuthor()); }
}

答案 2 :(得分:0)

您确定要在此处使用策略模式吗?

看起来,这里的策略操作取决于它处理的元素的类型。在这种情况下,Visitor模式也适用于此处。

答案 3 :(得分:0)

目前,您似乎想要执行可扩展的数据记录(Item和NewItem)。请考虑执行一些可插入行为(通过接口实现)。

你的写作难以猜测这种行为会是什么,因为(新)项目只是你提供的例子中一个美化的数据结构。

答案 4 :(得分:0)

如果你想在另一个对象中使用对象进行操作/操作,你可以/应该使用接口。

interface IStrategy
{
    public function execute(ItemInterface $item);
}

interface ItemInterface 
{
   public function getTitle();
   .....
}

如果要扩展(New)Item类的公共功能,可以为newItem创建新接口

interface NewItemInterface extends ItemInterface 
{
...
}

class SecondStrategy implements IStrategy
{
    public function execute(NewItemInterface $item) 
    { .... }
}

或者你可以像其他人提到的那样使用一些实例检查。

答案 5 :(得分:0)

如果你的继承和建议SecondHandler应该首先处理Item和NewItem是正确的,那么你应该能够在公共接口后面隐藏这个功能。从您的示例中,可能会调用toString(),它可能是Item接口的一部分。

否则,您的设计最初可能有问题。而且你必须改变继承或处理项目的方式。或者其他我们不了解的事情。

另外,我不知道为什么你需要DTO,但似乎对Doctrine有一些误解。 Doctrine是一个ORM,它解决了你的持久性问题。它增加了与存储引入存储库的通信方式的限制,但它没有定义域逻辑。