SHOW ME THE CODE - 面向对象设计系列
- 1 SHOW ME THE CODE - 面向对象基本概念
- 2 SHOW ME THE CODE - 面向对象程序设计之 - 单一职责原则(SRP)
- 3 SHOW ME THE CODE - 面向对象程序设计之 - 开闭原则(OCP)
- 4 SHOW ME THE CODE - 面向对象程序设计之 - 里氏替换原则(LSP)
- 5 SHOW ME THE CODE - 面向对象程序设计之 - 接口隔离原则(ISP)
从上一篇“开闭原则(Open - Close Principle)”中可以看出面向对象设计的重要原则是创建抽象化,从抽象导出具体,而具体可以有不同的实现。而今天我们要讲的里氏替换原则就是讲的如何从抽象到具体。
里氏替换原则规定了子类与父类的关系。该原则由芭芭拉·利斯科夫(Barbara Liskov)在1977年提出,是“数据抽象与层次”概念的重要组成部分。
定义
如果对每一个类型T1的对象 t1, 都有类型为T2 的对象 t2, 使得以T1定义的所有程序P在所有的对象t1 替换成 t2 时, 程序P的行为没有变化,那么T2是T1的子类。
简单来讲,就是针对一个基类(BaseT)编写的程序P,用子类(SubT)的对象c1替换基类(BaseT)的对象b1,程序P的行为和结果应该保持不变。
Java Code Sample
abstract class BaseT
{
public abstract String echo();
}
class SubT1 extends BaseT
{
@Override
public String echo()
{
return "Greeting from Child T1".
}
}
class SubT2 extends BaseT
{
@Override
public String echo()
{
return "Greeting from Child T2".
}
}
class APP
{
public void main(String[] args)
{
BaseT t1 = new SubT1();
BaseT t2 = new SubT2();
sayHello(t1);
sayHello(t2);
//下面的sayHelloV2会编译不通过,sayHelloV2方法不能接收BaseT类开的对象;
BaseT child1 = new SubT1();
sayHelloV2(child1);
//下面的sayHelloV2调用没有问题
ChildT1 child2 = new SubT1();
sayHelloV2(child2);
}
//sayHello方法接收一个BaseT类型的对象
public static String sayHello(BaseT type)
{
return type.echo();
}
//sayHelloV2方法接收一个ChildT1类型的对象
public static String sayHelloV2(SubT1 type)
{
return type.echo();
}
}
怎样理解LSP
理解里氏替换原则(Liskov Substitution Principle, LSP)时,关键在于确保子类的对象能够替换掉父类的对象而不破坏程序的正确性和预期行为。换句话说,就是对于程序中能够使用父类对象的地方,都应该能够无缝地使用子类的对象来替代,而不会引起行为上的错误或异常。
比如我们要注意以下几点:
- 可扩展,但不要重写非抽像方法,不要改变行为
父类中的非抽象方法提供了一个通用实现,该实现预计在大多数或所有子类中都是合适的,子类可能没有必要覆写这个方法。如果覆写,可能意味着父类的设计不够通用,或者在子类中有特殊的需求。比如说:父类的这个方法提供了一些必要的初始化工作,会对一些特定的变量赋值等。如果子类Override这个方法,可能会引入错误。
class BaseType {
public void doWork() {
System.out.println("Setting up the environment.");
// 执行一些初始化工作
}
}
class Child extends BaseType {
@Override
public void doWork() {
System.out.println("Doing specific work.");
// 假设这里忘记调用 super.doWork(), 导致环境没有正确设置,行为改变了,且引入了错误。
// 执行特定的工作
}
}
// 使用示例
BaseType obj = new Child();
obj.doWork(); // 环境没有正确设置,可能导致程序执行错误
- 不要改变预期结果
下面例子,就改变了预期的结果
// 父类
class Authenticator {
boolean authenticate(String username, String password) {
// 一些验证逻辑
// 约定:如果验证成功返回true,失败返回false
return "admin".equals(username) && "password123".equals(password);
}
}
// 子类
class StrictAuthenticator extends Authenticator {
@Override
boolean authenticate(String username, String password) {
// 一些更严格的验证逻辑
// 错误的行为:改变了方法的预期结果, 如果password不符合强密码要求会导致认证失败。
if (!password.matches("^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).{8,}$")) return false;
//即使用户名,密码能匹配上,但由于不符合密码强度,下面的逻辑不会执行,导致认证失败了。
String realPassword = getPassword("admin");
return "admin".equals(username) && realPassword.equals(password);
}
}
- 要遵守父类契约
契约原则:将父类方法视为一种契约,子类必须遵守该契约才能保证程序的正确性。例如,如果父类方法定义了参数检查逻辑,子类不能省略该检查逻辑,否则可能会导致程序出现安全漏洞。
// 父类
class Account {
public void deposit(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Amount must be positive.");
}
// 执行存款操作
System.out.println("Deposited: " + amount);
}
}
// 子类
class SavingsAccount extends Account {
@Override
public void deposit(double amount) {
// 错误的做法:没有执行父类要求的参数检查
// 执行存款操作
System.out.println("[SavingsAccount] Deposited: " + amount);
}
}
// 使用示例
Account savingsAccount = new SavingsAccount();
savingsAccount.deposit(-100); // 应该抛出 IllegalArgumentException,但是子类没有检查,违反了契约原则
LSP的一种典型运用:策略模式
策略模式是一种行为设计模式,它允许在运行时选择算法的行为。在策略模式中,将算法封装在单独的类中,使得它们可以相互替换,而不影响客户端的代码。下面以支付场景为例讲解什么是策略模式。
例如,我们在电商平台购物时,通常会提供不同的支付方式,例如微信支付,支付宝支付,银行卡支付等等。
首先,我们定义一个接口 PaymentStrategy,它表示支付策略:
interface PaymentStrategy {
void pay(int amount);
}
然后,我们创建几个实现了 PaymentStrategy 接口的具体策略类,比如 DebitCardPayment和 WechatPayment, AlipayPayment:
class DebitCardPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
//借记卡支付逻辑
System.out.println("Paid " + amount + " via debit card.");
}
}
class WechatPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
//微信支付逻辑
System.out.println("Paid " + amount + " via Wechat.");
}
}
class AlipayPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
//支付宝支付逻辑
System.out.println("Paid " + amount + " via Alipay.");
}
}
一个简单的购物车
import java.util.List;
class ShoppingCart {
private List<Item> items;
private PaymentStrategy paymentStrategy;
ShoppingCart(List<Item> items) {
this.items = items;
}
void setPaymentStrategy(PaymentStrategy paymentStrategy) {
this.paymentStrategy = paymentStrategy;
}
void checkout() {
int totalAmount = calculateTotal();
paymentStrategy.pay(totalAmount);
}
private int calculateTotal() {
int total = 0;
for (Item item : items) {
total += item.getPrice();
}
return total;
}
}
测试不同的支付策略
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Item> items = new ArrayList<>();
items.add(new Item("Laptop", 1000));
items.add(new Item("Mouse", 20));
items.add(new Item("Keyboard", 50));
ShoppingCart cart = new ShoppingCart(items);
// 使用支付宝支付
cart.setPaymentStrategy(new AlipayPayment());
cart.checkout();
// 使用借记卡支付
cart.setPaymentStrategy(new DebitCardPayment();
cart.checkout();
}
}
总结一下
当我们在运应用和理解里氏替换原则时,一定要注意以下几点:
- 子类能够表现出父类的行为:子类应该继承并扩展父类的行为,而不是修改或删除它。
- 保持接口一致性:子类应该实现与父类相同的接口,这样客户端代码在不知道具体子类的情况下也能正常工作。
- 不要改变父类的含义:子类不应该改变父类的预期行为。如果子类修改了父类的行为,那么它违反了里氏替换原则。
- 通过抽象化进行设计:使用抽象类和接口来定义通用行为,从而使得子类可以自由地扩展而不违反里氏替换原则。
希望这篇文章能够帮助大家更好的理解里氏替换原则。
也欢迎大家关注我的公众号,一起交流软件开发、架构设计、云原生技术