Bootstrap

写代码总被Dis:5个项目案例带你掌握SOLID技巧,代码有架构风格

SOLID 原则图

在这里插入图片描述

  • 单一职责原则是基础,确保每个类只关注一件事情。
  • 开闭原则建立在单一职责原则之上,强调通过扩展而不是修改现有代码来添加新功能。
  • 里氏替换原则确保继承体系的健全性,让派生类可以安全地替换基类。
  • 接口隔离原则避免让客户端依赖于它们不需要的接口,提高了接口的可用性。
  • 依赖倒置原则将高层策略性业务规则与低层细节解耦,使系统更加灵活。

文本通过ERP案例、支付系统集成案例、订单案例、折扣案例、汽车租赁项目案例代码片段来讲解SOLID设计原则如何落地到业务项目来提高项目整体的质量,不讲概念,注重实战落地。

肖哥弹架构 跟大家“弹弹” 代码设计技巧,需要代码关注

欢迎 点赞,点赞,点赞。

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

历史热点文章

依赖倒置

1. 依赖倒置原则设计图:

在这里插入图片描述

2. 依赖倒置原则解决什么:

依赖倒置原则解决了在支付网关设计中,业务逻辑层与具体支付实现之间的紧密耦合问题。

3. 依赖倒置原则使用场景:

  • 当系统需要支持多种支付方式,并且预期会引入更多支付方式或修改现有支付逻辑时。
  • 在ERP系统中,当需要从多个数据源生成报告,或预期报告生成方式将随时间变化时。

4. ERP 案例

重构前:

public class FinancialModule {
    public void generateReports() {
        // 直接依赖于具体的数据库报告生成服务
        DatabaseReportingService service = new DatabaseReportingService();
        service.generate(...);
    }
}

class DatabaseReportingService {
    public void generate(/* parameters */) {
        // 从数据库生成报告的逻辑
    }
}

问题分析:

  1. 紧耦合: FinancialModule 直接依赖于 DatabaseReportingService 的具体实现,耦合度高,导致系统灵活性差。
  2. 扩展性差: 当需要添加新的报告生成方式(例如基于文件的报告)时,可能需要修改 FinancialModule 的代码,这违背了开闭原则。
  3. 维护困难: 如果 DatabaseReportingService 的接口或实现发生变化,可能会影响到 FinancialModule,增加了维护成本。
  4. 测试复杂性: 直接依赖具体实现使得单元测试 FinancialModule 变得复杂,可能需要大量模拟(mocking)具体类。
  5. 代码重复: 如果系统中其他模块也需要生成报告,可能会复制 FinancialModule 中的代码,导致代码重复。

重构后:

interface ReportingService {
    void generate(ReportRequest request);
}

class DatabaseReportingService implements ReportingService {
    public void generate(ReportRequest request) {
        // 从数据库生成报告的逻辑
    }
}

class FileBasedReportingService implements ReportingService {
    public void generate(ReportRequest request) {
        // 从文件生成报告的逻辑
    }
}

class FinancialModule {
    private ReportingService reportingService;

    public FinancialModule(ReportingService reportingService) {
        this.reportingService = reportingService;
    }

    public void generateReports(ReportRequest request) {
        // 使用抽象的报告服务生成报告
        reportingService.generate(request);
    }
}

// 使用FinancialModule
FinancialModule financialModule = new FinancialModule(new DatabaseReportingService());
financialModule.generateReports(new ReportRequest());

解决的问题:

  1. 解耦合: FinancialModule 现在依赖于 ReportingService 接口,而不是任何具体实现,降低了耦合度。
  2. 扩展性增强: 新的报告生成方式(如 FileBasedReportingService)可以通过实现 ReportingService 接口轻松添加,无需修改 FinancialModule
  3. 维护简化: 由于 FinancialModule 不依赖于具体实现,因此维护和升级报告生成服务时对 FinancialModule 的影响较小。
  4. 测试简化: 可以轻松地为 FinancialModule 编写单元测试,只需模拟 ReportingService 接口,而不是具体实现。
  5. 代码复用: 其他需要报告生成功能的模块可以重用 FinancialModule,通过注入不同的 ReportingService 实现来满足特定需求。
  6. 依赖注入: 通过构造函数或设置器注入 ReportingService 的实现,提高了 FinancialModule 的灵活性和可配置性。
  7. 单一职责: FinancialModule 专注于其业务逻辑,而报告生成的具体细节由 ReportingService 的实现负责,符合单一职责原则。

