Segregating Commands and Queries#
在前一個 module 中,我們將應用程式重構為 task-based interface。所有學生可以執行的操作現在都有明確定義,並擁有各自的 API endpoints。本模組的下一個目標是為這些 API endpoints 引入顯式的 commands 和 queries,這將帶來 CQRS 的三大優勢:scalability、performance 和 simplicity。
Introducing a First Command#
首先,我們需要辨識 StudentController 中哪些 API endpoints 屬於 commands,哪些屬於 queries:
- Query:
getList方法 – 從資料庫查詢學生列表並回傳,不會改變狀態 - Commands:
Register、Unregister、Enroll、Transfer、Disenroll、EditPersonalInfo– 這些方法都會改變資料庫狀態,且除了操作確認外不回傳資料
這個分類與 REST 最佳實務完美對齊:PUT/POST HTTP methods 對應 commands,GET HTTP method 對應 queries。REST 本質上遵循 Command-Query Separation 原則。
為了繼續走向 CQRS,我們需要為每個 command 和 query 建立獨立的 class。以 EditPersonalInfo 為例,建立一個 command class:
package logic.students;
public class EditPersonalInfoCommand {
public long id;
public String name;
public String email;
// setters & getters
}Command 與 Handler 的分離#
Command 本身應該是意圖的宣告(declaration of intent),代表「需要做什麼」。而執行(execution)是另一回事,通常需要存取外部世界(如資料庫、第三方系統),因此不應把執行邏輯直接放在 command 中。應該建立一個獨立的 command handler class:
class EditPersonalInfoCommandHandler {
public void handle(EditPersonalInfoCommand command) {
}
}Handler 包含一個 handle 方法,接受 command 並執行對應的業務邏輯。
引入共用 Interface#
為了之後能引入 decorators 來擴展 handler 的功能,我們需要一個共用的 interface。引入兩個新型別:
ICommand – 一個 marker interface,用於標記 codebase 中的 commands:
interface Command {}
public class EditPersonalInfoCommand implements ICommand {
// ...
}ICommandHandler – 定義 handler 的共用介面:
interface ICommandHandler {
public Result handle(ICommand command);
}使用 Result class 處理錯誤#
不建議讓 command handler 回傳 IActionResult 或拋出 exception 來處理驗證錯誤。更好的做法是使用一個 Result class,代表操作的成功或失敗:
class EditPersonalInfoCommandHandler implements ICommandHandler {
private UnitOfWork unitOfWork;
public EditPersonalInfoCommandHandler(UnitOfWork unitOfWork) {
this.unitOfWork = unitOfWork;
}
public Result handle(EditPersonalInfoCommand command) {
Repository studentRepository = new StudentRepository(unitOfWork);
Student student = studentRepository.getById(command.getId());
if (student == null)
return Result.fail("No student found with Id '{id}'");
student.setName(command.getName());
student.setEmail(command.getEmail());
unitOfWork.commit();
return Result.oK();
}
}不要用 exception 來控制程式流程或做驗證。Exception 會讓程式碼變得複雜且難以追蹤,使用明確的回傳值(如
Resultclass)是更好的設計選擇。
Controller 此時的職責變得很簡單:建立 command、實例化 handler、呼叫 handle、解析結果:
@PutMapping("{id}")
public IActionResult editPersonalInfo(@PathParam("id") long id,
@RequestBody StudentPersonalInfoDto dto) {
EditPersonalInfoCommand command = new EditPersonalInfoCommand();
command.setEmail(dto.getEmail());
command.setName(dto.getName());
command.setId(id);
EditPersonalInfoCommandHandler handler = new EditPersonalInfoCommandHandler(unitOfWork);
Result result = handler.handle(command);
return result.isSuccess() ? ok() : Error(result.error());
}Commands in CQS vs Commands in CQRS#
我們引入了第一個 command(EditPersonalInfoCommand)和它的 command handler。需要注意的是,Command 這個詞在兩種不同的語境下有不同的含義:
- CQS 中的 Command:指的是一個 controller method,它會改變應用程式狀態且不回傳資料給 client
- CQRS 中的 Command:指的是一個 class(如
EditPersonalInfoCommand),是需要在應用程式中執行的操作的顯式表示
你可以把 CQRS command 想像成 serializable method call – 一個可序列化的方法呼叫。Command handler 的
handle()方法本身在 CQS 的意義上也是一個 command,因為它會改變狀態且除了操作確認外不回傳任何東西。這兩個概念是緊密相連的。
Commands and Queries in CQRS#
應用程式中的所有 messages 可以分為三類:
- Commands:告訴應用程式去做某件事(Tell the application to do something)
- Queries:向應用程式詢問某些資訊(Ask the application about something)
- Events:通知外部應用程式某些變更(Inform external applications about some change)
Client 透過 commands 和 queries 與我們的應用程式互動;而應用程式則透過 events 與外部系統溝通。

