我听说利斯科夫替换原则(LSP)是面向对象设计的基本原则。它是什么?它的一些使用例子是什么?
当前回答
使用指向基类的指针或引用的函数必须能够在不知道它的情况下使用派生类的对象。
当我第一次阅读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();
}
我在每个答案中都看到了矩形和正方形,以及如何违反LSP。
我想用一个真实的例子来展示LSP是如何符合的:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return $result;
}
}
这种设计符合LSP,因为无论我们选择使用哪种实现,行为都不会改变。
是的,你可以在这个配置中违反LSP,做一个简单的改变,像这样:
<?php
interface Database
{
public function selectQuery(string $sql): array;
}
class SQLiteDatabase implements Database
{
public function selectQuery(string $sql): array
{
// sqlite specific code
return $result;
}
}
class MySQLDatabase implements Database
{
public function selectQuery(string $sql): array
{
// mysql specific code
return ['result' => $result]; // This violates LSP !
}
}
现在子类型不能以同样的方式使用,因为它们不再产生相同的结果。
Liskov's Substitution Principle(LSP) All the time we design a program module and we create some class hierarchies. Then we extend some classes creating some derived classes. We must make sure that the new derived classes just extend without replacing the functionality of old classes. Otherwise, the new classes can produce undesired effects when they are used in existing program modules. Liskov's Substitution Principle states that if a program module is using a Base class, then the reference to the Base class can be replaced with a Derived class without affecting the functionality of the program module.
例子:
Below is the classic example for which the Liskov's Substitution Principle is violated. In the example, 2 classes are used: Rectangle and Square. Let's assume that the Rectangle object is used somewhere in the application. We extend the application and add the Square class. The square class is returned by a factory pattern, based on some conditions and we don't know the exact what type of object will be returned. But we know it's a Rectangle. We get the rectangle object, set the width to 5 and height to 10 and get the area. For a rectangle with width 5 and height 10, the area should be 50. Instead, the result will be 100
// Violation of Likov's Substitution Principle
class Rectangle {
protected int m_width;
protected int m_height;
public void setWidth(int width) {
m_width = width;
}
public void setHeight(int height) {
m_height = height;
}
public int getWidth() {
return m_width;
}
public int getHeight() {
return m_height;
}
public int getArea() {
return m_width * m_height;
}
}
class Square extends Rectangle {
public void setWidth(int width) {
m_width = width;
m_height = width;
}
public void setHeight(int height) {
m_width = height;
m_height = height;
}
}
class LspTest {
private static Rectangle getNewRectangle() {
// it can be an object returned by some factory ...
return new Square();
}
public static void main(String args[]) {
Rectangle r = LspTest.getNewRectangle();
r.setWidth(5);
r.setHeight(10);
// user knows that r it's a rectangle.
// It assumes that he's able to set the width and height as for the base
// class
System.out.println(r.getArea());
// now he's surprised to see that the area is 100 instead of 50.
}
}
结论: 这个原则只是开闭原则的延伸 意味着我们必须确保新的派生类正在扩展 基类而不改变它们的行为。
参见:开闭原则
对于更好的结构,还有一些类似的概念:约定优于配置
我建议您阅读这篇文章:违反利斯科夫替换原则(LSP)。
你可以在那里找到一个解释,什么是利斯科夫替换原则,一般线索帮助你猜测你是否已经违反了它,一个方法的例子,将帮助你使你的类层次结构更安全。
在一个非常简单的句子中,我们可以说:
子类不能违背它的基类特征。它必须有能力。我们可以说这和子类型是一样的。