接口隔离

1. 接口隔离设计图:

将一个臃肿的接口拆分为多个客户端特定的接口:

在这里插入图片描述

2. 接口隔离解决什么:

接口隔离原则解决了因为接口设计过于庞大而导致的低内聚和高耦合问题,使得使用接口的类实现了许多它们不需要的方法。

3. 接口隔离使用场景:

当一个类实现了许多它不需要的方法时,或者当多个类因为依赖同一个接口而互相影响时,应该考虑使用接口隔离原则。

4.支付系统集成案例

商务平台,需要集成多种支付方式,如信用卡支付和银行转账

重构前:


public interface IPayable {
    void processPayment();
}

public class PaymentService {
    public void processPayment(IPayable payment) {
        payment.processPayment();
    }
}

// 特定支付方式实现IPayable接口
public class CreditCardPayment implements IPayable {
    @Override
    public void processPayment() {
        // 处理信用卡支付逻辑
    }
}

public class BankTransferPayment implements IPayable {
    @Override
    public void processPayment() {
        // 处理银行转账逻辑
    }
}

分析问题:

  1. 过度依赖: IPayable 接口强制所有实现类都实现 processPayment 方法,但不同支付方式可能需要不同的处理逻辑和附加步骤,例如信用卡授权或账户验证。
  2. 低内聚性: IPayable 接口可能包含了与支付处理无直接关联的方法,导致接口的内聚性降低。
  3. 扩展性差: 当需要添加新的支付方式或改变现有支付逻辑时,可能需要修改接口定义或对多个类进行更改,这增加了扩展的难度。
  4. 违反LSP: 如果子类重写了基类的方法以添加特定逻辑,这可能会影响使用基类引用的客户端代码,违反了里氏替换原则。
  5. 代码冗余: 为了满足接口定义,某些类可能不得不提供空实现或不相关的实现,导致代码冗余。

重构后:


public interface IPaymentProcessor {
    void processPayment();
}

public interface ICreditCardPaymentProcessor extends IPaymentProcessor {
    void authorizeCard();
}

public interface IBankTransferPaymentProcessor extends IPaymentProcessor {
    void validateAccount();
}

public class CreditCardPaymentProcessor implements ICreditCardPaymentProcessor {
    @Override
    public void processPayment() {
        // 处理信用卡支付逻辑
    }
    
    @Override
    public void authorizeCard() {
        // 授权信用卡
    }
}

public class BankTransferPaymentProcessor implements IBankTransferPaymentProcessor {
    @Override
    public void processPayment() {
        // 处理银行转账逻辑
    }
    
    @Override
    public void validateAccount() {
        // 验证账户信息
    }
}

public class PaymentService {
    public void processPayment(IPaymentProcessor paymentProcessor) {
        paymentProcessor.processPayment();
    }
}

解决的问题:

  1. 职责明确: 通过将 IPayable 拆分为 IPaymentProcessorICreditCardPaymentProcessorIBankTransferPaymentProcessor,每个接口都聚焦于特定的支付处理职责,提高了内聚性。
  2. 更好的扩展性: 新的支付方式可以通过实现相应的接口轻松添加,而不影响现有代码。例如,如果需要添加微信支付,可以创建一个实现 IPaymentProcessorWeChatPaymentProcessor 类。
  3. 遵循LSP: 子类(特定支付处理器)现在可以安全地替换基类(IPaymentProcessor),而不改变客户端代码的行为,因为它们提供了与基类相同的方法。
  4. 减少代码冗余: 每个支付处理器类只需要实现与其职责相关的接口方法,消除了不必要的空实现或不相关的方法。
  5. 灵活性增强: 通过分离接口,可以更灵活地组合不同的支付处理步骤,例如,可以在信用卡支付处理器中添加额外的授权步骤,而不会影响到其他支付方式。
  6. 易于维护: 当需要修改特定支付方式的逻辑时,只需修改对应的接口实现,而不会影响到其他支付方式的实现,简化了维护工作。

单一职责

1. 单一职责设计图:

