【译】SOLID:Part 3 - 里氏代换原则 & 接口隔离原则
【译】SOLID:Part 3 - 里氏代换原则 & 接口隔离原则原文地址 作者:Patkos Csaba
单一职责(SRP),开闭原则,里氏代换原则,接口隔离原则以及依赖倒转原则。在编程的过程中应当牢记这五种敏捷原则。 ? 因为里氏代换原则(LSP)和接口隔离原则(ISP)都很简单并容易例证,所以在这篇文章里会一起说。 里氏代换原则(LSP)
? 这个原则是 Barbara Liskov 在 1987 年在一个会议上提出的,并在 1994 年和 Jannette Wing 共同推出书面说明。原始的定式见下:
? 后来随着 Robert C. Martin 的书 Agile Software Development,Principles,Patterns,and Practices 出版,这条定义一里氏代换的名字被大众所知。 ? 让我们看一下 Robert C. Martin 给出的定义:
? 尽可能简单的来说,从使用者的角度来说,子类不应该破坏父类的功能。这里用一个简单的例子来论证这一观点。 class Vehicle {
function startEngine() {
// Default engine start functionality
}
function accelerate() {
// Default acceleration functionality
}
}
? 提供抽象类 class Car extends Vehicle {
function startEngine() {
$this->engageIgnition();
parent::startEngine();
}
private function engageIgnition() {
// Ignition procedure
}
}
class ElectricBus extends Vehicle {
function accelerate() {
$this->increaseVoltage();
$this->connectIndividualEngines();
}
private function increaseVoltage() {
// Electric logic
}
private function connectIndividualEngines() {
// Connection logic
}
}
? 如果一个应用类可以使用 class Driver {
function go(Vehicle $v) {
$v->startEngine();
$v->accelerate();
}
}
? 这是一个简单的模板模式的应用,就像我们在 OCP 那里用的那样 ? 根据我们在 OCP 中的经验,我们可以发现里氏代换和 OCP 有着很强烈的联系。事实上,“违反了 LSP 就是潜在地违反了 OCP”(Robert C. Martin),并且模板模式就是一个经典的演示 LSP 的例子,同时也是 OCP 的一种实现方式。 一个经典的违反了 LSP 的例子? 为了彻底的阐明 LSP,我们将会看一个经典的例子,它很有意义而且容易理解。 class Rectangle {
private $topLeft;
private $width;
private $height;
public function setHeight($height) {
$this->height = $height;
}
public function getHeight() {
return $this->height;
}
public function setWidth($width) {
$this->width = $width;
}
public function getWidth() {
return $this->width;
}
}
? 让我们从一个基本的几何长方形 ? 在实际的几何学中,正方形是长方形的一种特例。所以我们会尝试通过用继承 ? ? 但是在实际编码中 class Square extends Rectangle {
public function setHeight($value) {
$this->width = $value;
$this->height = $value;
}
public function setWidth($value) {
$this->width = $value;
$this->height = $value;
}
}
? 正方形是长宽相等的长方形,我们也可以像上面那样通过继承实现它,虽然看起来比较奇怪。我们通过覆写 setter 和 getter 让长宽相等。但是这么做会对应用代码产生什么影响呢? class Client {
function areaVerifier(Rectangle $r) {
$r->setWidth(5);
$r->setHeight(4);
if($r->area() != 20) {
throw new Exception('Bad area!');
}
return true;
}
? 可以假想一个应用类验证长方形的面积,如果不对就抛出异常。 function area() {
return $this->width * $this->height;
}
? 我们把上面的方法加到 class LspTest extends PHPUnit_Framework_TestCase {
function testRectangleArea() {
$r = new Rectangle();
$c = new Client();
$this->assertTrue($c->areaVerifier($r));
}
}
? 上面的例子是能够通过测试的。如果 function testSquareArea() {
$r = new Square();
$c = new Client();
$this->assertTrue($c->areaVerifier($r));
}
? 测试程序很简单并且它崩溃了。当我们运行它的时候一个异常被抛出了。 PHPUnit 3.7.28 by Sebastian Bergmann.
Exception : Bad area!
#0 /paht/: /.../.../LspTest.php(18): Client->areaVerifier(Object(Square))
#1 [internal function]: LspTest->testSquareArea()
? 所以,我们的类 ? 我特别喜欢这个例子因为它除了违反了 LSP 也说明面向对象编程并不仅仅是将现实生活映射到代码里。每一个对象都必须是一个概念的抽象。如果人么尝试将真实的对象一一对应到程序里去,那么就会总是出错。 接口隔离原则? 单一职责是关于角色和高层架构的。开闭原则负责的是类的设计和功能拓展。里氏代换原则是关于子类类型化和继承的。而接口隔离原则(ISP)是关于和应用代码交互的业务逻辑。 ? 所有的模块化程序都会提供一些应用代码可以使用的接口。可能是接口类或者是实现了外观模式的对象。具体用的是那种模式并不重要。它们在与应用代码交互的过程中有一个共性。接口可以连接同一个工程的不同模块,或者连接一个第三方的库。这同样也没什么区别。通信是通信,应用是应用,不管是谁在写这个代码。 ? 所以我们该怎么样定义这些接口呢。我们可以考虑我们的模块并且列出所有我们想要提供的功能。 ? 这看起来是一个不错的开始,在模块里定义了我们想实现的方法。不过一个这样的开始可能会导致两种结果。
最后的思考? LSP 教会我们为什么不能将现实一对一的带入到程序对象中和子类不应该破坏父类。我们也结合我们已经知道的别的原则进行了一些讨论。 ? ISP 将会我们要更多的考虑使用者。尊重它们的需求将会让代码以及生活更好。 ? 感谢您花费时间阅读这篇文章。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |