0ead7d6f4e2d12e114dbb78994212faa_1240.webp

里氏替换原则(Liskov Substitution Principle, LSP)是SOLID原则之一,强调子类型能够替换其基类型而不影响程序的正确性。这要求子类能够保证基类的行为不变。

肖哥弹架构 跟大家“弹弹” 业务中设计模式的使用,需要代码关注

欢迎 点赞,点赞,点赞。

关注公号Solomon肖哥弹架构获取更多精彩内容

历史热点文章

2. 里氏替换原则设计图:

展示金融交易系统中的基类和子类之间的关系:

image.png

3. 里氏替换原则解决什么:

里氏替换原则解决了继承关系中的脆弱性问题,确保子类能够安全地替换基类,避免因继承带来的风险和错误。例如: 在金融交易系统中,需要处理不同类型的交易,每种交易都有其特定的执行和撤销逻辑。LSP确保这些不同的交易类型可以安全地使用基类接口进行操作。

4. 里氏替换原则特点:

  • 继承性:子类继承基类的所有属性和方法。
  • 一致性:子类重写的方法应该不改变基类方法的原有行为(对外表现的业务要一致)
  • 安全性:子类不会破坏基类的行为。
  • 扩展性:通过继承可以扩展基类的功能,同时保持基类的接口不变(子类的功能从基类被引用)。

5. 里氏替换原则缺点:

  • 实现限制:基类的设计需要足够通用,以适应所有可能的子类实现。
  • 过度设计:在一些简单的场景中,过度应用LSP可能导致设计过于复杂。
  • 实现难度:在某些情况下,完全遵循LSP可能需要复杂的设计。

6. 什么情况下需要用原则:

在需要实现多种类型交易的金融系统中,当系统要求不同类型的交易能够统一处理,但又各自具有特殊行为时。

7. 里氏替换原则案例

7.1 金融交易系统案例

考虑一个需要处理信用卡交易和银行转账的金融交易系统。

重构前:

    public class Transaction {
        public boolean execute() {
            // 通用执行逻辑
            return true;
        }

        public boolean reverse() {
            // 通用撤销逻辑
            return true;
        }
    }

    // 特定交易类型直接继承了基类,但没有考虑到特殊行为
    public class CreditCardTransaction extends Transaction {
        // 信用卡特有逻辑
    }

问题分析:

  1. 基类假定Transaction 类的设计假定所有子类都会遵循其方法定义的行为,但 CreditCardTransaction 并没有提供任何特定的实现,这可能导致基类方法的通用性(无意义)被错误地应用(无替换)。
  2. 扩展性差:如果未来需要添加特定于信用卡交易的逻辑,如信用卡授权,直接在 CreditCardTransaction 中添加这些逻辑会破坏里氏替换原则,因为这种新增的行为在基类中是不存在的。
  3. 代码复用问题:由于 CreditCardTransaction 没有实现自己的 executereverse 方法,它只是简单地复用了基类的方法,这限制了代码的复用性和灵活性。

重构后:

    public abstract class Transaction {
        public abstract boolean execute();
        public abstract boolean reverse();
    }

    public class CreditCardTransaction extends Transaction {
        private boolean isCardAuthorized = false;

        public boolean authorizeCard() {
            // 授权信用卡逻辑
            this.isCardAuthorized = true;
            return this.isCardAuthorized;
        }

        @Override
        public boolean execute() {
            if (!isCardAuthorized) {
                authorizeCard();
            }
            // 信用卡执行逻辑
            return true;
        }

        @Override
        public boolean reverse() {
            // 信用卡撤销逻辑
            return true;
        }
    }

    public class BankTransferTransaction extends Transaction {
        // 银行转账特有逻辑
        @Override
        public boolean execute() {
            // 执行银行转账
            return true;
        }

        @Override
        public boolean reverse() {
            // 撤销银行转账
            return true;
        }
    }

