一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
设计模式Git项目地址:https://github.com/yangchong211/YCDesignBlog
里式替换原则(LSP)是面向对象设计的重要原则之一,确保子类可以无缝替换父类而不破坏程序功能。本文详细介绍了LSP的定义、背景、理解方法及应用场景,通过电商支付和鸟类飞行案例展示了如何遵循LSP,并分析了其优缺点。
LSP强调子类应保持父类的行为一致性,有助于提高代码的可扩展性、可维护性和可重用性,但也可能导致过度设计。最后,对比了LSP与多态的区别,明确了LSP作为设计原则的重要性。
什么是里氏替换的原则,如何理解这一原则?
有那些场景满足里氏替换原则?它跟多态有何区别?
在面向对象编程中,继承是一种重要的机制,它允许我们创建一个类(子类)来继承另一个类(父类)的属性和行为。子类通过继承父类,可以重用父类的代码,并且可以添加或修改一些特定的行为。
然而,当使用继承时,必须确保子类可以无缝地替换父类,而不会破坏原有的程序功能。这就是里式替换原则的背景。
里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的:Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
综合两者的描述,将这条原则用中文描述出来,是这样的:
子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。
里氏替换原则(Liskov Substitution Principle,LSP)是设计模式六大原则之一:
通俗地说,如果我们在程序中使用的是一个基类对象,那么在不修改程序的前提下,用它的子类对象替换这个基类对象,程序应该仍然可以正常运行。
假设我们在电商系统中设计了一个支付类Payment,有一个子类CreditCardPayment用于处理信用卡支付:
class Payment {
public void pay(double amount) {
// 支付逻辑
}
}
class CreditCardPayment extends Payment {
@Override
public void pay(double amount) {
if (amount > 1000) {
throw new IllegalArgumentException("信用卡支付金额不能超过1000元");
}
// 信用卡支付逻辑
}
}
此时,如果我们在系统中使用Payment基类对象进行支付:
Payment payment = new CreditCardPayment();
payment.pay(1200);
由于CreditCardPayment类中的逻辑限制,当支付金额超过1000元时会抛出异常。这导致了父类Payment的行为在子类CreditCardPayment中发生了变化,违反了里氏替换原则。
为了遵循里氏替换原则,我们应该确保子类在扩展父类功能时,保持父类的行为一致性。可以通过在父类中添加必要的约束来确保子类行为的一致性:
class Payment {
public void pay(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("支付金额必须大于0");
}
// 通用支付逻辑
}
}
class CreditCardPayment extends Payment {
@Override
public void pay(double amount) {
super.pay(amount);
// 信用卡支付逻辑
}
}
在这个设计中,CreditCardPayment类继承了父类Payment的行为,并且在支付逻辑之前调用了super.pay(amount),确保所有支付金额都符合父类的约束。
这样,无论是使用基类对象还是子类对象,程序的行为都保持一致,遵循了里氏替换原则。
例如:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。
假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期。
未遵守里氏替换原则:
public class LSPtest {
public static void main(String[] args) {
Bird bird1 = new Swallow();
Bird bird2 = new BrownKiwi();
bird1.setSpeed(120);
bird2.setSpeed(120);
System.out.println("如果飞行300公里:");
try {
System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");
System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。");
} catch (Exception err) {
System.out.println("发生错误了!");
}
}
}
//鸟类
class Bird {
double flySpeed;
public void setSpeed(double speed) {
flySpeed = speed;
}
public double getFlyTime(double distance) {
return (distance / flySpeed);
}
}
//燕子类
class Swallow extends Bird {
}
//几维鸟类
class BrownKiwi extends Bird {
public void setSpeed(double speed) {
flySpeed = 0;
}
}
这个设计存在的问题:
几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。
燕子和几维鸟都是鸟类,但是父类抽取的共性有问题,几维鸟的的飞行不是正常鸟类的功能,需要特殊处理,应该抽取更加共性的功能。
取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。
public class Lsptest2 {
public static void main(String[] args) {
Animal animal1 = new Bird();
Animal animal2 = new BrownKiwi();
animal1.setRunSpeed(120);
animal2.setRunSpeed(180);
System.out.println("如果奔跑300公里:");
try {
System.out.println("鸟类将奔跑" + animal1.getRunSpeed(300) + "小时.");
System.out.println("几维鸟将奔跑" + animal2.getRunSpeed(300) + "小时。");
Bird bird = new Swallow();
bird.setFlySpeed(150);
System.out.println("如果飞行300公里:");
System.out.println("燕子将飞行" + bird.getFlyTime(300) + "小时.");
} catch (Exception err) {
System.out.println("发生错误了!");
}
}
}
/**
* 动物类,抽象的功能更加具有共性
*/
class Animal{
Double runSpeed;
public void setRunSpeed(double runSpeed) {
this.runSpeed = runSpeed;
}
public double getRunSpeed(double distince) {
return distince/runSpeed;
}
}
/**
* 鸟类继承动物类
*/
class Bird extends Animal{
double flySpeed;
public void setFlySpeed(double flySpeed) {
this.flySpeed = flySpeed;
}
public double getFlyTime(double distince) {
return distince/flySpeed;
}
}
/**
* 几维鸟继承动物类
*/
class BrownKiwi extends Animal{
}
/**
* 燕子继承鸟类 飞行属于燕子的特性,
*/
class Swallow extends Bird{
}
子类应该能够替代父类并且表现出相同的行为,而不需要修改原有的程序逻辑。这样可以确保代码的可扩展性、可维护性和可重用性。
遵循里式替换原则的好处包括:
它也有一些潜在的缺点和限制,包括:
里氏替换原则与开闭原则的关系
里氏替换原则与开闭原则密切相关。开闭原则强调对扩展开放、对修改关闭,而里氏替换原则则确保子类能够正确地替换父类,使得扩展在不修改现有代码的情况下进行。
在电商交易系统中,遵循里氏替换原则可以确保我们在扩展支付方式、引入新的支付逻辑时,不会破坏已有系统的稳定性。例如,我们可以添加新的支付方式,而不影响原有的支付逻辑。
里式替换原则是用来指导,继承关系中子类该如何设计的一个原则
理解里式替换原则,最核心的就是理解“design by contract,按照协议来设计”这几个字。
父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。
这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;甚至包括注释中所罗列的任何特殊说明。
要弄明白里式替换原则跟多态的区别
虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。
模块 | 描述 | 备注 |
---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。