CQRS là gì?

Khi chúng ta có một ứng dụng tập trung vào dữ liệu (data-centric application) chỉ triển khai các thao tác CRUD (Create, Read, Update, Delete) cơ bản và để lại quy trình nghiệp vụ cho người dùng xử lý (ví dụ như cần thay đổi những dữ liệu nào và theo thứ tự nào), chúng có ưu điểm là người dùng có thể thay đổi quy trình nghiệp vụ mà không cần phải thay đổi ứng dụng.

Nói cách khác, điều này ngụ ý rằng tất cả người dùng cần biết chi tiết các quy trình nghiệp vụ để có thể thực hiện trên ứng dụng, nhưng vấn đề là phần lớn người dùng đều không thể nắm hết được tất cả các quy trình nghiệp vụ mà họ cần để thao tác.

Trong một ứng dụng tập trung vào dữ liệu, ứng dụng không có kiến thức về quy trình nghiệp vụ, vì vậy domain (miền) sẽ không có bất kỳ một hành động nào và không thể làm bất kỳ điều gì khác ngoài việc thay đổi dữ liệu thô.

Nó trở thành một  sự trừu tượng hóa của mô hình dữ liệu . Các quy trình chỉ tồn tại trong đầu của người dùng ứng dụng.

Một ứng dụng thực sự hữu ích khi nó có thể loại bỏ gánh nặng “quy trình” trên vai người dùng bằng cách nắm bắt ý định của họ, biến nó trở thành một ứng dụng có khả năng xử lý các hành vi thay vì chỉ lưu trữ dữ liệu.

CQRS (Command Query Responsibility Separation) là kết quả của sự phát triển của một số khái niệm kỹ thuật hoạt động cùng nhau để giúp cung cấp cho ứng dụng sự phản ánh chính xác về miền, đồng thời khắc phục các hạn chế kỹ thuật phổ biến.

CQS Pattern

Thông thường, chúng ta hay sử dụng kiến trúc sau cho dự án:

Kiến trúc N Layer

Đây là kiến trúc N-layer điển hình như tất cả chúng ta đều biết. Nếu chúng ta muốn áp dụng nguyên tắc Command Query Separation principle (CQS), chúng ta có thể “đơn giản” tách tầng Business Logic ra thành Commands và Queries như sau:

CQS Pattern

Theo Martin Fowler, thuật ngữ "Command Query Separation" được đặt ra bởi Bertrand Meyer trong cuốn sách của ông “Object Oriented Software Construction” (1988) – một cuốn sách được cho là một trong những cuốn sách OO có ảnh hưởng nhất, trong những ngày đầu của OO.

Meyer nói rằng, chúng ta không nên có các phương thức vừa thay đổi dữ liệu vừa trả về dữ liệu. Vì vậy, chúng ta có hai loại phương thức:

  • Queries: Trả về dữ liệu nhưng không thay đổi dữ liệu, do đó không có tác dụng phụ.
  • Commands: Thay đổi dữ liệu và không trả về dữ liệu.

Nói cách khác, đặt một câu hỏi thì không nên làm thay đổi câu trả lời và làm điều gì đó thì không nên trả lại câu trả lời,  điều này cũng giúp tôn trọng nguyên tắc Single Responsibility Principle. Xem thêm ở bài viết "SOLID principle qua hình ảnh":

SOLID principles qua hình ảnh | Comdy
Tìm hiểu SOLID Principles là gì? Giải thích 5 nguyên tắc của SOLID Principles bằng hình ảnh rất dễ hiểu.

Tuy nhiên, có một số mẫu (pattern) là ngoại lệ đối với nguyên tắc này, đó là QueueStack. Khi thực thi phương thức Pop/Dequeue một phần tử của Stack/Queue, phương thức này làm thay đổi Stack/Queue và trả về phần tử bị xóa khỏi Stack/Queue.

Command Pattern