2. 单一职责解决什么:

单一职责原则解决了类设计中的职责不明确和过度耦合问题。它确保每个类只关注一个功能领域,从而降低类之间的依赖,提高代码的可维护性和灵活性。

3. 单一职责使用场景:

当系统复杂度增加,原有类开始承担多个职责,或者当修改一个功能需要同时修改多个地方时,就需要考虑使用单一职责原则进行重构。

4. 订单案例

重构案例

  • 考虑一个电子商务平台的订单处理系统,其中订单服务类同时处理订单创建、支付和物流等。

重构前

public class OrderService {
    public void createOrder(Order order) {
        // 创建订单逻辑
    }
    public void processPayment(Order order) {
        // 处理支付逻辑
    }
    public void shipOrder(Order order) {
        // 发货逻辑
    }
}

重构后

public class OrderCreationService {
    public void createOrder(Order order) {
        // 创建订单逻辑
    }
}

public class PaymentService {
    public void processPayment(Order order) {
        // 处理支付逻辑
    }
}

public class ShipmentService {
    public void shipOrder(Order order) {
        // 发货逻辑
    }
}

开闭原则

1. 开闭原则设计图:

在这里插入图片描述

设计一个电子商务平台的支付模块,该模块需要能够处理不同类型的支付方式,例如信用卡支付和PayPal支付。随着市场的发展,我们可能需要添加更多的支付方式,如微信支付、Apple Pay等。

2. 开闭原则解决什么:

开闭原则解决了软件中因需求变更导致的频繁修改问题。遵循此原则的系统可以在不修改现有代码的基础上,通过增加新的代码来扩展功能。

3. 开闭原则使用场景:

当面临需求频繁变更或预期将来会有变更时,应考虑使用开闭原则来设计系统。

4. 折扣案例

图书管理系统需要根据不同的会员等级应用不同的折扣策略。我们预期未来可能会增加更多的会员等级和折扣规则。

重构前的代码:

  • 硬编码编写所有的折扣逻辑
public class DiscountCalculator {
    public double calculateDiscount(double originalPrice, String membershipLevel) {
        switch (membershipLevel) {
            case "Gold":
                return originalPrice * 0.90; // Gold members get 10% off
            case "Silver":
                return originalPrice * 0.95; // Silver members get 5% off
            default:
                return originalPrice; // No discount for others
        }
    }
}

应用开闭原则的重构:

为了遵循开闭原则,我们将折扣逻辑抽象化,使其能够容易地扩展新会员等级和折扣规则,而不需要修改现有的DiscountCalculator类。

  1. 定义折扣策略接口 (DiscountStrategy.java)
public interface DiscountStrategy {
    double calculateDiscount(double originalPrice);
}
  1. 实现具体的折扣策略 (GoldDiscountStrategy.java, SilverDiscountStrategy.java)
public class GoldDiscountStrategy implements DiscountStrategy {
    public double calculateDiscount(double originalPrice) {
        return originalPrice * 0.90;
    }
}

public class SilverDiscountStrategy implements DiscountStrategy {
    public double calculateDiscount(double originalPrice) {
        return originalPrice * 0.95;
    }
}
  1. 修改折扣计算器以使用策略 (DiscountCalculator.java)
public class DiscountCalculator {
    private DiscountStrategy discountStrategy;

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

    public double calculateDiscount(double originalPrice) {
        return discountStrategy.calculateDiscount(originalPrice);
    }
}
  1. 客户端代码 (Client.java)
public class Client {
    public static void main(String[] args) {
        DiscountStrategy goldDiscount = new GoldDiscountStrategy();
        DiscountCalculator calculator = new DiscountCalculator(goldDiscount);
        double discountedPrice = calculator.calculateDiscount(100.0);
        System.out.println("Discounted price for Gold member: " + discountedPrice);
        
        // Easily switch to a different discount strategy
        DiscountStrategy silverDiscount = new SilverDiscountStrategy();
        calculator.setDiscountStrategy(silverDiscount);
        discountedPrice = calculator.calculateDiscount(100.0);
        System.out.println("Discounted price for Silver member: " + discountedPrice);
    }
}

里氏替换原则

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

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

在这里插入图片描述

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

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

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

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

4. 汽车租赁

重构前:


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;
    }
}
;