Như Lỗ Tấn đã nói, "Trên đời này làm gì có đường, người ta đi mãi thì thành đường thôi.". Vì vậy, giai đoạn bắt đầu tìm hiểu luôn là quan trọng nhất. Nếu bắt đầu mọi thứ quá phức tạp từ đầu, có thể các bạn sẽ cảm thấy mất hứng thú và không muốn tiếp tục đọc các phần sau.
Bài viết này gồm nhiều phần, đầu tiên là mình nói về Command Query Separation, sau đó mới là phần Command Query Responsibility Segregation. Mục đích chính là chúng ta đi từ cách tiếp cận đơn giản đến phức tạp.
Command Query Separation
Ý tưởng cơ bản là chia các method của 1 đối tượng thành 2 loại:
- Command (Lệnh): Được sử dụng để thực hiện các thay đổi dữ liệu, như thêm, sửa, xóa.
- Query (Truy vấn): Được sử dụng để đọc dữ liệu, không gây ảnh hưởng đến trạng thái của hệ thống.
Giả sử bạn muốn viết 1 BookService, thông thường bạn sẽ có các hàm như sau:
interface IBookService
{
void Edit(string name);
Book GetById(long id);
IEnumerable<Book> GetAllBooks();
}
Ở đây bạn có 2 loại method Read và Write
- Hàm Edit không trả về giá trị, chỉ cập nhật Book theo name. Đây là hàm thay đổi dữ liệu (mutable data) => command
- Hàm Get không thay đổi trạng thái (do not mutable the state). Đây là hàm get data => query
Nếu bạn phân tách ra 2 service IBookQueryService và IBookCommandService thì việc quản lý source code trở nên dễ dàng và dễ hiểu hơn.
interface IBookCommandService
{
void Edit(string name);
}
interface IBookQueryService
{
Book GetById(long id);
IEnumerable<Book> GetAllBooks();
}
Một số sai lầm thường gặp
Mình rút ra từ kinh nghiệm mình làm nên nhờ các bạn góp ý thêm
Không phân biệt rõ ràng giữa Command và Query
Sai lầminterface IBookService
{
void Edit(string name);
Book GetById(long id);
IEnumerable<Book> GetAllBooks();
}
Cách khắc phục
interface IBookCommandService
{
void Edit(string name);
}
interface IBookQueryService
{
Book GetById(long id);
IEnumerable<Book> GetAllBooks();
}
Quiz
Hàm nào dưới đây là hàm Command hợp lệ1. void getComment (int commentId);
2. Job createJob (Job job);
3. List<Comment> postComment (Comment comment);
4. void approveComment (int commentId);
=> Hàm 4
Hàm nào là hàm Query hợp lệ
1. Task<List<Comment>> GetAllCommentAsync();
2. Task<Comment> GetCommentByIdAsync(int commentId);
3. Task GetAllCommentAsync();
4. Task<Comment> GetAllCommentByIdAsync(int commentId);
=> Hàm 1, 2, 4
Không tuân thủ nguyên tắc Single Responsibility
public class CommentCommandService
{
// ...
public async Task PostCommentAndPostToSlack(string name, string url, string content)
{
var comment = await this.commentRepo.PostComment(name, url, content);
await this.slackService.SendMessage($@"
New comment posted:
=> Name: {name}
=> Url: {url}/commentId/{comment.Id}
=> Content: {content}
");
}
}
Khắc phục
public class CommentCommandService
{
public async Task PostComment(string name, string url, string content)
{
await this.commentRepo.PostComment(name, url, content);
}
public async Task PostToSlack(PostComment comment)
{
await this.slackService.SendMessage(comment);
}
}
Nên sử dụng DTO
DTO (Data transfer object): là các class đóng gói data để chuyển giữa client - server hoặc giữa các service trong microservice. Mục đích tạo ra DTO là để giảm bớt lượng info không cần thiết phải chuyển đi, và cũng tăng cường độ bảo mật.
public class ProductService
{
public Product GetProductDetails(int productId)
{
//...
return product;
}
}
Khắc phục
public class ProductQueryService
{
public ProductDTO GetProductDetails(int productId)
{
//...
return productDto;
}
}
CQS và CRUD: Sự đồng hành trong thiết kế MVC
Trong lập trình hướng đối tượng, Command Query Separation (CQS) là một design pattern thường gặp, tách biệt các thao tác thay đổi dữ liệu (command) khỏi các thao tác truy vấn dữ liệu (query). Mối liên hệ giữa CQS và CRUD (Create, Read, Update, Delete) trong MVC vô cùng chặt chẽ, mang lại nhiều lợi ích cho việc thiết kế ứng dụng.
CRUD theo cách thông thường:
- Create: Tạo một bản ghi mới trong database.
- Read: Truy vấn và lấy dữ liệu từ database.
- Update: Cập nhật thông tin một bản ghi hiện có.
- Delete: Xóa một bản ghi khỏi database.
Phân loại CRUD thành Command và Query:
- CRUD Commands: Create, Update, Delete
- CRUD Queries: READ
Trong kiến trúc MVC truyền thống, các hành động CRUD thường được trộn lẫn trong các controller, action method, và view model. Nếu bạn sử dụng REST API thì sẽ phân chia theo HTTP Method
- HTTP Commands: POST, PUT, DELETE, PATCH
- HTTP Queries: GET
Tương ứng bạn sẽ có các hàm trong Use-Case Design.
Ví dụ:
- Commands: CreatePost, UpdatePost, DeletePost, PostComment, UpdateComment
- Queries: GetPostById, GetAllPosts, GetCommentById, GetAllCommentsForPost
Xây dựng hệ thống sử dụng CQS Pattern
Bạn có thể tham khảo source code ở bài viết: https://www.dotnetcurry.com/patterns-practices/1461/command-query-separation-cqs
Ở bài viết trên, tác giả đã nói khác rõ về cách implement 1 project console application sử dụng CQS như thế nào. Mình bổ sung thêm 1 số ý khác
Đầu tiên, chúng ta sẽ nói về việc chia tách ứng dụng ra thành nhiều component. Nếu ứng dụng của bạn chỉ có vài dòng code như sau thì việc sử dụng CQS sẽ không có nhiều ý nghĩa
//resolve context
var _context = container.Resolve<ApplicationDbContext>();
//save some books if there are none in the database
if (!_context.Books.Any())
{
_context.Books.Add(new Book()
{
Authors = "Andrew Hunt, David Thomas",
Title = "The Pragmatic Programmer",
InMyPossession = true,
DatePublished = new DateTime(1999, 10, 20),
});
_context.Books.Add(new Book()
{
Authors = "Robert C. Martin",
Title = "The Clean Coder: A Code of Conduct for Professional Programmers",
InMyPossession = false,
DatePublished = new DateTime(2011, 05, 13),
});
_context.SaveChanges();
_Log.Info("Books saved..");
}
_Log.Info("Retrieving all books the NON CQS Way..");
foreach (var _book in _context.Books)
{
_Log.InfoFormat("Title: {0}, Authors: {1}, InMyPossession: {2}", _book.Title, _book.Authors, _book.InMyPossession);
}
Bạn nên phân ra làm mô hình 3 lớp chuẩn, ở đây mình phân ra làm 2 lớp để đơn giản hóa vấn đề: Console - Business - Database. Tầng business sẽ chứa các services liên quan tới việc xử lý logic.
Ở tầng Business, bạn sẽ chia ra làm 2 thành phần: Service Interfaces (cung cấp API) và Services (implement).
Tới bước này bạn thực hiện việc thêm Query Dispatchers để thực hiện gọi Query hay execute Command tương ứng.
/// <summary>
/// Dispatches a query and invokes the corresponding handler
/// </summary>
public interface IQueryDispatcher
{
/// <summary>
/// Dispatches a query and retrieves a query result
/// </summary>
/// <typeparam name="TParameter">Request to execute type</typeparam>
/// <typeparam name="TResult">Request Result to get back type</typeparam>
/// <param name="query">Request to execute</param>
/// <returns>Request Result to get back</returns>
TResult Dispatch<TParameter, TResult>(TParameter query)
where TParameter : IQuery
where TResult : IResult;
/// <summary>
/// Dispatches a query and retrieves am async query result
/// </summary>
/// <typeparam name="TParameter">Request to execute type</typeparam>
/// <typeparam name="TResult">Request Result to get back type</typeparam>
/// <param name="query">Request to execute</param>
/// <returns>Request Result to get back</returns>
Task<TResult> DispatchAsync<TParameter, TResult>(TParameter query)
where TParameter : IQuery
where TResult : IResult;
}
/// <summary>
/// Passed around to all allow dispatching a command and to be mocked by unit tests
/// </summary>
public interface ICommandDispatcher
{
/// <summary>
/// Dispatches a command to its handler
/// </summary>
/// <typeparam name="TParameter">Command Type</typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="command">The command to be passed to the handler</param>
TResult Dispatch<TParameter, TResult>(TParameter command) where TParameter : ICommand where TResult : IResult;
/// <summary>
/// Dispatches an async command to its handler
/// </summary>
/// <typeparam name="TParameter">Command Type</typeparam>
/// <typeparam name="TResult"></typeparam>
/// <param name="command">The command to be passed to the handler</param>
Task<TResult> DispatchAsync<TParameter, TResult>(TParameter command) where TParameter : ICommand where TResult : IResult;
}
Implement Dispatcher
public class QueryDispatcher : IQueryDispatcher
{
private readonly IComponentContext _Context;
public QueryDispatcher(IComponentContext context)
{
_Context = context ?? throw new ArgumentNullException(nameof(context));
}
public TResult Dispatch<TParameter, TResult>(TParameter query)
where TParameter : IQuery
where TResult : IResult
{
//Look up the correct QueryHandler in our IoC container and invoke the retrieve method
var _handler = _Context.Resolve<IQueryHandler<TParameter, TResult>>();
return _handler.Retrieve(query);
}
public async Task<TResult> DispatchAsync<TParameter, TResult>(TParameter query)
where TParameter : IQuery
where TResult : IResult
{
//Look up the correct QueryHandler in our IoC container and invoke the retrieve method
var _handler = _Context.Resolve<IQueryHandler<TParameter, TResult>>();
return await _handler.RetrieveAsync(query);
}
}
public class CommandDispatcher : ICommandDispatcher
{
private readonly IComponentContext _context;
public CommandDispatcher(IComponentContext context)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
}
public TResult Dispatch<TParameter, TResult>(TParameter command) where TParameter : ICommand where TResult : IResult
{
//Look up the correct CommandHandler in our IoC container and invoke the Handle method
var _handler = _context.Resolve<ICommandHandler<TParameter, TResult>>();
return _handler.Handle(command);
}
public async Task<TResult> DispatchAsync<TParameter, TResult>(TParameter command) where TParameter : ICommand where TResult : IResult
{
//Look up the correct CommandHandler in our IoC container and invoke the async Handle method
var _handler = _context.Resolve<ICommandHandler<TParameter, TResult>>();
return await _handler.HandleAsync(command);
}
}
Handler
/// <summary>
/// Base interface for command handlers
/// </summary>
/// <typeparam name="TParameter"></typeparam>
/// <typeparam name="TResult"></typeparam>
public interface ICommandHandler<in TParameter, TResult> where TParameter : ICommand
where TResult : IResult
{
/// <summary>
/// Executes a command handler
/// </summary>
/// <param name="command">The command to be used</param>
TResult Handle(TParameter command);
/// <summary>
/// Executes an async command handler
/// </summary>
/// <param name="command">The command to be used</param>
Task<TResult> HandleAsync(TParameter command);
}
/// <summary>
/// Base interface for query handlers
/// </summary>
/// <typeparam name="TParameter">Request type</typeparam>
/// <typeparam name="TResult">Request Result type</typeparam>
public interface IQueryHandler<in TParameter, TResult> where TResult : IResult where TParameter : IQuery
{
/// <summary>
/// Retrieve a query result from a query
/// </summary>
/// <param name="query">Request</param>
/// <returns>Retrieve Request Result</returns>
TResult Retrieve(TParameter query);
/// <summary>
/// Retrieve a query result async from a query
/// </summary>
/// <param name="query">Request</param>
/// <returns>Retrieve Request Result</returns>
Task<TResult> RetrieveAsync(TParameter query);
}
Implement Handler
public abstract class QueryHandler<TParameter, TResult> : IQueryHandler<TParameter, TResult>
where TResult : IResult, new()
where TParameter : IQuery, new()
{
protected readonly ILog Log;
protected ApplicationDbContext ApplicationDbContext;
protected QueryHandler(ApplicationDbContext applicationDbContext)
{
ApplicationDbContext = applicationDbContext;
Log = LogManager.GetLogger(GetType().FullName);
}
public TResult Retrieve(TParameter query)
{
var _stopWatch = new Stopwatch();
_stopWatch.Start();
TResult _queryResult;
try
{
//do authorization and validatiopn
//handle the query request
_queryResult = Handle(query);
}
catch (Exception _exception)
{
Log.ErrorFormat("Error in {0} queryHandler. Message: {1} \n Stacktrace: {2}", typeof(TParameter).Name, _exception.Message, _exception.StackTrace);
//Do more error more logic here
throw;
}
finally
{
_stopWatch.Stop();
Log.DebugFormat("Response for query {0} served (elapsed time: {1} msec)", typeof(TParameter).Name, _stopWatch.ElapsedMilliseconds);
}
return _queryResult;
}
public async Task<TResult> RetrieveAsync(TParameter query)
{
var _stopWatch = new Stopwatch();
_stopWatch.Start();
Task<TResult> _queryResult;
try
{
//do authorization and validatiopn
//handle the query request
_queryResult = HandleAsync(query);
}
catch (Exception _exception)
{
Log.ErrorFormat("Error in {0} queryHandler. Message: {1} \n Stacktrace: {2}", typeof(TParameter).Name, _exception.Message, _exception.StackTrace);
//Do more error more logic here
throw;
}
finally
{
_stopWatch.Stop();
Log.DebugFormat("Response for query {0} served (elapsed time: {1} msec)", typeof(TParameter).Name, _stopWatch.ElapsedMilliseconds);
}
return await _queryResult;
}
/// <summary>
/// The actual Handle method that will be implemented in the sub class
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
protected abstract TResult Handle(TParameter request);
/// <summary>
/// The actual async Handle method that will be implemented in the sub class
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
protected abstract Task<TResult> HandleAsync(TParameter request);
}
public abstract class CommandHandler<TRequest, TResult> : ICommandHandler<TRequest, TResult>
where TRequest : ICommand
where TResult : IResult, new()
{
protected readonly ILog Log;
protected ApplicationDbContext ApplicationDbContext;
protected CommandHandler(ApplicationDbContext context)
{
ApplicationDbContext = context;
Log = LogManager.GetLogger(GetType().FullName);
}
public TResult Handle(TRequest command)
{
var _stopWatch = new Stopwatch();
_stopWatch.Start();
TResult _response;
try
{
//do data validation
//do authorization
_response = DoHandle(command);
}
catch (Exception _exception)
{
Log.ErrorFormat("Error in {0} CommandHandler. Message: {1} \n Stacktrace: {2}", typeof(TRequest).Name, _exception.Message, _exception.StackTrace);
throw;
}
finally
{
_stopWatch.Stop();
Log.DebugFormat("Response for query {0} served (elapsed time: {1} msec)", typeof(TRequest).Name, _stopWatch.ElapsedMilliseconds);
}
return _response;
}
public async Task<TResult> HandleAsync(TRequest command)
{
var _stopWatch = new Stopwatch();
_stopWatch.Start();
Task<TResult> _response;
try
{
//do data validation
//do authorization
_response = DoHandleAsync(command);
}
catch (Exception _exception)
{
Log.ErrorFormat("Error in {0} CommandHandler. Message: {1} \n Stacktrace: {2}", typeof(TRequest).Name, _exception.Message, _exception.StackTrace);
throw;
}
finally
{
_stopWatch.Stop();
Log.DebugFormat("Response for query {0} served (elapsed time: {1} msec)", typeof(TRequest).Name, _stopWatch.ElapsedMilliseconds);
}
return await _response;
}
// Protected methods
protected abstract TResult DoHandle(TRequest request);
protected abstract Task<TResult> DoHandleAsync(TRequest request);
}
Còn đây là Source code từ dotnetcurry: MediafireTham khảo
Command Query Separation (CQS) - A simple but powerful pattern
Command Query Separation | Object-Oriented Design Principles w/ TypeScript
Microservices using MediatR on .Net Core 3.1 with exception handling Triển khai CQRS Pattern với MediatR trong .NET Core
Nhận xét
Đăng nhận xét