Ý tưởng chính của Command Pattern là đưa chúng ta thoát khỏi ứng dụng tập trung vào dữ liệu và chuyển sang ứng dụng tập trung vào quy trình, người có kiến ​​thức về miền và kiến ​​thức về quy trình ứng dụng.

Trên thực tế, điều này có nghĩa là thay vì yêu cầu người dùng thực hiện hành động “CreateUser”, theo sau là hành động “ActivateUser” và hành động “SendUserCreateEmail”, chúng ta sẽ yêu cầu người dùng chỉ cần thực hiện lệnh “RegisterUser”, lệnh này sẽ thực hiện ba hành động đã đề cập ở trên như một quy trình nghiệp vụ được gói gọn.

Một ví dụ thú vị hơn là khi chúng ta có một form để thay đổi dữ liệu khách hàng. Giả sử rằng form cho phép chúng ta thay đổi tên, địa chỉ, số điện thoại của khách hàng và nếu anh ta là khách hàng ưu tiên. Cũng giả sử rằng một khách hàng chỉ có thể là khách hàng ưu tiên nếu anh ta đã thanh toán các hóa đơn của mình.

Trong ứng dụng CRUD, chúng ta sẽ nhận dữ liệu, kiểm tra xem khách hàng đã thanh toán hóa đơn chưa và chấp nhận hoặc từ chối yêu cầu thay đổi dữ liệu.

Tuy nhiên, ở đây chúng ta có hai quy trình nghiệp vụ khác nhau: Thay đổi tên, địa chỉ và số điện thoại của khách hàng sẽ thành công ngay cả khi khách hàng chưa thanh toán hóa đơn của mình.

Sử dụng Command Pattern, chúng ta có thể phân biệt rõ ràng điều này trong code bằng cách tạo hai lệnh đại diện cho hai quy trình nghiệp vụ khác nhau: một lệnh để thay đổi dữ liệu khách hàng và một lệnh khác để nâng cấp khách hàng lên khách hàng ưu tiên.

Tuy nhiên, bạn nên nhớ rằng điều này không có nghĩa là không thể có một lệnh “CreateUser” đơn giản. Các trường hợp sử dụng CRUD có thể cùng tồn tại một cách hoàn hảo với các trường hợp sử dụng mang mục đích, đại diện cho một quy trình nghiệp vụ phức tạp, nhưng điều quan trọng là không được nhầm lẫn chúng.

Command Pattern là một behavioral pattern trong đó một đối tượng được sử dụng để đại diện và đóng gói tất cả các thông tin cần thiết để gọi một phương thức. Thông tin này bao gồm tên phương thức, các đối tượng của phương thức và các giá trị cho các tham số của phương thức đó.

Ví dụ, tất cả các Command sẽ có cùng một phương thức Execute() để tại một thời điểm nào đó, bất cứ Command nào cũng có thể được thực thi mà không cần biết nó là Command gì. Điều này sẽ cho phép các Command được xếp hàng đợi và được thực thi khi cần thiết, có thể đồng bộ (sync) hoặc bất đồng bộ (async).

Tuy nhiên, vì Command Pattern đóng gói tất cả những gì cần thiết (dữ liệu và logic) để thực hiện một số quy trình nghiệp vụ vào trong phương thức Execute để thực thi. Điều này gây ra một sốvấn đề như: phải tạo các command có chung logic nghiệp vụ, chỉ khác nhau về mặt dữ liệu đầu vào.

Nếu muốn gom các command có chung logic nghiệp vụ lại thành 1 command, thì phải truyền dữ liệu đầu vào cho command này. Nhưng do phương thức Execute trong ICommand interface không có tham số, và nếu có thêm tham số vào thì tham số này không thể sử dụng chung cho các Command khác được.

Command Bus

Để giải quyết hạn chế của Command Pattern đã đề cập ở trên, chúng ta sẽ áp dụng một trong những nguyên tắc lâu đời nhất trong OO là: tách những gì thay đổi khỏi những gì không thay đổi.

