+4

07. [Design Pattern] Tìm hiểu về Mediator Design Pattern

Mediator Pattern là gì?

Mediator là một mẫu design pattern thuộc nhóm behavioral.

Mục tiêu của Mediator pattern là giảm sự phụ thuộc giữa các đối tượng trong một hệ thống phức tạp bằng cách tạo ra một đối tượng trung gian (mediator) để chứa logic giao tiếp giữa các đối tượng khác nhau.

Trong Mediator pattern, các đối tượng không giao tiếp trực tiếp với nhau, thay vào đó, chúng giao tiếp thông qua đối tượng mediator. Điều này giúp giảm sự rắc rối và tăng tính linh hoạt của hệ thống, vì mỗi đối tượng chỉ cần biết về đối tượng mediator thay vì biết về tất cả các đối tượng khác trong hệ thống.

Tình huống bài toán

Giả sử chúng ta có một phần mềm, trong đó các dịch vụ được giao tiếp với nhau như sau: image.png

Tại sao phải dùng Mediator?

Phân tích hình trên chúng ta có thể thấy rằng các đối tượng:

  • Phụ thuộc mạnh mẽ: Các đối tượng có thể phụ thuộc quá nhiều vào nhau, dẫn đến sự kết nối chặt chẽ và sự phức tạp trong việc hiểu và duy trì mã nguồn. Các đối tượng có thể cần biết về sự tồn tại và chi tiết của các đối tượng khác, điều này có thể làm tăng sự kết nối giữa chúng và làm cho mã nguồn trở nên phức tạp hơn.

  • Khả năng mở rộng giảm: việc thêm mới các chức năng hoặc thay đổi trong hệ thống có thể gây ra các tác động phụ không mong muốn đối với các đối tượng hiện có.

  • Khả năng tái sử dụng giảm: Các đối tượng không được thiết kế để tái sử dụng dễ dàng trong các ngữ cảnh khác nhau vì chúng có thể phụ thuộc quá nhiều vào các đối tượng khác trong hệ thống.

Mô hình Mediator giúp giảm bớt các vấn đề này bằng cách tạo ra một lớp trung gian để điều phối tất cả các tương tác giữa các đối tượng, giữ cho chúng độc lập và giảm thiểu sự phụ thuộc. Tuy nhiên không giải pháp nào đều hoàn hảo, chúng đều có trade-off riêng. Chúng ta sẽ bàn luận sau. Sau đây sẽ là mô hình áp dụng Mediator đều giải quyết bài toán trên:

image.png

Triển khai bằng code C#

Tiếp theo chúng ta sẽ triển khai mô hình trên với ngôn ngữ lập trình C# như sau: image.png

Đầu tiên chúng ta sẽ tạo một dự án Console App bằng Visual Studio.

Sau đó triển khai những thành phần sau:

  • Tạo interface IRequest<out TResponse>: là một Covariant Interface định nghĩa ràng buộc cho những lớp implement interface này trả về một kiểu dữ liệu dạng TResponse. Lớp này sẽ chứa thông tin cần thiết để gửi cho Mediator xử lý yêu cầu mà bạn.
    public interface IRequest<out TResponse>;
  • Tiếp theo, tạo interface IRequestHandler<in TRequest, TResponse>: là một Contravariant Interface định nghĩa ràng buộc nhận vào một kiểu TRequest đã được định nghĩa ở trên và trả về kiểu TResponse trùng với lớp chúng ta đã định nghĩa theo interface IRequest. Trong interface này sẽ khai báo một phương thức Handle. Phương thức này nhận vào giá trị từ IRequest để xử lý yêu cầu và trả về kết quả của yêu cầu cho lệnh gọi đến Mediator với dữ liệu là TRequest.
    public interface IRequestHandler<in TRequest, TResponse> 
                                     where TRequest : IRequest<TResponse>
    {
        Task<TResponse> Handle(TRequest request);
    }
  • Định nghĩa interface IMediator: IMediator là một đối tượng trung gian, chứa một phương thức Send để gửi yêu cầu lấy dữ liệu đến những lớp tương ứng, nó được khai báo như sau:
    public interface IMediator
    {
        Task<TResponse> Send<TResponse>(IRequest<TResponse> request);
    }

  • Tạo lớp Mediator implement IMediator: lớp này là trung tâm của ví dụ này, phương thức Send ở IMediator sẽ được triển khai (định nghĩa) ở đây. Phương thức Send sẽ lấy những lớp đã được định nghĩa theo IRequestHandler và gọi hàm Handle tương ứng theo từng lớp để trả về kết quả cho yêu cầu:
    public class Mediator(IServiceProvider serviceProvider) : IMediator
    {
        private readonly IServiceProvider _serviceProvider = serviceProvider;

        public async Task<TResponse> Send<TResponse>(IRequest<TResponse> request)
        {
            ArgumentNullException.ThrowIfNull(request);

            var requestType = request.GetType();
            var wrapperType = typeof(RequestHandlerWrapper<,>).MakeGenericType(requestType, typeof(TResponse));             
            var wrapper = (RequestHandlerBase<TResponse>)Activator.CreateInstance(wrapperType);

            return await wrapper.Handle(request, _serviceProvider);
        }
    }

    public class RequestHandlerWrapper<TRequest, TResponse> : RequestHandlerBase<TResponse> where TRequest : IRequest<TResponse>
    {
        public override async Task<TResponse?> Handle(object request, IServiceProvider serviceProvider)
        {
            return await serviceProvider.GetRequiredService<IRequestHandler<TRequest, TResponse>>().Handle((TRequest)request);
        }
    }

    public abstract class RequestHandlerBase<TResponse>
    {
        public abstract Task<TResponse?> Handle(object request, IServiceProvider serviceProvider);
    }
  • Sau khi hoàn thành những bước trên, chúng ta sẽ tạo một lớp để implemt IRequestHandler để xử lý dữ liệu trả về cho yêu cầu, giả sử client gọi đến Mediator mong muốn điều phối đến Service A trả về một dữ liệu dạng string sẽ viết như sau:
public class ServiceA : IRequest<string>
{
    public class ServiceAHandler : IRequestHandler<ServiceA, string>
    {
        // Xử lý một yêu cầu với thông tin yêu cầu là ServiceARequest và nhận một kết quả trả về dạng string
        public async Task<string> Handle(ServiceA request)
        {
            return "Kết quả xử lý sau khi nhận giá trị yêu cầu ServiceARequest";
        }
    }
}

Các Service khác cũng sẽ implement tương tự và xử lý mọi yêu cầu trong phương thức Handle.

  • Vậy là chúng ta đã hoàn thành những lớp cần thiết để thực hành Mediator, chúng ta sẽ quay lại Program.cs và gọi những lớp đã được triển khai ở trên:
var services = new ServiceCollection();
    
// Đăng ký service
services.AddScoped<IMediator, Mediator>();
services.AddScoped<IRequestHandler<ServiceA, string>, ServiceAHandler>();
services.AddScoped<IRequestHandler<ServiceB, string>, ServiceBHandler>();
services.AddScoped<IRequestHandler<ServiceC, string>, ServiceCHandler>();


var serviceProvider = services.BuildServiceProvider();

var mediator = serviceProvider.GetRequiredService<IMediator>();

// Gửi một yêu cầu với thông tin request để Mediator điều phối đến hàm Handle để xử lý
var messageServiceA = await mediator.Send(new ServiceA());
var messageServiceB = await mediator.Send(new ServiceB());
var messageServiceC = await mediator.Send(new ServiceC());

Console.OutputEncoding = System.Text.Encoding.UTF8;
Console.WriteLine($"Gọi đến Mediator với dữ liệu yêu cầu là ServiceA: {messageServiceA}");
Console.WriteLine($"Gọi đến Mediator với dữ liệu yêu cầu là ServiceB: {messageServiceB}");
Console.WriteLine($"Gọi đến Mediator với dữ liệu yêu cầu là ServiceC: {messageServiceC}");

Lưu ý là bạn cần cài package Microsoft.Extensions.DependencyInjection để sử dụng cơ chế DI. Trong đoạn code trên chúng ta đã lấy được MediatorSend một lớp Request để gửi đến hàm HandleRequestHandler xử lý yêu cầu và trả về kết quả.

Việc gửi yêu cầu lấy dữ liệu sẽ không gửi trực tiếp đến từng Service, mà sẽ gửi thông qua Mediator. Đảm bảo kiến trúc của Mediator Pattern. Chúng ta tìm hiểu về những ưu điểm của Mediator trong phát triển phần mềm. Vậy Mediator có những nhược điểm gì?

Nhược điểm của Mediator?

Mặc dù Mediator Pattern có nhiều ưu điểm, như giảm sự phụ thuộc giữa các đối tượng và tăng tính linh hoạt của hệ thống, nhưng cũng có một số bất lợi:

Tăng sự phức tạp: Khi số lượng các đối tượng tăng lên, mediator có thể trở nên phức tạp và khó bảo trì. Điều này đặc biệt đúng nếu mediator cần phải xử lý nhiều loại tương tác khác nhau giữa các đối tượng.

Tính đóng gói thấp: Mặc dù mediator giúp giảm sự phụ thuộc giữa các đối tượng, nhưng nó cũng có thể dẫn đến việc làm giảm tính đóng gói của các đối tượng. Các đối tượng phải "biết" về mediator của chúng, điều này làm mất đi tính đóng gói và làm cho chúng không độc lập.

Hiệu suất: Trong một số trường hợp, việc thông qua mediator có thể tạo ra overhead không cần thiết, đặc biệt là khi có quá nhiều tương tác hoặc khi việc truyền thông qua mediator không cần thiết.

Khó hiểu và phức tạp: Khi một hệ thống sử dụng nhiều mediator hoặc khi các tương tác phức tạp, việc hiểu và debug có thể trở nên khó khăn. Đặc biệt là khi các tương tác không rõ ràng hoặc khi có sự phụ thuộc lớn vào các mediator.

Một số bài toán áp dụng Mediator Pattern

  • Triển khai một ESB — enterprise service bus.
  • Tạo Message brokers để trao đổi dữ liệu giữa các service hay woker...
  • Implement CQRS pattern. ....

Mã nguồn bài viết

Mã nguồi bài viết: Tại đây.

Lời kết

Như vậy chúng ta đã tìm hiểu cơ bản về Mediator Pattern, cách triển khai với ngôn ngữ lập trình C#. Hy vọng bài viết này hữu ích với mọi người.

Nếu mọi người thấy hay có thể đăng ký kênh của mình nhé. Cảm ơn mọi người đã dành thời gian đọc bài viết của mình!!!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí