Bootstrap

将 SOLID 原则应用于 Spring Boot 应用程序

在软件开发中,面向对象设计对于创建可以轻松更改、扩展和再次使用的代码非常重要。

SOLID原则是面向对象编程软件开发中的五项设计原则,旨在创建更易于维护、更灵活、更可扩展的软件。它们由 Robert C. Martin 提出,被广泛用作设计简洁高效代码的指南。单词“ SOLID ”中的每个字母代表以下原则之一:

  1. 年代单一责任原则(SRP)
  2. 开放/封闭原则(OCP)
  3. L iskov 替代原则 (LSP)
  4. 接口隔离原则(ISP)
  5. 依赖倒置原则(DIP)

在本文中,我们将研究如何在 Spring Boot 应用程序中使用每个原则

  1. 单一职责原则(SRP)

一个类应该有且只有一个改变的原因。

正如其名称所示,单一职责原则有两个关键原则。

让我们在下面的例子中检查一下错误的用法。

@RestController
@RequestMapping("/report")
public class ReportController {

    private final ReportService reportService;


    public ReportController(ReportService reportService) {
        this.reportService = reportService;
    }

    @PostMapping("/send")
    public ResponseEntity<Report> generateAndSendReport(@RequestParam String reportContent,
                                                        @RequestParam String to,
                                                        @RequestParam String subject) {
        String report = reportService.generateReport(reportContent);
        reportService.sendReportByEmail(report, to, subject);
        return new ResponseEntity<>(HttpStatus.OK);
    }
}
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {

    private final ReportRepository reportRepository;

    public ReportServiceImpl(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Override
    public String generateReport(String reportContent) {
        Report report = new Report();
        report.setReportContent(reportContent);
        return reportRepository.save(report).toString();
    }

    @Override
    public void sendReportByEmail(Long reportId, String to, String subject) {
        Report report = findReportById(reportId);
        sendEmail(report.getReportContent(), to, subject);
    }

    private Report findReportById(Long reportId) {
        return reportRepository.findById(reportId)
                .orElseThrow(() -> new RuntimeException("Report not found"));
    }

    private void sendEmail(String content, String to, String subject) {
       log.info(content, to, subject);
    }

ReportService具有多项职责,这违反了单一职责:

  • 生成报告:该类负责生成报告并将其保存到generateReport方法中的存储库。
  • 通过电子邮件发送报告:该类还负责在sendReportByEmail方法中通过电子邮件发送报告。

创建代码时,需要避免将太多任务放在一个地方——无论是类还是方法。

这使得代码变得复杂且难以处理。这也使得进行小改动变得很棘手,因为它们可能会影响代码的其他部分,即使是很小的更新也需要测试所有内容。

让我们纠正这个实现;

为了遵守 SRP,这些职责被分为不同的类别。

@RestController 
@RequestMapping( "/report" ) 
public  class  ReportController { 

    private  final ReportService reportService; 
    private  final EmailService emailService; 

    public ReportController(ReportService reportService, EmailService emailService) { 
        this .reportService = reportService; 
        this .emailService = emailService; 
    } 

    @PostMapping( "/send" ) 
    public ResponseEntity<Report> generateAndSendReport( @RequestParam String reportContent, 
                                                        @RequestParam String to, 
                                                        @RequestParam String subject) { 
        // 正确的 impl reportService 负责生成
        Long reportId = Long .valueOf(reportService.generateReport(reportContent)); 
        // 正确的 impl emailService 负责发送
        emailService.sendReportByEmail(reportId, to, subject); 
        return new ResponseEntity<>(HttpStatus.OK); 
    } 
}
@Service
public class ReportServiceImpl implements ReportService {

    private final ReportRepository reportRepository;


    public ReportServiceImpl(ReportRepository reportRepository, EmailService emailService) {
        this.reportRepository = reportRepository;
    }

    @Override
    public String generateReport(String reportContent) {
        Report report = new Report();
        report.setReportContent(reportContent);
        return reportRepository.save(report).toString();
    }


@Service
public class EmailServiceImpl implements EmailService {

    private final ReportRepository reportRepository;

    public EmailServiceImpl(ReportRepository reportRepository) {
        this.reportRepository = reportRepository;
    }

    @Override
    public void sendReportByEmail(Long reportId, String to, String subject) {
        Report report = findReportById(reportId);
        if (ObjectUtils.isEmpty(report) || !StringUtils.hasLength(report.getReportContent())) {
            throw new RuntimeException("Report or report content is empty");
        }
    }

    private Report findReportById(Long reportId) {
        return reportRepository.findById(reportId)
                .orElseThrow(() -> new RuntimeException("Report not found"));
    }

}

重构后的代码包括以下更改;

  • ReportServiceImpl负责生成报告。
  • EmailServiceImpl负责通过ReportServiceImpl-电子邮件发送生成的报告。
  • ReportController通过使用适当的服务来管理生成和发送报告的过程。

2.开放/封闭原则(OCP)

开放-封闭原则是指类应该对扩展开放,对修改封闭。这有助于避免将错误引入正在运行的应用程序。简而言之,这意味着您应该能够在不更改现有代码的情况下向类添加新功能

让我们在下面的例子中检查一下错误的用法。

// 违反 OCP 的错误实现
public  class  ReportGeneratorService { 
    public String generateReport(Report report) { 
        if ( "PDF" .equals(report.getReportType())) { 
            // 错误:直接实现生成 PDF 报告
            return  "PDF report generated" ; 
        } else  if ( "Excel" .equals(report.getReportType())) { 
            // 错误:直接实现生成 Excel 报告
            return  "Excel report generated" ; 
        } else { 
            return  "Unsupported report type" ; 
        } 
    } 
}

在这个错误的实现中,generateReport的方法ReportService有条件语句来检查报告类型并直接生成相应的报告。这违反了开放封闭原则,因为如果你想添加对新报告类型的支持,你就需要修改这个类

让我们纠正这个实现;

public interface ReportGenerator {
    String generateReport(Report report);
}

@Component
public class PdfReportGenerator implements ReportGenerator {
    @Override
    public String generateReport(Report report) {
        return String.format("PDF report generated for %s", report.getReportType());
    }
}

@Component
public class ExcelReportGenerator implements ReportGenerator {
    @Override
    public String generateReport(Report report) {
        return String.format("Excel report generated for %s", report.getReportType());
    }
}

@Service
public class ReportGeneratorService {

    private final Map<String, ReportGenerator> reportGenerators;

    @Autowired
    public ReportGeneratorService(List<ReportGenerator> generators) {
        this.reportGenerators = generators.stream()
                .collect(Collectors.toMap(generator -> generator.getClass().getSimpleName(), Function.identity()));
    }

    public String generateReport(Report report, String reportType) {
        return reportGenerators.getOrDefault(reportType, unsupportedReportGenerator())
                .generateReport(report);
    }

    private ReportGenerator unsupportedReportGenerator() {
        return report -> "Unsupported report type";
    }
}

Interface ->ReportGenerator

  • 增加了一个接口(ReportGenerator来定义生成报告的通用方法。

Concrete Implementations ->PdfReportGeneratorExcelReportGenerator

  • 创建实现PDF 和 Excel 报告生成接口的类。
  • 遵循开放-封闭原则,允许扩展而不修改现有代码。

Report Generator Service -> ReportGeneratorService

  • 引入了管理不同报告生成器实现的服务。
  • 允许添加新的报告生成器而无需更改现有代码。

总而言之,该服务动态处理这些实现,从而可以轻松添加新功能而无需更改现有代码,遵循开放封闭原则。

3.里氏替代原则(LSP)

里氏替换原则指出,如果你有一个类,你应该能够用子类替换它,而不会给你的程序带来任何问题

换句话说,您可以在任何地方使用更通用的版本,并且一切仍应正常工作。

让我们在下面的例子中检查一下错误的用法。

// 违反 LSP 的错误实现
public  class  Bird { 
    public  void  fly ( ) { 
        // 我能飞
    } 

    public  void  swim ( ) { 
        // 我能游泳
    } 
} 

public  class  Penguin  extends  Bird { 

    // 企鹅不能飞,但是我们重写了 fly 方法并抛出异常
    @Override 
    public  void  fly ( ) { 
        throw  new  UnsupportedOperationException ( "企鹅不能飞" ); 
    } 
}

让我们纠正这个实现;

// LSP 的正确实现
public  class  Bird { 

    // 方法
} 

public  interface  Flyable { 
    void  fly () ; 
} 

public  interface  Swimmable { 
    void  swim () ; 
} 


public  class  Penguin  extends  Bird  implements  Swimmable { 
    // 企鹅不能飞,因此我们只实现 swim 接口
    @Override 
    public  void  swim () { 
        System. out .println( "I can swim" ); 
    } 
} 

public  class  Eagle  extends  Bird  implements  Flyable { 
    @Override 
    public  void  fly () { 
        System. out .println( "I can fly" ); 
    } 
}
  • Bird该类是鸟类的基类,包括所有鸟类共有的共同属性或方法。
  • 引入FlyableSwimmable接口来表示特定的行为。
  • Penguin课堂上,实现了Swimmable体现企鹅游泳能力的界面。
  • Eagle课堂上,实现了Flyable体现老鹰飞翔能力的界面。

通过将特定行为分离到接口并在子类中实现它们,我们遵循里氏替换原则,该原则让我们可以切换子类而不会引起任何意外问题。

4.接口隔离原则(ISP)

接口隔离原则指出,较大的接口应该分成较小的接口。

通过这样做,我们可以确保实现类只需要关注它们感兴趣的方法。

让我们在下面的例子中检查一下错误的用法

public  interface  Athlete { 

    void  compete () ; 

    void  swim () ; 

    void  highJump () ; 

    void  longJump () ; 
} 

// 违反接口隔离的错误实现
public  class  xiaoming  implements  Athlete { 
    @Override 
    public  void  compete () { 
        System. out .println( "xiaoming started contest" ); 
    } 

    @Override 
    public  void  swim () { 
        System. out .println( "xiaoming started swimming" ); 
    } 

    @Override 
    public  void  highJump () { 
    // 对于 xiaoming 来说不是必需的
    } 

    @Override 
    public  void  longJump () { 
    // 对于 xiaoming 来说不是必需的
    } 
}

 

假设小明 是一名游泳运动员。他被迫为 和 提供空的实现highJumplongJump这与他作为游泳运动员的角色无关。

让我们纠正这个实现;

public interface Athlete {

    void compete();
}

public interface JumpingAthlete {

    void highJump();

    void longJump();
}

public interface SwimmingAthlete {

    void swim();
}

public class xiaoming implements Athlete, SwimmingAthlete {
    @Override
    public void compete() {
        System.out.println("xiaoming started competing");
    }

    @Override
    public void swim() {
        System.out.println("xiaoming started swimming");
    }
}

原有的Athlete界面被拆分为三个独立的界面:Athlete一般活动界面、JumpingAthlete跳跃相关活动界面、SwimmingAthlete游泳界面。

这遵循接口隔离原则,确保类不会被迫实现它不需要的方法。

5.依赖倒置原则(DIP)

依赖倒置原则 (DIP) 指出,高级模块不应该依赖于低级模块;两者都应该依赖于抽象。抽象不应该依赖于细节。

让我们在下面的例子中检查一下错误的用法。

// 依赖倒置原则的错误实现
@Service 
public  class  PayPalPaymentService { 

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

@RestController 
public  class  PaymentController { 

    // 直接依赖于具体的实现
    private  final PayPalPaymentService paymentService; 

    // 构造函数直接初始化具体的实现
    public  PaymentController () { 
        this .paymentService = new  PayPalPaymentService (); 
    } 

    @PostMapping("/pay") 
    public  void  pay ( @RequestBody Order order) { 
        paymentService.processPayment(order); 
    } 
}

让我们纠正这个实现;

// 引入接口
public  interface  PaymentService { 
    void  processPayment (Order order) ; 
} 

// 在服务类中实现接口
@Service 
public  class  PayPalPaymentService  implements  PaymentService { 
    @Override 
    public  void  processPayment (Order order) { 
        // 支付处理逻辑
    } 
} 

@RestController 
public  class  PaymentController { 

    private  final PaymentService paymentService; 

    // 构造函数注入
    public  PaymentController (PaymentService paymentService) { 
        this .paymentService = paymentService; 
    } 

    @PostMapping("/pay") 
    public  void  pay ( @RequestBody Order order) { 
        paymentService.processPayment(order); 
    } 
}
  • 引入PaymentService接口。
  • PaymentService接口注入到控制器的构造函数中以便在控制器中提供抽象。
  • 控制器依赖于抽象PaymentService),允许实现接口的任何类的依赖注入。

依赖倒置原则 (DIP)依赖注入 (DI)是 Spring 框架中相互关联的概念。DIP 由Bob Martin 大叔提出,旨在保持代码松散连接。它将 Spring 中的依赖注入代码分离出来,框架在运行时管理应用程序。

结论

SOLID 原则在面向对象编程 (OOP) 中至关重要,因为它们提供了一组指南和最佳实践来设计更易于维护、更灵活和可扩展的软件。

在本文中,我们首先讨论了在 Java 应用程序中应用 SOLID 原则的错误。之后,我们研究了相关示例,以了解如何解决这些问题。

所有示例均以基础级别呈现,您可以参考所提供的参考资料进行进一步阅读。

;