Trong trường hợp này những gì thay đổi là dữ liệu và những gì không thay đổi là logic được thực thi trong lệnh để chúng ta có thể tách chúng thành hai class:

  • Một class sẽ là DTO đơn giản để lưu trữ dữ liệu (chúng ta sẽ gọi nó là Command).
  • Một class sẽ chứa logic được thực thi (chúng ta sẽ gọi nó là Handler) sẽ có một phương thức để kích hoạt việc thực thi logic. Ví dụ: void Execute( ICommand command).

Chúng ta cũng sẽ xây dựng Command Invoker thành một thứ có khả năng nhận Command và tìm ra Handler có thể xử lý nó. Chúng tôi sẽ gọi nó là Command Bus.

Hơn nữa, bằng cách thay đổi các mẫu giao diện người dùng một chút, các Command không cần phải được xử lý ngay lập tức, chúng có thể được xếp vào hàng đợi và thực thi không đồng bộ (async). Điều này có một số ưu điểm giúp hệ thống mạnh mẽ hơn:

  • Phản hồi cho người dùng được gửi lại nhanh hơn bởi vì chúng ta không xử lý Command ngay lập tức;
  • Nếu vì lỗi hệ thống, giống như một lỗi hoặc DB đang offline, một Command không thành công, người dùng thậm chí không nhận ra nó. Command này có thể được thực thi lại khi vấn đề được giải quyết.

Ngoài ra, việc sử dụng Command Bus còn mang lại một số lợi điểm như chúng ta có thể áp dụng thêm Aspect-oriented programming (AOP) có thể thêm các logic trước và / hoặc sau khi xử lý được thực hiện.

Ví dụ, chúng ta có thể xác thực dữ liệu của Command trước khi chuyển nó tới Handler, thêm các Handler trong transaction logic (commit, rollback) khi làm việc với DB transaction, hoặc chúng ta có thể làm cho Command Bus hỗ trợ việc truy vấn, phân luồng các logic phức tạp và thực thi bất đồng bộ.

Cách thông thường mà Command Bus đạt được điều này là sử dụng Decorator Pattern bọc xung quanh Command Bus (một Decorator object có thể decorate cho một Decorator object khác), giống như trò matryoshka.

Command Bus

Điều này cho phép chúng ta tạo ra các Decorators riêng của chúng ta và để cấu hình cho Command Bus (có thể bên thứ ba) được tạo ra bởi bất kỳ Decorator nào, bất kể thứ tự nào, thêm chức năng tuỳ chỉnh của chúng ta vào Command Bus.

Nếu chúng ta cần quản lý command theo hàng đợi, chúng ta thêm một Decorator để quản lý hàng đợi (queue) của các Command. Nếu không sử dụng các transaction DB thì chúng ta không cần transaction management Decorator làm gì, …

CQRS Pattern

Bằng cách kết hợp các khái niệm về CQS, Command, Query và CommandBus, cuối cùng chúng ta đã đạt tới Command Query Responsibility Segregation (CQRS).

Về cơ bản, chúng ta có thể nói rằng CQRS là một triển khai của nguyên tắc Command Query Separation principle trong kiến trúc phần mềm. CQRS có thể được thực hiện theo những cách khác nhau và các cấp độ khác nhau, có thể chỉ có Command, hoặc có thể không sử dụng Command Bus. Dưới đây là sơ đồ cho thấy triển khai đầy đủ của CQRS:

CQRS Pattern

Query

Nếu làm theo CQS, phía Query sẽ chỉ trả lại dữ liệu và không làm thay đổi gì cả. Vì chúng ta không có ý định thực hiện quy trình nghiệp vụ trên dữ liệu đó, chúng ta không cần các đối tượng nghiệp vụ (tức là các Entity).

Vì vậy chúng ta không cần sử dụng ORM để truy vấn dữ liệu. Chúng ta chỉ cần truy vấn dữ liệu thô để hiển thị cho người dùng và chính xác dữ liệu mà chúng ta cần hiển thị trong giao diện người dùng.

