在软件开发中,面向对象设计对于创建可以轻松更改、扩展和再次使用的代码非常重要。
SOLID原则是面向对象编程和软件开发中的五项设计原则,旨在创建更易于维护、更灵活、更可扩展的软件。它们由 Robert C. Martin 提出,被广泛用作设计简洁高效代码的指南。单词“ SOLID ”中的每个字母代表以下原则之一:
- 年代单一责任原则(SRP)
- 开放/封闭原则(OCP)
- L iskov 替代原则 (LSP)
- 接口隔离原则(ISP)
- 依赖倒置原则(DIP)
在本文中,我们将研究如何在 Spring Boot 应用程序中使用每个原则
- 单一职责原则(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 ->PdfReportGenerator
和ExcelReportGenerator
- 创建实现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
该类是鸟类的基类,包括所有鸟类共有的共同属性或方法。- 引入
Flyable
和Swimmable
接口来表示特定的行为。 - 在
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 来说不是必需的
}
}
假设小明 是一名游泳运动员。他被迫为 和 提供空的实现highJump
,longJump
这与他作为游泳运动员的角色无关。
让我们纠正这个实现;
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 原则的错误。之后,我们研究了相关示例,以了解如何解决这些问题。
所有示例均以基础级别呈现,您可以参考所提供的参考资料进行进一步阅读。