我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?


当前回答

长话短说,让我们留下矩形矩形和正方形,实际的例子,当扩展一个父类时,你必须要么保留确切的父API,要么扩展IT。

假设您有一个基本ItemsRepository。

class ItemsRepository
{
    /**
    * @return int Returns number of deleted rows
    */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        return $numberOfDeletedRows;
    }
}

以及扩展它的子类:

class BadlyExtendedItemsRepository extends ItemsRepository
{
    /**
     * @return void Was suppose to return an INT like parent, but did not, breaks LSP
     */
    public function delete()
    {
        // perform a delete query
        $numberOfDeletedRows = 10;

        // we broke the behaviour of the parent class
        return;
    }
}

然后,您可以让客户端使用Base ItemsRepository API并依赖它。

/**
 * Class ItemsService is a client for public ItemsRepository "API" (the public delete method).
 *
 * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository
 * but if the sub-class won't abide the base class API, the client will get broken.
 */
class ItemsService
{
    /**
     * @var ItemsRepository
     */
    private $itemsRepository;

    /**
     * @param ItemsRepository $itemsRepository
     */
    public function __construct(ItemsRepository $itemsRepository)
    {
        $this->itemsRepository = $itemsRepository;
    }

    /**
     * !!! Notice how this is suppose to return an int. My clients expect it based on the
     * ItemsRepository API in the constructor !!!
     *
     * @return int
     */
    public function delete()
    {
        return $this->itemsRepository->delete();
    }
} 

当用子类替换父类破坏了API的契约时,LSP就被破坏了。

class ItemsController
{
    /**
     * Valid delete action when using the base class.
     */
    public function validDeleteAction()
    {
        $itemsService = new ItemsService(new ItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is an INT :)
    }

    /**
     * Invalid delete action when using a subclass.
     */
    public function brokenDeleteAction()
    {
        $itemsService = new ItemsService(new BadlyExtendedItemsRepository());
        $numberOfDeletedItems = $itemsService->delete();

        // $numberOfDeletedItems is a NULL :(
    }
}

你可以在我的课程中学习更多关于编写可维护软件的知识:https://www.udemy.com/enterprise-php/

其他回答

LSP是关于类的契约的规则:如果基类满足契约,则LSP派生的类也必须满足该契约。

在Pseudo-python

class Base:
   def Foo(self, arg): 
       # *... do stuff*

class Derived(Base):
   def Foo(self, arg):
       # *... do stuff*

如果每次在派生对象上调用Foo,它给出的结果与在Base对象上调用Foo完全相同,只要arg是相同的。

以Board数组的形式实现ThreeDBoard会有用吗?

也许你想把不同平面上的ThreeDBoard切片作为一个板。在这种情况下,您可能希望为Board抽象出一个接口(或抽象类),以允许多种实现。

就外部接口而言,您可能希望为TwoDBoard和ThreeDBoard提取一个Board接口(尽管上述方法都不适合)。

使用指向基类的指针或引用的函数必须能够在不知道它的情况下使用派生类的对象。

当我第一次阅读LSP时,我认为这是一个非常严格的含义,本质上等同于接口实现和类型安全强制转换。这意味着语言本身要么保证LSP,要么不保证LSP。例如,在严格意义上,ThreeDBoard当然可以取代Board,就编译器而言。

在阅读了更多关于LSP的概念之后,我发现LSP的解释通常比这更广泛。

简而言之,对于客户端代码来说,“知道”指针后面的对象是派生类型而不是指针类型的含义并不仅限于类型安全。对LSP的遵守也可以通过探测对象的实际行为进行测试。也就是说,检查对象的状态和方法参数对方法调用结果或从对象抛出的异常类型的影响。

再次回到示例,理论上Board方法可以在ThreeDBoard上很好地工作。然而,在实践中,在不妨碍ThreeDBoard打算添加的功能的情况下,防止客户端可能无法正确处理的行为差异是非常困难的。

掌握了这些知识后,评估LSP粘附性可以成为一个很好的工具,可以确定何时组合机制更适合扩展现有功能,而不是继承。

LSP说“对象应该被它们的子类型替换”。 另一方面,这一原则指向

子类永远不应该破坏父类的类型定义。

通过以下示例,可以更好地理解LSP。

没有太阳能发电:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

LSP修复:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

该原则由Barbara Liskov在1987年提出,并通过关注超类及其子类型的行为来扩展开闭原则。

当我们考虑违反它的后果时,它的重要性就变得显而易见了。考虑一个使用以下类的应用程序。

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

想象一下,有一天,客户要求除了矩形之外还能操作正方形。因为正方形是矩形,所以square类应该派生自rectangle类。

public class Square : Rectangle
{
} 

然而,这样做会遇到两个问题:

一个正方形不需要从矩形继承高度和宽度变量,如果我们必须创建成千上万个正方形对象,这可能会造成严重的内存浪费。 从矩形继承的width和height setter属性不适用于正方形,因为正方形的宽度和高度是相同的。 为了将height和width设置为相同的值,我们可以创建两个新属性,如下所示:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

现在,当有人设置一个正方形物体的宽度时,它的高度将相应地改变,反之亦然。

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

让我们继续考虑另一个函数:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

如果我们将一个方形对象的引用传递给这个函数,我们将违反LSP,因为该函数对其参数的导数不起作用。属性width和height不是多态的,因为它们在矩形中没有被声明为虚的(正方形对象将被损坏,因为高度不会被改变)。

然而,通过将setter属性声明为virtual,我们将面临另一个违反,即OCP。事实上,派生类正方形的创建会导致基类矩形的变化。