Phần đọc dữ liệu được thiết kế riêng không lệ thuộc vào các class model của phần ghi dữ liệu. Do đó có thể linh hoạt trong việc truy xuất dữ liệu từ database, cũng như sử dụng micro-ORM khác (ví dụ Dapper) để tối ưu về tốc độ truy xuất.

Thêm một lợi ích về hiệu suất nữa: Khi truy vấn dữ liệu, chúng ta không cần đi qua tầng Business Logic, chúng ta chỉ làm việc với repository và nhận được chính xác những gì chúng ta cần.

Do sự tách biệt này, một cách tối ưu khác có thể là để tách database thành hai database riêng biệt: WRITE DB được tối ưu hóa cho ghi dữ liệu và READ DB để tối ưu hóa cho việc đọc dữ liệu. Ví dụ, nếu chúng ta đang sử dụng một RDBMS:

  • Việc đọc dữ liệu không cần bất kỳ xác thực tính toàn vẹn dữ liệu nào, chúng không cần các ràng buộc khoá ngoại bởi vì việc xác thực tính toàn vẹn dữ liệu được thực hiện khi ghi vào WRITE DB. Vì vậy, chúng ta có thể loại bỏ các ràng buộc toàn vẹn dữ liệu ở READ DB.
  • Chúng ta cũng có thể sử dụng các DB View với chính xác dữ liệu mà chúng ta cần, làm cho việc truy vấn trở nên đơn giản và do đó nhanh hơn, và do đó, tại sao chúng ta cần một RDBMS cho việc đọc dữ liệu? Chúng ta có thể sử dụng MongoDB hoặc Redis, chúng nhanh hơn rất nhiều. Việc thay đổi này có ích nếu ứng dụng đang có vấn đề hiệu suất về việc đọc dữ liệu.

Việc truy vấn có thể được thực hiện bằng cách sử dụng một đối tượng truy vấn trả về một mảng dữ liệu mong muốn hoặc chúng ta có thể sử dụng một cái gì đó tinh vi hơn như Query Bus. Ví dụ, nhận một query name, sử dụng một đối tượng truy vấn để truy vấn dữ liệu và trả về một thể hiện của ViewModel mà query cần.

Command

Như đã giải thích trước đó, bằng cách sử dụng Command, chúng ta chuyển ứng dụng từ thiết kế tập trung vào dữ liệu sang thiết kế hành vi, phù hợp với Thiết kế theo hướng miền (Domain Driven Design - DDD).

Bằng việc loại bỏ các tác vụ READ ra khỏi code xử lý command và domain, có thể giải quyết các vấn đề sau:

  • Domain objects sẽ không cần phải lộ ra (expose) các trạng thái internal của nó
  • Các Repository sẽ chỉ còn rất ít (nếu có) các tác vụ truy vấn dữ liệu.
  • Có thể tập trung vào các hành vi hơn.

Mối quan hệ “một-nhiều” và “nhiều-nhiều” giữa các thực thể có thể có tác động nghiêm trọng đến hiệu suất của ORM. Tin tốt là chúng ta hiếm khi cần những quan hệ đó khi xử lý các Command. Chúng chủ yếu được sử dụng để truy vấn trong Query, mà chúng ta đã tách rời Query và Command, do đó chúng ta có thể loại bỏ những mối quan hệ thực thể đó.

Ở đây tôi không nói về mối quan hệ giữa các bảng trong RDBMS, những ràng buộc khóa ngoại đó sẽ vẫn tồn tại trong Write DB, tôi đang nói về các kết nối giữa các thực thể được cấu hình ở mức ORM.

Giống như Query, nếu Command không được sử dụng cho các truy vấn phức tạp, chúng ta có thể thay thế RDBMS với một cách lưu trữ như document hoặc key-value? Tất nhiên điều đó còn phụ thuộc vào nếu ứng dụng đang có vấn đề hiệu suất về ghi dữ liệu.

Event