解决的问题:

  1. 抽象化基类:将 Transaction 类改为抽象类,并声明了抽象的 executereverse 方法。这强制要求所有子类实现这些方法,从而确保每个交易类型都有其特定的执行和撤销逻辑。
  2. 遵循里氏替换原则:通过让子类实现自己的 executereverse 方法,我们确保了子类可以安全地替换基类,而不会改变系统的行为。每个子类都可以根据其业务逻辑提供合适的实现。
  3. 增强了系统的扩展性:现在,如果需要添加新的交易类型,比如 BankTransferTransaction,我们只需创建一个新的类实现 Transaction 接口即可。每个类都封装了自己的业务逻辑,不会影响其他类。
  4. 提高了代码的复用性和灵活性:通过抽象化 Transaction 类,我们可以在不同的交易类型之间共享通用的交易管理逻辑,同时允许每个子类提供特定的实现,从而提高了代码的复用性和灵活性。
  5. 信用卡交易特有逻辑的处理:在 CreditCardTransaction 类中,我们添加了 authorizeCard 方法来处理信用卡授权的特有逻辑,并在 execute 方法中调用它。这样,信用卡交易的执行逻辑就与基类的通用逻辑解耦,并且可以根据需要进行扩展。
7.2 产品折扣案例

电子商务平台中有一个基类 Product 和多个子类,例如 ElectronicsClothing。这些子类继承自 Product 类并添加了一些特定的属性和方法。随着业务的发展,我们需要确保新增的产品类型能够无缝集成到现有的系统中。基类有一个方法 getDiscountedPrice,它根据产品类型应用不同的折扣策略。然而,随着时间推移,我们发现某些子类的方法实现改变了基类方法的预期行为,导致系统出现错误。

重构前:

public class Product {
    protected double price;

    public double getDiscountedPrice() {
        // 基类的折扣逻辑
        return price * 0.9; // 假设所有产品都有10%的折扣
    }
}

public class Electronics extends Product {
    private boolean isHighEnd;

    public void setHighEnd(boolean highEnd) {
        isHighEnd = highEnd;
    }

    @Override
    public double getDiscountedPrice() {
        if (isHighEnd) {
            // 高端电子产品不打折
            return price;
        } else {
            return super.getDiscountedPrice();
        }
    }
}

public class Clothing extends Product {
    // 服装类没有重写getDiscountedPrice方法,继承自Product
}

问题分析: Electronics 类重写了 getDiscountedPrice 方法,这违反了里氏替换原则,因为现在 Product 类的实例和 Electronics 类的实例在 getDiscountedPrice 方法的行为上不一致。如果客户端代码依赖于 Product 类的 getDiscountedPrice 方法总是返回打折后的价格,那么使用 Electronics 类的实例将导致错误。

重构后:

为了解决这个问题,我们可以引入一个策略模式来定义不同的折扣策略,并将这些策略应用到具体的产品类中。

public interface DiscountStrategy {
    double applyDiscount(double originalPrice);
}

public class StandardDiscountStrategy implements DiscountStrategy {
    public double applyDiscount(double originalPrice) {
        return originalPrice * 0.9;
    }
}

public class NoDiscountStrategy implements DiscountStrategy {
    public double applyDiscount(double originalPrice) {
        return originalPrice;
    }
}

public class Product {
    protected double price;
    private DiscountStrategy discountStrategy;

    public Product(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
    }

    public double getDiscountedPrice() {
        return discountStrategy.applyDiscount(price);
    }
}

public class Electronics extends Product {
    public Electronics(DiscountStrategy discountStrategy) {
        super(discountStrategy);
    }

    // 其他特定于电子产品的方法
}

public class Clothing extends Product {
    public Clothing(DiscountStrategy discountStrategy) {
        super(discountStrategy);
    }

    // 其他特定于服装的方法
}
7.3 汽车租赁

重构前:


public class Vehicle {
    protected double dailyRate;

    public Vehicle(double dailyRate) {
        this.dailyRate = dailyRate;
    }

