【问题标题】:Liskov substitution principle and proper way to use inherited classesLiskov替换原则和使用继承类的正确方法
【发布时间】:2016-03-12 02:19:45
【问题描述】:

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

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 类中更改接受方法的签名,我将捕获E_STRICT 关于声明兼容性的错误。当然,这有点违反 LSP。

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

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

【问题讨论】:

  • 我相信在示例中使用较少通用的名称将有助于我们更好地识别和解决问题。
  • 有什么想法吗?你有“处理程序”类和简单的“DTO”类(Doctrine 实体、Yii ActiveRecord 等)
  • 如果Strategy is not a referencedesign pattern,请不要在我们面前放红鲱鱼。这看起来很像策略。
  • 这些项目之间的关系究竟如何?继承真的有必要吗?为什么不作曲?如果给出更具描述性的名称(暗示更具体的场景),我们可以做出更好的判断。
  • 已编辑。项目实际上是相关的,因为我使用 Doctrine 的类表继承,并且它们具有共享方法(getId、getTitle 等)

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


【解决方案1】:

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

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

【讨论】:

  • Strategy 只是一个类名,不参考设计模式。
  • @GuyFawkes 不管 ;) 我的建议对你的情况无效吗?
  • 可能是有效的 :) 你能提供一些解决 LSP 违规问题的例子吗?
【解决方案2】:

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

从您的文章中很难猜出这种行为会是什么,因为 (New)Item 在您提供的示例中只是一个美化的数据结构。

【讨论】:

  • 我认为就 Doctrine 实体(实际上是 Items)而言,向它们添加一些逻辑是错误的。可插拔行为是什么意思?
  • 不幸的是,我对“教义实体”或(我现在收集的是)PHP 中的 OO 实现一无所知。通常在支持它们的 OO 语言中,人们使用接口来提供具有共同行为(而不是状态)的对象之间的多态性。
  • 例如,在DTO类中存在一些逻辑就不是一个好主意(简单的类只用于其他类之间的值交换)
  • 我支持编写面向对象的代码,而不是让代码作用于数据的过程方法,尤其是将复杂的问题或领域建模为代码。抱歉,在这种情况下我觉得我无能为力。
  • 无法理解为什么 DTO 不是“OOP 编写”代码的一部分。
【解决方案3】:

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

在对您的问题进行了一些思考后,我得出了Félix 提出的解决方案,但与问题相比,我认为它过于工程化。

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

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

PHP 比 Java/C++ 更类似于 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;
    }
}

希望对你有所帮助。 ^_^

【讨论】:

  • 是的,我认为使用 instanceof 检查的解决方案要简单得多。在 Java 中我可以使用子类提示而不是父类?
  • Java支持方法重载,所以可以实现两种方法: - public void execute(Item item) {} - public void execute(NewItem item) {} 第一个满足接口定义,第二个一个是您对界面的扩展。
  • 为了完整起见,如果你想使用子类提示而不是“execute()”方法的重载,你可以。只需使用 Java 泛型重新实现您的代码。
  • 当这是您唯一需要这样代码的地方时,此解决方案很好。就质量/努力而言,我会同意的。但是,如果我们假设您需要在 100 个地方使用类似的 if 语句,那肯定会告诉我们您的设计有问题。
  • 您正在检查 NewItem 的类型,但为什么不检查 Item(在 FirstStrategy 中)?
【解决方案4】:

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

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】:

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

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

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

    【讨论】:

    • 我在所有处理程序处理后保存项目。而且我没有 Doctrine 的限制,我有项目“通用管道处理”的限制。
    • 如果所有项目都必须由所有处理程序处理,并且每个处理程序对每个项目的行为相同并且您的继承是正确的,那么您应该能够将特定于类的功能隐藏在公共接口后面。我不清楚你为什么决定 Doctrine 实体应该是 DTO。但是假设这些必须是 DTO,您仍然拥有具有相应继承层次结构的相应模型。在这种情况下,您只需将此类特定于模型的功能隐藏在模型的公共接口后面,然后将 DTO 转换为处理程序中的模型以调用它。
    • 是的,我想我需要额外的逻辑电平。我不喜欢在 Doctrine 实体中使用另一个逻辑然后“设置/获取”。可能有些程序员把实体当做“胖模型”,但我更喜欢“瘦控制器、瘦模型、胖服务层”
    • 好吧,你回答了你自己的问题。我仍然担心的是,使用这种继承层次结构,您的服务可能包含 if 语句,或者它们可能隐藏在某些行为模式后面(在我看来,这仍然是解决域设计问题的工程解决方案)。我建议你看看DDD。它可能会将您的注意力从瘦/胖视图转移到非常适合 OOP 范式的领域驱动方法。
    • 另外,看看 Martin Fowler 对本地 DTO 的看法:martinfowler.com/bliki/LocalDTO.html
    【解决方案6】:

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

    【讨论】:

    • 我猜,你想输入`protected function executeNewItem(NewItem $item);`
    • 您必须回滚您的更改。接口中没有方法可见性,它们都是公共的;)
    • @IhorBurlachenko,谢谢。学过的知识!很遗憾,现在除了抽象类之外,还有办法在任何地方公开私有方法。我能想到的最好的事情是将它们保留为 cmets =(
    • 好吧,界面用户不应该知道实现的细节,所以最好只给他们留下特征。
    • 很公平。它更适合将编写接口的新实现的开发人员。让我们这样说吧,如果开发人员在界面中留下这样的评论,如果我最终维护他们的代码,我将不胜感激。特质和界面之间的联系非常难以捉摸。
    【解决方案7】:

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

    ```

    # 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 类的依赖关系。您应该编写一些重复的代码以保持类可插入/独立。所以打开/关闭原则也应该在那里。

    【讨论】:

      猜你喜欢
      • 2014-06-05
      • 1970-01-01
      • 1970-01-01
      • 2011-01-16
      • 1970-01-01
      • 1970-01-01
      • 2013-10-15
      • 1970-01-01
      • 2010-12-03
      相关资源
      最近更新 更多