Sau khi Command được xử lý, và nếu nó đã được xử lý thành công, trình xử lý sẽ kích hoạt một event thông báo cho phần còn lại của ứng dụng về những gì đã xảy ra. Các event nên được đặt tên theo Command đã kích hoạt nó, và một điều nữa như là tên event sử dụng thì quá khứ, ví dụ ActionPerformed.

CQRS và Event Sourcing

Event Sourcing là một ý tưởng đã được trình bày cùng với CQRS, và thường được xác định là một phần của CQRS.

Ý tưởng về Event Sourcing rất đơn giản: domain của chúng ta đang tạo ra các sự kiện đại diện cho mọi thay đổi được thực hiện trong hệ thống. Nếu chúng ta lấy mọi sự kiện từ đầu của hệ thống và phát lại chúng từ trạng thái ban đầu, chúng ta sẽ nhận được trạng thái hiện tại của hệ thống.

Nó hoạt động tương tự như các giao dịch trên tài khoản ngân hàng của chúng ta. Chúng ta có thể bắt đầu với tài khoản trống, thực hiện lại mọi giao dịch và (hy vọng) nhận được số dư hiện tại. Vì vậy, nếu chúng ta lưu trữ tất cả các sự kiện, chúng ta luôn có thể nhận được trạng thái hiện tại của hệ thống.

CQRS và Event Sourcing

Trong khi Event Sourcing là một phương pháp tuyệt vời để lưu trữ trạng thái của hệ thống thì chúng không nhất thiết cần thiết trong CQRS. Đối với CQRS, điều quan trọng là Domain Model thực sự được lưu giữ như thế nào và đây chỉ là một trong những lựa chọn.

Mô hình READ và WRITE

Ý tưởng về tách biệt mô hình READ và WRITE có vẻ khá rõ ràng và dễ hiểu khi chúng ta đang đọc về CQRS nhưng có vẻ như không rõ ràng trong quá trình thực hiện. Vai trof của mô hình WRITE là gì? Tôi có nên đưa tất cả dữ liệu vào mô hình READ của mình không?

Mô hình READ

Trong nỗ lực đầu tiên của tôi với CQRS, tôi đã sử dụng mô hình WRITE để xây dựng các truy vấn và… nó ổn (hoặc ít nhất là hoạt động). Sau một thời gian, chúng tôi đạt tới điểm trong dự án mà một số truy vấn của chúng tôi đã mất rất nhiều thời gian. Tại sao?

Bởi vì chúng tôi là lập trình viên và tối ưu hóa là một phần cuộc sống của chúng tôi. Chúng tôi đã thiết kế mô hình của mình theo cách thông thường, do đó phía READ của chúng tôi đang gặp phải vấn đề về JOIN.

Chúng tôi buộc phải tính toán trước một số dữ liệu cho các báo cáo để giữ cho nó chạy nhanh chóng. Đó là một điều khá thú vị, bởi vì trên thực tế, chúng tôi đã giới thiệu một bộ nhớ cache.

Và theo quan điểm của tôi, đây là định nghĩa tốt nhất về mô hình READ: nó là một bộ nhớ cache hợp pháp. Bộ nhớ cache ở đó theo thiết kế và không được giới thiệu vì chúng tôi phải phát hành dự án và các yêu cầu phi chức năng không được đáp ứng.

Trong thực tế, mô hình READ có thể rất phức tạp, bạn có thể sử dụng cơ sở dữ liệu đồ thị để lưu trữ các kết nối xã hội và RDBMS để lưu trữ dữ liệu tài chính. Đây là nơi cho phép sử dụng nhiều loại database khác nhau để lưu trữ dữ liệu.

Mô hình WRITE

Tôi thích nghĩ về mô hình WRITE của mình như về trái tim của hệ thống. Đây là Domain Model của tôi, nó xử lý các quy trình nghiệp vụ. Nó đại diện cho trạng thái thực sự của hệ thống, trạng thái có thể được sử dụng để đưa ra các quyết định có giá trị. Mô hình này là nguồn duy nhất của sự thật.

