将 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 原则的错误。之后,我们研究了相关示例,以了解如何解决这些问题。

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值