不破坏SOLID / LSP的正方形矩形实现


Square Rectangle implementation that does not break SOLID / LSP

在SO中有几个线程属于打破Liskov替换原则的经典设计示例:Square和Rectangle类。问题开始于证明如果正方形扩展矩形,它违反LSP。很多人问如何解决这个问题。我听到的想法包括有一个多边形基类,由矩形和正方形等扩展。我也想过这个问题,但对我来说,这是一个错误的方向——错误的讨论。正方形不能扩展矩形。正方形不向矩形添加功能或属性。正方形是长方形。它是一种矩形。

假设存在一个Rectangle类,并且我的任务是构建一个重用Rectangle的Square类,我将构建一个具有受保护属性的Square类,该属性是Rectangle的实例。我会公开所有适用于Square的矩形属性/方法,而不公开那些不适用的。Square的任何不适用于Rectangle的属性或方法都将出现在我的Square类中。以下是我在PHP中的解决方案:

class Square {
    protected $rectangle;   // instance of Rectangle
    function __construct ($length) {
        if (!is_numeric ($length))
            throw new Exception ("Square::__contruct - length must be numeric");
        $this->rectangle = new Rectangle ($length, $length);  // given Rectangle (width, height)
    }
    public function getArea () {
        return $this->rectangle->getArea();
    }
    public function getLength () {
        return $this->rectangle->getWidth();  // or height pick one
    }
    public function setLength ($length) {
        if (!is_numeric ($length))
            throw new Exception ("Square::setLength - length must be numeric");
        $this->rectangle->setWidth ($length);
        $this->rectangle->setHeight ($length);
    }
    public function isSquare() {
        return ($this->rectangle->getHeight() == $this->rectangle->getWidth());
    }
}

正如争论的那样,在Square extends Rectangle的情况下破坏LSP是有问题的原因是,其他人向Rectangle添加了一个方法,该方法通过某些因素改变了矩形的面积——让我们称之为带有类似

签名的方法
public function transformArea (factor) 

被实现为将宽度乘以因子。这个论证说,现在你的正方形会增加这个因子的平方,因为宽度和高度是一样的,如果宽度增加10%,正方形就会增加21%。这并不完全正确,因为这不是这个系统的实际工作方式。实际发生的情况是,宽度按因子增长,矩形的面积也按因子增长,但现在Square的实例实际上不再是正方形(在Square extends rectangle的情况下),因为宽度和高度不再相同。在我的版本中,其他人可以将该方法添加到Rectangle中,并且它不会破坏Square -没有人可以在Square的实例上使用Rectangle的transformArea方法,因为它还没有被Square类公开。

如果有人要求添加这个方法,然后另一个程序员通过简单地从Rectangle类中公开它来将它添加到Square类,像这样:

public function transformArea ($factor) {
    $this->rectangle->transformArea ($factor);
}

然后假设这个人完成了他们的工作,在类上运行了一个单元测试,他们会发现无论何时运行这个方法,他们都会破坏isSquare。但是该测试可能会被添加到测试堆栈的末尾,并且可能不会发现错误。但这是总体开发的一个问题而不一定是这个系统的设计缺陷。

正确的答案是通过增加长度的平方根因子来实现平方方法。这样的:

public function transformArea ($factor) {
    if (!is_numeric ($factor))
        throw new Exception ("Square::transformArea - factor must be numeric");
    if ($factor <= 0)
        throw new Exception ("Square::transformArea - factor must be greater than zero");
    $growBy = (float) sqrt ($factor);
    $newLength = $this->getLength() * $growBy;
    $this->setLength ($newLength);
}

我能想到的另一个论点是,这个类不能保证它里面的矩形仍然是一个正方形(宽度==高度)。我不同意。Square不公开任何一个都改变的方法。矩形有一些方法可以打破这个,但没有一个是公开的。

就我个人而言,这更适合现实世界。正方形是长方形。所有的正方形都是长方形,但不是所有的长方形都是正方形。

我的解决方案是否违反了SOLID的任何规则?

设计就是权衡,"最佳"解决方案因应用而异:

  • 解决Square is-a - Rectangle问题的传统方法是在保持is-a关系的同时使类不可变(权衡)。
  • 您的示例演示了组合(Square has-a Rectangle),这是一个完全有效的解决方案,但现在您放弃了is-a关系,同时保持对象可变。你不能再将Square实例传递给接受Rectangle实例的方法。

您可以选择其中一个,但如果您试图同时执行这两个操作,则最终会破坏LSP。


PS:你的Square类不应该有isSquare函数,这是类的责任,以确保实例始终处于有效状态