Nếu bạn muốn biết thêm về thiết kế Domain Model, tôi khuyên bạn nên đọc về Thiết kế theo hướng miền (Domain Driven Design).

Tính nhất quán cuối cùng (Event Consistency)

Nếu các mô hình của chúng ta được tách biệt về mặt vật lý, thì đương nhiên là việc đồng bộ hóa sẽ mất một thời gian, nhưng thời gian này bằng cách nào đó rất đáng sợ đối với những người kinh doanh.

Trong các dự án của tôi, nếu mọi bộ phận hoạt động chính xác, thời gian đồng bộ sang mô hình READ thường là không đáng kể. Tuy nhiên, chúng tôi chắc chắn sẽ cần phải tính đến các nguy cơ về thời gian trong quá trình phát triển trong các hệ thống phức tạp hơn. Giao diện người dùng được thiết kế tốt cũng rất hữu ích trong việc xử lý tính nhất quán cuối cùng.

Chúng ta phải giả định rằng ngay cả khi mô hình READ được cập nhật đồng bộ với mô hình WRITE, người dùng vẫn có thể đưa ra quyết định dựa trên dữ liệu cũ. Thật không may, chúng tôi không thể chắc chắn rằng khi dữ liệu được hiển thị cho người dùng (ví dụ: được hiển thị trong trình duyệt web) thì dữ liệu đó vẫn còn mới.

Triển khai CQRS

Việc triển khai CQRS cũng rất đơn giản và không cần đến bất kỳ framwork hỗ trợ nào.

Đầu tiên, chúng ta sẽ định nghĩa các class/interface cho phía Command, bao gồm ICommand, ICommandHandlerICommandDispatcher interface.

public interface ICommand
{
}
 
public interface ICommandHandler
    where TCommand : ICommand
{
    void Execute(TCommand command);
}
 
public interface ICommandDispatcher
{
    void Execute(TCommand command)
        where TCommand : ICommand;
}

Tiếp theo là phía Query side, chúng ta cũng có IQuery, IQueryHandler và IQueryDispatcher.

public interface IQuery
{
}
 
public interface IQueryHandler
    where TQuery : IQuery
{
    TResult Execute(TQuery query);
}
 
public interface IQueryDispatcher
{
    TResult Execute(TQuery query)
        where TQuery : IQuery;
}

Tiếp theo là viết 1 lớp nhằm hiện thực ICommandDispatcher interface, ta gọi đó là CommandDispatcher. CommandDispatcher có một public method là Execute(TCommand command) để thực thi bất cứ Command nào thông qua CommandHandler. CommandHandler được khởi tạo bởi IDependencyResolver đã được truyền vào constructor của ICommandDispatcher với mục đích tìm ra CommandHandler tương ứng để execute 1 Command.

public class CommandDispatcher : ICommandDispatcher
{
    private readonly IDependencyResolver _resolver;
 
    public CommandDispatcher(IDependencyResolver resolver)
    {
        _resolver = resolver;
    }
 
    public void Execute(TCommand command)
        where TCommand : ICommand
    {
        if(command == null)
        {
            throw new ArgumentNullException("command");
        }
 
        var handler = _resolver.Resolve>();
 
        if (handler == null)
        {
            throw new CommandHandlerNotFoundException(typeof(TCommand));
        }
 
        handler.Execute(command);
    }
}

Nữa là việc định nghĩa ra các đối tượng Command và CommandHandler cụ thể cho từng action. Ở đây ta có SignOnCommandSignOnCommandHandler. SignOnCommand là một đối tượng Command như đã đề cập ở trên, chỉ chứa data đơn giản cần thiết cho việc execute command đó. SignOnCommandHandler sẽ nhận vào SignOnCommand và execute action tương ứng với data nhận được.

public class SignOnCommand : ICommand
{
    public AssignmentId Id { get; private set; }
    public LocalDateTime EffectiveDate { get; private set; }
 
    public SignOnCommand(AssignmentId assignmentId, LocalDateTime effectiveDate)
    {
        Id = assignmentId;
        EffectiveDate = effectiveDate;
    }
}
 
