精益求精!在 Spring Boot 應(yīng)用中應(yīng)用 SOLID 原則
在軟件開(kāi)發(fā)中,面向?qū)ο笤O(shè)計(jì)對(duì)于創(chuàng)建可輕易修改、擴(kuò)展和重復(fù)使用的代碼至關(guān)重要。
SOLID原則是一組在面向?qū)ο缶幊毯蛙浖_(kāi)發(fā)中的五個(gè)設(shè)計(jì)原則,旨在創(chuàng)建更易維護(hù)、靈活和可擴(kuò)展的軟件。這些原則由Robert C. Martin提出,廣泛作為設(shè)計(jì)清晰高效代碼的指導(dǎo)方針?!癝OLID”一詞中的每個(gè)字母代表一個(gè)原則:
- 單一責(zé)任原則(SRP)
- 開(kāi)放/封閉原則(OCP)
- 里氏替換原則(LSP)
- 接口隔離原則(ISP)
- 依賴反轉(zhuǎn)原則(DIP)
在本文中,我們將深入探討這些原則在Spring Boot應(yīng)用中的使用。
單一責(zé)任原則(SRP)
Robert C. Martin描述道:
一個(gè)類應(yīng)該有且僅有一個(gè)理由去改變。
單一責(zé)任原則有兩個(gè)關(guān)鍵點(diǎn),如其名所示。
讓我們看看下面的錯(cuò)誤用法示例。
@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);
}
}
// 錯(cuò)誤實(shí)現(xiàn)
@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(); // 返回報(bào)告的字符串表示
}
@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("未找到報(bào)告")); // 修改為中文錯(cuò)誤信息
}
private void sendEmail(String content, String to, String subject) {
log.info(content, to, subject);
}
}
如您所見(jiàn),ReportService有多個(gè)責(zé)任,違反了單一責(zé)任原則:
- 生成報(bào)告:負(fù)責(zé)在generateReport方法中生成報(bào)告并將其保存到倉(cāng)庫(kù)。
- 通過(guò)電子郵件發(fā)送報(bào)告:在sendReportByEmail方法中也負(fù)責(zé)發(fā)送報(bào)告。
創(chuàng)建代碼時(shí),需要避免在一個(gè)地方放入過(guò)多任務(wù)——無(wú)論是類還是方法。
這使代碼復(fù)雜且難以處理,也使得進(jìn)行小修改變得棘手,因?yàn)樗鼈兛赡苡绊懘a的其他部分,導(dǎo)致即使是小更新也需要測(cè)試所有內(nèi)容。
讓我們糾正這個(gè)實(shí)現(xiàn);
為遵循SRP,這些責(zé)任被分離到不同的類中。
@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) {
Long reportId = Long.valueOf(reportService.generateReport(reportContent));
emailService.sendReportByEmail(reportId, to, subject);
return new ResponseEntity<>(HttpStatus.OK);
}
}
@Service
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(); // 返回報(bào)告的字符串表示
}
}
@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("報(bào)告或報(bào)告內(nèi)容為空"); // 修改為中文錯(cuò)誤信息
}
}
private Report findReportById(Long reportId) {
return reportRepository.findById(reportId)
.orElseThrow(() -> new RuntimeException("未找到報(bào)告")); // 修改為中文錯(cuò)誤信息
}
}
重構(gòu)后的代碼包括以下更改:
- ReportServiceImpl負(fù)責(zé)生成報(bào)告。
- EmailServiceImpl負(fù)責(zé)通過(guò)電子郵件發(fā)送報(bào)告——這些報(bào)告由ReportServiceImpl生成。
- ReportController管理生成和發(fā)送報(bào)告的過(guò)程,使用適當(dāng)?shù)姆?wù)。
開(kāi)放/封閉原則(OCP)
開(kāi)放-封閉原則表示一個(gè)類應(yīng)該對(duì)擴(kuò)展開(kāi)放,對(duì)修改封閉。這有助于避免在工作應(yīng)用中引入錯(cuò)誤。簡(jiǎn)單來(lái)說(shuō),這意味著您應(yīng)該能夠向類中添加新功能,而無(wú)需更改現(xiàn)有代碼。
讓我們看看下面的錯(cuò)誤用法示例。
public class ReportGeneratorService {
public String generateReport(Report report) {
if ("PDF".equals(report.getReportType())) {
return "生成PDF報(bào)告"; // 修改為中文輸出
} else if ("Excel".equals(report.getReportType())) {
return "生成Excel報(bào)告"; // 修改為中文輸出
} else {
return "不支持的報(bào)告類型"; // 修改為中文輸出
}
}
}
在這個(gè)錯(cuò)誤實(shí)現(xiàn)中,ReportService的generateReport方法包含條件語(yǔ)句來(lái)檢查報(bào)告類型,并直接生成相應(yīng)的報(bào)告。這違反了開(kāi)放-封閉原則,因?yàn)槿绻胩砑訉?duì)新報(bào)告類型的支持,您需要修改這個(gè)類。
讓我們糾正這個(gè)實(shí)現(xiàn);
public interface ReportGenerator {
String generateReport(Report report);
}
// 生成PDF報(bào)告的具體實(shí)現(xiàn)
@Component
public class PdfReportGenerator implements ReportGenerator {
@Override
public String generateReport(Report report) {
return String.format("為%s生成PDF報(bào)告", report.getReportType()); // 修改為中文輸出
}
}
// 生成Excel報(bào)告的具體實(shí)現(xiàn)
@Component
public class ExcelReportGenerator implements ReportGenerator {
@Override
public String generateReport(Report report) {
return String.format("為%s生成Excel報(bào)告", report.getReportType()); // 修改為中文輸出
}
}
// 遵循OCP的服務(wù)
@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 -> "不支持的報(bào)告類型"; // 修改為中文輸出
}
}
- 引入了ReportGenerator接口,定義了一個(gè)通用的方法來(lái)生成報(bào)告。
- 創(chuàng)建了實(shí)現(xiàn)該接口的PdfReportGenerator和ExcelReportGenerator類。
- 引入了ReportGeneratorService,管理不同的報(bào)告生成實(shí)現(xiàn),允許在不改變現(xiàn)有代碼的情況下添加新的報(bào)告生成器。
里氏替換原則(LSP)
里氏替換原則表示,如果您有一個(gè)類,您應(yīng)該能夠用一個(gè)子類替換它,而不會(huì)對(duì)程序造成任何問(wèn)題。換句話說(shuō),您可以在任何使用通用版本的地方使用特定版本,且一切仍然應(yīng)正常工作。
讓我們看看下面的錯(cuò)誤用法示例。
public class Bird {
public void fly() {
// 我會(huì)飛
}
public void swim() {
// 我會(huì)游泳
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("企鵝不能飛"); // 修改為中文錯(cuò)誤信息
}
}
讓我們糾正這個(gè)實(shí)現(xiàn);
public class Bird {
// 方法
}
public interface Flyable {
void fly(); // 飛
}
public interface Swimmable {
void swim(); // 游泳
}
public class Penguin extends Bird implements Swimmable {
@Override
public void swim() {
System.out.println("我會(huì)游泳");
}
}
public class Eagle extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("我會(huì)飛");
}
}
Bird類作為鳥類的基類,包含所有鳥類共享的通用屬性或方法。引入Flyable和Swimmable接口以表示特定行為。在Penguin類中,實(shí)現(xiàn)Swimmable接口以反映企鵝的游泳能力;在Eagle類中,實(shí)現(xiàn)Flyable接口以反映老鷹的飛行能力。通過(guò)將特定行為分離到接口中,并在子類中實(shí)現(xiàn)它們,我們遵循了里氏替換原則,允許我們?cè)诓灰鹨馔鈫?wèn)題的情況下切換子類。
接口隔離原則(ISP)
接口隔離原則指出,較大的接口應(yīng)拆分為較小的接口。這樣可以確保實(shí)現(xiàn)類只需關(guān)心對(duì)它們感興趣的方法。
讓我們看看下面的錯(cuò)誤用法示例。
public interface Athlete {
void compete(); // 比賽
void swim(); // 游泳
void highJump(); // 跳高
void longJump(); // 跳遠(yuǎn)
}
// 錯(cuò)誤實(shí)現(xiàn)違反接口隔離
public class JohnDoe implements Athlete {
@Override
public void compete() {
System.out.println("路條小哥開(kāi)始比賽");
}
@Override
public void swim() {
System.out.println("路條小哥開(kāi)始游泳");
}
@Override
public void highJump() {
// 路條小哥來(lái)說(shuō)不必要
}
@Override
public void longJump() {
// 路條小哥來(lái)說(shuō)不必要
}
}
假設(shè)John Doe是一名游泳運(yùn)動(dòng)員,他被迫提供高跳和長(zhǎng)跳的空實(shí)現(xiàn),這對(duì)他作為游泳運(yùn)動(dòng)員的角色無(wú)關(guān)緊要。
讓我們糾正這個(gè)實(shí)現(xiàn);
public interface Athlete {
void compete(); // 比賽
}
public interface JumpingAthlete {
void highJump(); // 跳高
void longJump(); // 跳遠(yuǎn)
}
public interface SwimmingAthlete {
void swim(); // 游泳
}
// 正確的接口隔離實(shí)現(xiàn)
public class JohnDoe implements Athlete, SwimmingAthlete {
@Override
public void compete() {
System.out.println("路條小哥開(kāi)始比賽");
}
@Override
public void swim() {
System.out.println("路條小哥開(kāi)始游泳");
}
}
原始的Athlete接口被拆分為三個(gè)獨(dú)立接口:Athlete用于一般活動(dòng),JumpingAthlete用于跳躍相關(guān)活動(dòng),SwimmingAthlete用于游泳。這遵循了接口隔離原則,確保一個(gè)類不被迫實(shí)現(xiàn)它不需要的方法。
依賴反轉(zhuǎn)原則(DIP)
依賴反轉(zhuǎn)原則(DIP)指出,高層模塊不應(yīng)依賴于低層模塊;兩者都應(yīng)依賴于抽象。抽象不應(yīng)依賴于細(xì)節(jié)。
讓我們看看下面的錯(cuò)誤用法示例。
@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);
}
}
讓我們糾正這個(gè)實(shí)現(xiàn);
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接口。
- 在控制器的構(gòu)造函數(shù)中注入PaymentService接口,以提供控制器中的抽象??刂破饕蕾囉诔橄螅≒aymentService),允許注入任何實(shí)現(xiàn)該接口的類。
結(jié)論
SOLID原則在面向?qū)ο缶幊蹋∣OP)中至關(guān)重要,因?yàn)樗鼈兲峁┝艘唤M設(shè)計(jì)更易維護(hù)、靈活和可擴(kuò)展的軟件的指南和最佳實(shí)踐。在本文中,我們首先討論了在Java應(yīng)用中應(yīng)用SOLID原則時(shí)的錯(cuò)誤,隨后檢查了相關(guān)示例,以查看這些問(wèn)題是如何修復(fù)的。