三種訊息流:Client 透過 Commands 和 Queries 與應用程式互動,應用程式透過 Events 通知外部系統
命名規範#
三種 messages 各有各的命名規範:
- Commands:使用祈使語氣(imperative tense),例如
EditPersonalInfoCommand,因為它們是在「命令」應用程式做某件事。這也暗示伺服器可以拒絕這個命令 - Queries:通常以 “Get” 開頭,例如
GetListQuery,因為 queries 是在請求應用程式提供資料 - Events:使用過去式(past tense),例如
PersonalInfoChangedEvent,因為它們描述的是已經發生的事實,應用程式無法拒絕一個 event
使用 ubiquitous language 來命名 commands,避免 CRUD-based thinking。例如,不要使用
CreateStudentCommand/UpdateStudentCommand/DeleteStudentCommand,而應使用更貼近業務語言的名稱。
後綴(Command、Query、Event)並非必要。如果你遵循上述命名規範,光靠命名慣例就足以區分三種 messages。保留或省略後綴都是可以的。
Commands and Queries in the Onion Architecture#
在 Onion Architecture 中,應用程式分為三層:
- Core domain(核心層):Entities、Aggregates、Value Objects、Domain Events、Pure Domain Services
- Non-core domain(中間層):Repositories、Impure Domain Services
- Non-domain(外層):Application Services、UI

Onion Architecture 三層:Core Domain(Entities、Aggregates 等)、Non-core Domain(Repositories)、Non-domain(Application Services、UI)
Messages 屬於 Core Domain#
所有 messages(commands、queries、events)都屬於 core domain 層,與 domain events 位於同一層。它們明確地表示:
- Command = 對應用程式執行的操作
- Query = 向應用程式提出的問題
- Event = 給外部應用程式的結果
Push Model vs Pull Model#
Commands 和 events 位於同一個抽象層次,但有一個關鍵差異:
- Commands 遵循 push model:是外部(其他人、其他系統)發送 commands 給我們的應用程式
- Events 遵循 pull model:是我們的應用程式自己產生 events,通知外部系統

Commands 遵循 Push Model(外部推入),Events 遵循 Pull Model(內部產出)
Command Handlers 的位置#
由於 commands 和 queries 屬於 core domain,它們不應包含處理邏輯(handling logic)。Core domain 中的 classes 不應該引用外部世界的元件。
Command handlers 則應放在 Application Services 層,因為它們扮演的角色與 Application Services 相同 – 需要存取外部世界(如 repository、database)。將 handler 從 controller 搬到獨立的 handler class,只是在 Application Services 層內部的重新安排。

