Java項目中Service 層不直接返回Result 對象的原因分析
前言
昨天在Code Review時,我發(fā)現(xiàn)阿城在Service層直接返回了Result對象。
指出這個問題后,阿城有些不解,反問我為什么不能這樣寫。
于是我們展開了一場技術(shù)討論(battle ??)。
討論過程中,我發(fā)現(xiàn)這個看似簡單的設(shè)計問題,背后其實涉及分層架構(gòu)、職責(zé)劃分、代碼復(fù)用等多個重要概念。
與其讓這次討論的內(nèi)容隨風(fēng)而去,不如整理成文,幫助更多遇到同樣困惑的朋友理解原因。
知其然,更知其所以然。
耐心看完,你一定有所收獲。
正文
職責(zé)分離原則
在傳統(tǒng)的MVC架構(gòu)中,Service層和Controller層各自承擔(dān)著不同的職責(zé)。
Service層負責(zé)業(yè)務(wù)邏輯的處理,而Controller層負責(zé)HTTP請求的處理和響應(yīng)格式的封裝。
當(dāng)我們將數(shù)據(jù)包裝成 Result 對象的任務(wù)交給 Service 層時,意味著 Service 層不再單純地處理業(yè)務(wù)邏輯,而是牽涉到了數(shù)據(jù)處理和響應(yīng)的部分。
這樣會導(dǎo)致業(yè)務(wù)邏輯與表現(xiàn)邏輯的耦合,降低了代碼的清晰度和可維護性。
看一個不推薦的寫法:
@Service
public class UserService {
public Result<User> getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
return Result.error(404, 用戶不存在);
}
return Result.success(user);
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
return userService.getUserById(id);
}
}上面代碼中,Service 層不僅負責(zé)從數(shù)據(jù)庫獲取用戶信息,還直接處理了返回的結(jié)果。
如果我們需要改變返回的格式,或者進行錯誤信息的標準化,所有 Service 層的方法都需要修改。這樣會導(dǎo)致代碼的高耦合。
相比之下,以下做法將展示邏輯留給 Controller 層,保證了業(yè)務(wù)邏輯的純粹性:
@Service
public class UserService {
public User getUserById(Long id) {
User user = userMapper.selectById(id);
if (user == null) {
throw new BusinessException(用戶不存在);
}
return user;
}
}
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}
}讓每一層都專注于自己的職責(zé)。
可復(fù)用性問題
當(dāng)Service層返回Result時,會嚴重影響方法的可復(fù)用性。
假設(shè)我們有一個訂單服務(wù)需要調(diào)用用戶服務(wù):
@Service
public class OrderService {
@Autowired
private UserService userService;
public void createOrder(Long userId, OrderDTO orderDTO) {
// 不推薦的方式:需要解包Result
Result<User> userResult = userService.getUserById(userId);
if (!userResult.isSuccess()) {
throw new BusinessException(userResult.getMessage());
}
User user = userResult.getData();
// 后續(xù)業(yè)務(wù)邏輯
validateUserStatus(user);
// ...
}
}這種寫法有個很明顯的問題。
OrderService 作為另一個業(yè)務(wù)服務(wù),業(yè)務(wù)之間的調(diào)用本來應(yīng)該簡單直接,但使用 Result 帶來了兩個問題:
- 不知道
Result里到底包含什么,還得去查看代碼里面的實現(xiàn),寫起來麻煩。 - 還需要額外判斷
Result的狀態(tài),增加了不必要的復(fù)雜度。
如果是調(diào)用第三方外部服務(wù),需要這種包裝還能理解,但在自己業(yè)務(wù)之間互相調(diào)用時,完全沒必要這樣做。
如果Service返回純業(yè)務(wù)對象:
@Service
public class OrderService {
@Autowired
private UserService userService;
public void createOrder(Long userId, OrderDTO orderDTO) {
// 推薦的方式:直接獲取業(yè)務(wù)對象
User user = userService.getUserById(userId);
// 后續(xù)業(yè)務(wù)邏輯
validateUserStatus(user);
// ...
}
}代碼變得簡潔且符合直覺。
業(yè)務(wù)層之間直接傳遞業(yè)務(wù)對象,保持簡單和清晰。
異常處理機制
有些 Service 層在業(yè)務(wù)判斷失敗后,會直接返回 Result.fail(xxx) 這樣的代碼,例如:
public Result<Void> createOrder(Long userId, OrderDTO orderDTO) {
if (userId == null) {
return Result.fail("用戶ID不能為空");
}
// 后續(xù)業(yè)務(wù)邏輯
return Result.success();
}
這種做法有幾個問題:
- 重復(fù)的錯誤處理:每個方法都得寫一大堆類似的錯誤判斷代碼,增加了代碼量。
- 錯誤分散:錯誤處理分散在每個方法里,如果需要改進錯誤邏輯,要在多個地方修改,麻煩且容易出錯。
而如果我們通過拋出異常并結(jié)合全局異常處理來統(tǒng)一處理錯誤,例如:
public void createOrder(Long userId, OrderDTO orderDTO) {
if (userId == null) {
throw new BusinessException("用戶ID不能為空");
}
// 后續(xù)業(yè)務(wù)邏輯
}
再通過全局異常捕獲來轉(zhuǎn)換為 Result:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e) {
return Result.error(400, e.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系統(tǒng)異常", e); // 這里可以查看堆棧信息
return Result.error(500, "系統(tǒng)繁忙");
}
}這樣做的好處是:
- 減少重復(fù)代碼:業(yè)務(wù)方法不再需要寫重復(fù)的錯誤判斷,代碼更簡潔。
- 集中錯誤處理:錯誤處理集中在一個地方,修改時只需修改全局異常處理器,不用改動每個 Service 層方法。
- 業(yè)務(wù)與錯誤分離:業(yè)務(wù)邏輯專注處理核心功能,錯誤處理交給統(tǒng)一的機制,代碼更加清晰易懂。
而且異??梢詳y帶更豐富的上下文信息,如果業(yè)務(wù)側(cè)需要時,可以帶上堆棧信息,便于一些問題的定位。
測試便利性
Service層返回業(yè)務(wù)對象而不是Result時,能夠大大提升單元測試的便利性:
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetUserById() {
// 推薦的方式:直接斷言業(yè)務(wù)對象
User user = userService.getUserById(1L);
assertNotNull(user);
assertEquals(張三, user.getName());
}
@Test
public void testGetUserById_NotFound() {
// 推薦的方式:斷言拋出異常
assertThrows(BusinessException.class, () -> {
userService.getUserById(999L);
});
}
}如果Service返回Result,測試代碼則需要寫得更復(fù)雜:
@Test
public void testGetUserById() {
// 不推薦的方式:需要解包Result
Result<User> result = userService.getUserById(1L);
assertTrue(result.isSuccess());
assertNotNull(result.getData());
assertEquals(張三, result.getData().getName());
}
測試代碼變得莫名冗長,還得去關(guān)注響應(yīng)結(jié)構(gòu),這并不是Service層測試的關(guān)注點。
Service 層本應(yīng)專注于業(yè)務(wù)邏輯,測試也應(yīng)該直接驗證業(yè)務(wù)數(shù)據(jù)。
領(lǐng)域驅(qū)動設(shè)計角度
再換個角度。
從領(lǐng)域驅(qū)動設(shè)計(DDD)的角度來看,Service 層屬于應(yīng)用層或領(lǐng)域?qū)?,?yīng)該使用領(lǐng)域語言來表達業(yè)務(wù)邏輯。
而 Result 是基礎(chǔ)設(shè)施層的概念,代表 HTTP 響應(yīng)格式,不應(yīng)該污染領(lǐng)域?qū)印?/p>
例如,考慮轉(zhuǎn)賬業(yè)務(wù):
@Service
public class TransferService {
public TransferResult transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromAccountId);
Account toAccount = accountRepository.findById(toAccountId);
fromAccount.deduct(amount);
toAccount.deposit(amount);
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
return new TransferResult(fromAccount, toAccount, amount);
}
}在這個例子中,TransferResult 是一個領(lǐng)域?qū)ο?,代表了轉(zhuǎn)賬的結(jié)果,包含了與業(yè)務(wù)相關(guān)的意義,而不是一個通用的 HTTP 響應(yīng)封裝 Result。
這種做法更符合領(lǐng)域模型的表達,體現(xiàn)了領(lǐng)域?qū)拥穆氊?zé)——處理業(yè)務(wù)邏輯,而不是涉及 HTTP 響應(yīng)格式的細節(jié)。
接口適配的靈活性
當(dāng) Service 層返回純粹的業(yè)務(wù)對象時,Controller 層可以根據(jù)不同的接口需求靈活封裝響應(yīng):
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// REST接口返回Result
@GetMapping("/user/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}
// GraphQL接口直接返回對象
@QueryMapping
public User user(@Argument Long id) {
return userService.getUserById(id);
}
// RPC接口返回自定義格式
@DubboService
public class UserRpcServiceImpl implements UserRpcService {
public UserDTO getUserById(Long id) {
User user = userService.getUserById(id);
return convertToDTO(user);
}
}
}同一個Service方法可以被不同類型的接口復(fù)用,每個接口根據(jù)自己的協(xié)議要求封裝響應(yīng)。
強行使用 Result 會導(dǎo)致接口的適配性變差,無法根據(jù)不同協(xié)議的需求靈活定制響應(yīng)格式。
靈活性反而丟失了。
事務(wù)邊界清晰
Service 層通常是事務(wù)邊界所在,當(dāng) Service 返回業(yè)務(wù)對象時,事務(wù)的語義更加清晰:
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderDTO orderDTO) {
Order order = new Order();
// 設(shè)置訂單屬性
orderMapper.insert(order);
// 扣減庫存
inventoryService.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
return order;
}
}在這個例子中,事務(wù)是圍繞 Service 層的方法展開的,@Transactional 注解確保在業(yè)務(wù)邏輯執(zhí)行失敗時,事務(wù)會回滾。因為方法正常返回時,事務(wù)會提交;如果拋出異常,事務(wù)會回滾,事務(wù)的邊界非常明確。
如果 Service 返回的是 Result,很難界定事務(wù)是否應(yīng)該回滾。比如:
public Result<Order> createOrder(OrderDTO orderDTO) {
Order order = new Order();
// 設(shè)置訂單屬性
orderMapper.insert(order);
// 扣減庫存
Result<Void> inventoryResult = inventoryService.deduct(orderDTO.getProductId(), orderDTO.getQuantity());
if (!inventoryResult.isSuccess()) {
return Result.fail("庫存不足");
}
return Result.success(order);
}在這種情況下,如果庫存不足,雖然 Result 返回失敗信息,但事務(wù)并不會回滾,可能會導(dǎo)致數(shù)據(jù)不一致,反而還得額外去拋出異常。
而通過拋出異常的方式,事務(wù)的回滾語義非常清晰:異常拋出則回滾,方法正常返回則提交,這種設(shè)計確保了事務(wù)的邊界更加明確,避免了潛在的數(shù)據(jù)一致性問題。
寫在最后
到此這篇關(guān)于Java項目中Service 層不直接返回Result 對象的原因分析的文章就介紹到這了,更多相關(guān)java service 層不直接返回result 對象內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java?Date和SimpleDateFormat時間類詳解
這篇文章主要介紹了java?Date和SimpleDateFormat時間類詳解,文章圍繞主題展開詳細的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-08-08
Java實戰(zhàn)角色權(quán)限后臺腳手架系統(tǒng)的實現(xiàn)流程
只學(xué)書上的理論是遠遠不夠的,只有在實戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+Springboot+Maven+myBaits-Plus+Vue+Element-UI+Mysql實現(xiàn)一個角色權(quán)限后臺腳手架系統(tǒng),大家可以在過程中查缺補漏,提升水平2022-01-01
使用JAVA8 filter對List多條件篩選的實現(xiàn)
這篇文章主要介紹了使用JAVA8 filter對List多條件篩選的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
Java設(shè)置PDF跨頁表格重復(fù)顯示表頭行的步驟詳解
這篇文章主要給大家介紹了關(guān)于Java設(shè)置PDF跨頁表格重復(fù)顯示表頭行的相關(guān)資料,這里使用的是Free Spire.PDF for Java的jar包,Spire.PDF for Java 是一款專門對 PDF 文檔進行操作的 Java 類庫,需要的朋友可以參考下2021-07-07
Java?restTemplate發(fā)送get請求query參數(shù)傳遞問題解決
這篇文章主要為大家介紹了Java?restTemplate發(fā)送get請求query參數(shù)傳遞問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-11-11

