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


Liskov substitution principle and proper way to use inherited classes

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

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(); }
}

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

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方法),这将被视为更好的解决方案:它们必须是不可变的,并且在应用程序生命周期中只允许每个策略的单个实例。

正如您自己所发现的,PHP的类型提示实现有很多局限性,这使得场景(如您所描述的场景)变得更加困难。在Java和Swift等其他类型语言中,您的实现是绝对合法的。

在思考了你的问题后,我找到了Félix提出的解决方案,但我认为与这个问题相比,它的设计太过了。

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

放弃PHP中的类型提示,以动态方式进行开发。

与Java/C++相比,PHP更类似于Ruby/Python/JavaScript,尝试从静态类型语言复制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;
    }
}

我希望能帮助你。^_^

@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()); }
}

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

看起来,战略的行动取决于它处理的元素的类型。在这种情况下,访问者模式可能也适用于此。

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

从你的写作中很难猜测这种行为会是什么,因为(New)Item在你提供的例子中只是一个美化的数据结构。

如果您想在另一个对象中处理/操作对象,您可以/应该使用接口。

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) 
    { .... }
}

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

如果您的继承和建议SecondHandler应该同时处理Item和NewItem,那么您应该能够将此功能隐藏在公共接口后面。从您的示例中,它可能被调用为toString(),它可能是Item接口的一部分。

否则,您最初的设计可能有问题。您必须更改您的继承或处理项目的方式。或者其他我们不知道的事情。

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

根据接口分离,请找到一些解决方案。

```

# based on interface segrigation.
interface BasicInfo
{
    public function getId();
    public function getTitle();
}
interface AuthorInfo
{
    public function getAuthor();
}
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 implements BasicInfo
{
    public function getId() { return rand(); }
    public function getTitle() { return 'title at ' . time(); }
}
class Item2 extends Item implements AuthorInfo
{
    public function getAuthor() { return 'author ' . rand(); }
}

但是我认为您不应该保留Item类的依赖关系。您应该编写一些重复的代码来保持类的可插入性/独立性。因此,开放/关闭原则也应该存在。