public class SignOnCommandHandler : ICommandHandler
{
    private readonly AssignmentRepository _assignmentRepository;
    private readonly SignOnPolicyFactory _factory;
 
    public SignOnCommandHandler(AssignmentRepository assignmentRepository,
                                SignOnPolicyFactory factory)
    {
        _assignmentRepository = assignmentRepository;
        _factory = factory;
    }
 
    public void Execute(SignOnCommand command)
    {
        var assignment = _assignmentRepository.GetById(command.Id);
 
        if (assignment == null)
        {
            throw new MeaningfulDomainException("Assignment not found!");
        }
 
        var policy = _factory.GetPolicy();
 
        assignment.SignOn(command.EffectiveDate, policy);
    }
}

Để execute SignOnCommand này, chúng ta chỉ cần pass nó vào dispatcher như sau:

_commandDispatcher.Execute(new SignOnCommand(new AssignmentId(rawId), effectiveDate));

Việc implement cho phía Query cũng tương tự như vậy, ta có QueryDispatcher.

public class QueryDispatcher : IQueryDispatcher
{
    private readonly IDependencyResolver _resolver;
 
    public QueryDispatcher(IDependencyResolver resolver)
    {
        _resolver = resolver;
    }
 
    public TResult Execute(TQuery query)
        where TQuery : IQuery
    {
        if (query == null)
        {
            throw new ArgumentNullException("query");
        }
 
        var handler = _resolver.Resolve>();
 
        if (handler == null)
        {
            throw new QueryHandlerNotFoundException(typeof(TQuery));
        }
 
        return handler.Execute(query);
    }
}

Như tôi đã nói, việc triển khai này rất dễ dàng mở rộng. Ví dụ: chúng ta có thể handle các transactions cho command dispatcher mà không thay đổi việc thực hiện ban đầu bằng cách sử dụng các Decorator:

public class TransactionalCommandDispatcher : ICommandDispatcher
{
    private readonly ICommandDispatcher _next;
    private readonly ISessionFactory _sessionFactory;
 
    public TransactionalCommandDispatcher(ICommandDispatcher next,
            ISessionFactory sessionFactory)
    {
        _next = next;
        _sessionFactory = sessionFactory;
    }
 
    public void Execute(TCommand command)
        where TCommand : ICommand
    {
        using (var session = _sessionFactory.GetSession())
            using (var tx = session.BeginTransaction())
            {
                try
                {
                    _next.Execute(command);
                    tx.Commit();
                }
                catch
                {
                    tx.Rollback();
                    throw;
                }
            }
    }
}

Kết Luận

Bằng cách sử dụng CQRS, chúng ta có thể tách rời hoàn toàn mô hình READWRITE, cho phép tối ưu hóa các thao tác đọc và ghi. Điều này làm tăng hiệu suất, làm cho code rõ ràng, đơn giản, phản ánh được domain, tăng tính bảo trì.

Một lần nữa, đó là tất cả về tính đóng gói (encapsulation), low coupling, high cohesion, và nguyên tắc Single Responsibility Principle.

Tuy nhiên, cần lưu ý rằng mặc dù CQRS cung cấp một phong cách thiết kế và một số giải pháp kỹ thuật có thể làm cho một ứng dụng rất mạnh mẽ, điều đó không có nghĩa là tất cả các ứng dụng phải được xây dựng theo cách này: Chúng ta nên sử dụng những gì chúng ta cần, và khi nào cần.

Bài viết được tham khảo từ:

Design PatternCQRS PatternLập Trình C#PrinciplesSOLID Principle
Bài Viết Liên Quan:
Các mẫu design pattern hiện đại
Trung Nguyen 27/11/2020
Các mẫu design pattern hiện đại

Một số mẫu design pattern có thể giúp bạn đạt được khả năng mở rộng, tính khả dụng, bảo mật, độ tin cậy và khả năng phục hồi cho ứng dụng.