Command Handlers 位於 Application Services 層(Non-domain),Core Domain 中的 classes 應與外部世界隔離
Commands vs DTOs#
Commands 和 DTOs(Data Transfer Objects)是不同的東西,解決不同的問題:
- Commands = serializable method calls(可序列化的方法呼叫)
- DTOs = data contracts(資料契約),主要用於提供 backward compatibility
為什麼不該用 Command 取代 DTO#
許多人會跳過 DTO 到 command 的 mapping 階段,直接讓 controller 接受 command:
@PutMapping("{id}")
public IActionResult editPersonalInfo(@RequestBody EditPersonalInfoCommand command) {
IHandler handler = new EditPersonalInfoCommandHandler(unitOfWork);
Result result = handler.handle(command);
return result.isSuccess() ? ok() : error(result.error);
}這樣做的問題在於,command 直接暴露給外部 client。如果未來需要修改 command 的結構(例如將 name 拆分為 firstName 和 lastName),就無法在保持 backward compatibility 的同時修改 command。
使用 commands 取代 DTOs 類似於使用 domain entities 取代 DTOs,兩者都會妨礙你重構 domain model 的能力。
DTO 與 Command 各司其職#
正確的做法是保留兩層,讓它們各自負責:
- DTOs = backward compatibility(向後相容性),可以有多個版本的 data contracts
- Commands = 明確表達應用程式可以做什麼(actions upon the application)
兩者之間透過 mapping 來連接,確保應用程式既能保持向後相容,又能自由重構。
如果你不需要 backward compatibility(例如你只有一個自己開發的 client,且可以同時部署 API 和 client),那麼直接使用 commands 取代 DTOs 是可以的。將其視為一種程式碼複雜度的優化手段。
Leveraging Spring to Resolve Command and Handlers Pattern#
手動在每個 controller action 中實例化 handlers 是不方便的。我們可以利用 Spring 的 dependency injection 來自動解析 command handlers。
使用 @CommandHandler annotation 標記 handler,並透過 @Autowired 注入依賴:
@CommandHandler
public class EditPersonalInfoCommandHandler
implements ICommandHandler<EditPersonalInfoCommand, Result> {
@Autowired
UnitOfWork unitOfWork;
@Override
public Result handle(final EditPersonalInfoCommand command) throws Exception {
Repository studentRepo = new StudentRepository(unitOfWork);
Student student = studentRepo.getById(command.getId());
if (student == null)
return Result.fail("No student found with Id '{id}'");
student.setName(command.getName());
student.setEmail(command.getEmail());
unitOfWork.commit();
return Result.oK();
}
}透過注入 Gate dependency,controller 變得非常簡潔:
@RestController
public class StudentController {
@Autowired
private Gate gate;
@PutMapping("{id}")
public IActionResult editPersonalInfo(@RequestBody EditPersonalInfoCommand command) {
Result result = gate.dispatch(command);
return result.isSuccess() ? ok() : error(result.error);
}
}傳送 command 有兩種方式:
- 同步:
gate.dispatch(command); - 非同步:
gate.dispatchAsync(command);
Introducing a Query#
利用 Spring 的 dependency injection 和 Command-Handler pattern 非常方便。接下來引入 query 的部分。
與 ICommand 類似,建立 IQuery interface 和 IQueryHandler interface:
public interface IQuery {}
public interface IQueryHandler {
IResult handle(IQuery query);
}差別在於 command handler 通常只回傳操作成功或失敗的確認(Result),而 query handler 需要回傳具體的資料,且資料型別會因 query 不同而不同。
以 GetListQuery 為例:
public class GetListQuery implements IQuery {
private String enrolledIn;
private int numberOfCourses;
// getters, setters and constructor
}
@QueryHandler
public class GetListQueryHandler implements IQueryHandler<GetListQuery, List<StudentDto>> {
@Autowired
private UnitOfWork unitOfWork;
public List<StudentDto> handle(GetListQuery query) {
Repository studentRepo = new StudentRepository(unitOfWork);
List<StudentDto> dtos = convertToDtos(studentRepo.getList(enrolled, number));
return dtos;
}
}Controller 中使用 Gate 來 dispatch query,與 command 保持一致的模式:
public class StudentController {
@Autowired
private Gate gate;
@GetMapping()
public IActionResult getList(String enrolled, int number) {
GetListQuery query = new GetListQuery(enrolled, number);
Result result = gate.dispatch(query);
return result.isSuccess() ? ok() : error(result.error);
}
}Query handler 不需要知道
StudentDto的存在(理想情況下,所有 DTOs / data contracts 應該放在獨立的 assembly 中),API 和 Logic 兩個 project 都引用這個獨立的 data contracts project。
小結#
本模組的核心內容是將應用程式重構為使用顯式的 commands 和 queries,以下是關鍵要點:
- CQS vs CQRS 中的 Command:在 CQS 中,command 是一個改變狀態的 method;在 CQRS 中,command 是一個代表「應用程式可以做什麼」的 class
- Command 是 serializable method call,command handler 相當於只有一個 CQS command method 的 Spring controller
- 三種 Messages:commands(告訴應用程式做事)、queries(向應用程式詢問)、events(通知外部系統)
- 命名規範:commands 用祈使語氣、events 用過去式、queries 以 “Get” 開頭,務必使用 ubiquitous language
- Onion Architecture 中的位置:commands、queries 和 events 屬於 core domain 層;command handlers 屬於 Application Services 層
- Commands vs DTOs:DTOs 負責 backward compatibility,commands 負責明確表達應用程式的操作。除非不需要向後相容,否則不應混用
- 利用 Spring DI:透過 Spring 的 dependency injection 和 Gate pattern,自動解析 handlers,讓 controller 保持簡潔
- 下一個模組將討論如何在 command 和 query handlers 之上實作 decorators,以簡單而強大的方式引入 cross-cutting concerns