    public double calculateRentalCost(int numberOfDays) {
        // 普通车辆的租赁费用计算
        return dailyRate * numberOfDays;
    }
}

public class RegularCar extends Vehicle {
    // 普通汽车特有逻辑
}

public class LuxuryCar extends Vehicle {
    private double luxurySurcharge;

    public LuxuryCar(double dailyRate, double luxurySurcharge) {
        super(dailyRate);
        this.luxurySurcharge = luxurySurcharge;
    }

    @Override
    public double calculateRentalCost(int numberOfDays) {
        // 豪华车的租赁费用计算,包括额外的附加费
        return super.calculateRentalCost(numberOfDays) + (luxurySurcharge * numberOfDays);
    }
}

分析问题:

  1. 不一致的成本计算LuxuryCar 类改变了 calculateRentalCost 方法的行为,这可能导致基类 Vehicle 的其他潜在子类也必须以相同的方式重写该方法,违反了里氏替换原则。
  2. 扩展性问题:如果未来需要添加更多的车辆类型,如 ElectricCar,它们可能有自己的成本计算逻辑,这将导致更多的方法重写,增加了系统的复杂性。

重构后:

引入策略模式来定义不同的成本计算策略,并将这些策略应用到具体的车辆类中。

public interface RentalCostStrategy {
    double calculateCost(Vehicle vehicle, int numberOfDays);
}

public class RegularCostStrategy implements RentalCostStrategy {
    public double calculateCost(Vehicle vehicle, int numberOfDays) {
        return vehicle.getDailyRate() * numberOfDays;
    }
}

public class LuxuryCostStrategy implements RentalCostStrategy {
    public double calculateCost(Vehicle vehicle, int numberOfDays) {
        double baseCost = new RegularCostStrategy().calculateCost(vehicle, numberOfDays);
        return baseCost + (vehicle.getLuxurySurcharge() * numberOfDays);
    }
}

public abstract class Vehicle {
    protected double dailyRate;
    private RentalCostStrategy costStrategy;

    public Vehicle(double dailyRate, RentalCostStrategy costStrategy) {
        this.dailyRate = dailyRate;
        this.costStrategy = costStrategy;
    }

    public double getDailyRate() {
        return dailyRate;
    }

    public double getLuxurySurcharge() {
        // 默认没有附加费
        return 0;
    }

    public double calculateRentalCost(int numberOfDays) {
        return costStrategy.calculateCost(this, numberOfDays);
    }
}

public class RegularCar extends Vehicle {
    public RegularCar(double dailyRate) {
        super(dailyRate, new RegularCostStrategy());
    }
}

public class LuxuryCar extends Vehicle {
    private double luxurySurcharge;

    public LuxuryCar(double dailyRate, double luxurySurcharge) {
        super(dailyRate, new LuxuryCostStrategy());
        this.luxurySurcharge = luxurySurcharge;
    }

    @Override
    public double getLuxurySurcharge() {
        return luxurySurcharge;
    }
}

解决的问题:

  • 遵循LSP:通过引入成本计算策略,我们确保了 Vehicle 类的子类可以安全地替换基类,而不会改变成本计算的预期行为。
  • 提高扩展性:新增车辆类型时,我们只需实现新的成本计算策略,而无需修改现有的 Vehicle 类或其他子类。
  • 增强灵活性:每种车辆类型都可以有自己独特的成本计算方式,而不会相互影响,提高了代码的灵活性和可维护性。

8. 参考开源框架:

在Apache Fineract中,不同的贷款产品通过实现相同的接口来保证它们可以被系统统一处理,同时保持各自的特性。

9. 总结:

里氏替换原则在工业级项目中至关重要,特别是在金融行业,它确保了系统的健壮性和可靠性。通过遵循LSP,我们可以构建一个灵活且安全的系统,支持多种交易类型,同时保持代码的简洁性和一致性。虽然这可能需要更多的设计工作,但它为系统的长期维护和扩展提供了坚实的基础。

历史热点文章