+2

08. [Design Pattern] Tìm hiểu về Decorator Design Pattern trong .NET

Decorator Design Pattern là gì?

Decorator pattern là một mẫu thiết kế thuộc nhóm cấu trúc (structural pattern) trong lập trình hướng đối tượng. Mẫu thiết kế này cho phép bạn thêm các chức năng (functionality) mới vào một đối tượng mà không làm thay đổi cấu trúc của nó. Điều này đảm bảo rằng bạn có thể mở rộng chức năng của đối tượng mà không cần thay đổi mã hiện tại đã được triển khai.

Pattern này là một cách tuyệt vời để tuân theo nguyên tắc Single Responsibility Principle, vì bạn có thể tách rời các vấn đề bên ngoài chức năng cốt lõi (cross-cutting concerns) như Logging, Validation, kiểm tra Authorization..., có thể được tách ra một lớp decorator với mục đích duy nhất của nó. Ngoài ra, nó cũng tuân theo nguyên tắc Open-Closed Principle, bởi vì bạn có thể mở rộng chức năng cho lớp đã tồn tại mà không phải sửa đổi chúng thông qua Decorator.

Khi nào sử dụng Decorator Design Pattern?

Decorator design pattern là một công cụ linh hoạt trong phát triển phần mềm, cho phép mở rộng đối tượng một cách linh hoạt mà không làm thay đổi cấu trúc của nó. Dưới đây là một số tình huống hữu ích để sử dụng nó:

  • Thay thế linh hoạt cho kết thừa: khi một lớp không cho phép kế thừa, hoặc giải pháp kế thừa quá cứng nhắc hoặc không phù hợp cho việc thêm chức năng mới, hoặc có thể gây ra vấn đề Subclass Explosion (quá nhiều các lớp con được tạo ra dẫn đến không thể kiểm soát), thì khi đó Decorator pattern là một lựa chọn linh hoạt để giải quyết cho phép kết hợp các behaviors để giải quyết bài toán.

  • Mở rộng mà không muốn sửa đổi đối tượng gốc: trong trường hợp bạn nhận được yêu cầu mới phải sửa đổi Legacy Code thì việc áp dụng Decorator là một lựa chọn tốt để thi công thêm mà không ảnh hưởng đến code cũ.

  • Bổ sung thêm các Behavior : trong kịch bản bạn muốn thêm Logging, Validation dữ liệu, kiểm tra Authorization , ghi Auditing Log hoặc Caching của từng phương thức thì Decorator là một lựa chọn tốt để mở rộng đối tượng mà vẫn đảm bảo tuân theo nguyên tắc SOLID.

  • Module hóa và tái sử dụng: từ các Decorator bạn có thể kết hợp với Generic Type để module hóa và tái sử dụng cho nhiều đối tượng khác nhau.

Triển khai trong ứng dụng .NET

Bài toán

Bây giờ, chúng ta sẽ đi đến phần thực hành triển khai Decorator Pattern bằng một ứng dụng .NET Core.

Giả sử chúng ta sẽ viết một API đơn giản là tạo một sản phẩm có những thông tin tên, mô tả, giá... Từ đó ta sẽ có một Domain Model là Product, một Repository là ProductRepository và một ApplicationService là ProductService, lớp này là đối tượng sẽ được wrap, nó định nghĩa behavior cơ bản, và có thể thay đổi bằng decorator:

image.png

Bây giờ bài toán đặt ra là nếu chúng ta muốn Validation dữ liệu Product đầu vào với những giá trị bắt buộc phải nhập, và ghi thông tin Logging khi vào phương thức AddProduct và thông tin sau khi thêm Product thành công.

Một cách làm sai lầm phổ biến là sẽ thực hiện trực tiếp trong phương thức AddProduct của ProductService, dẫn đến nó vi phạm nguyên tắc Single Responsibility Principle bởi vì cốt lõi của phương thức là thêm sản phẩm, việc ValidationLogging là vấn đề cross-cutting concerns, việc thêm logic vào sẽ phá vở nguyên tắc đơn nhiệm, hơn nữa việc sửa đổi code phương thức AddProduct cũng dẫn đến vi phạm nguyên tắc Open-Closed Principle.

Giải pháp ở đây là chúng ta sẽ tạo ra hai lớp Decorator được implement IProductService và nhận IProductService làm tham số khởi tạo để nó có thể chứa được cả lớp được mở rộng và các Decorator khác:

image.png

Để mở rộng một đối tượng, một lớp decorator như LoggingProductServiceDecoratorValidationProductServiceDecorator sẽ wrap (bao bọc) một đối tượng implement IProductService hiện có và cung cấp chức năng bổ sung bằng cách thực hiện các phương thức cần mở rộng. Ở đây, lớp ValidationProductServiceDecorator sẽ wrap lớp cơ bản ProductService và lớp LoggingProductServiceDecorator sẽ wrap lớp ValidationProductServiceDecorator:

image.png

Triển khai bằng code

Mã nguồn demo

Đầu tiên ta có một domain model là Product:

public class Product
{
    [Key]
    public int ProductId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
    public int StockQuantity { get; set; }
}

Định nghĩa IProductService và implement một lớp cơ bản ProductService:

public interface IProductService
{
    Task<int> AddProduct(Product product);
}

public class ProductService(IProductRepository productRepository) : IProductService
{
    public async Task<int> AddProduct(Product product)
    {
        var productEntity = productRepository.Add(product);
        await productRepository.UnitOfWork.SaveChangesAsync();
        return productEntity.ProductId;
    }
}

Tiếp theo, định nghĩa một Validation decorator là lớp ValidationProductServiceDecorator để đảm bảo dữ liệu Product đầu vào hợp lệ, việc validation tôi sẽ dùng thư viện FluentValidation:

  • Định nghĩa rule validation cho đối tượng Product:
public class ProductValidator : AbstractValidator<Product>
{
    public ProductValidator()
    {
        RuleFor(p => p.Name)
            .NotNull()
            .NotEmpty();

        RuleFor(p => p.Price)
            .NotNull();

        RuleFor(p => p.StockQuantity)
            .NotNull();
    }
}
  • Lớp ValidationProductServiceDecorator sẽ như sau:
public class ValidationProductServiceDecorator(
    IProductService decoratedProductService,
    IEnumerable<IValidator<Product>> validators) : IProductService
{
    public async Task<int> AddProduct(Product product)
    {
         var context = new ValidationContext<Product>(product);

        var failures = validators
                      .Select(v => v.Validate(context))
                      .SelectMany(result => result.Errors)
                      .Where(f => f != null)
                      .ToList();

        if (failures.Any())
        {
            throw new ValidationException(failures);
        }

        var result = await decoratedProductService.AddProduct(product);
        return result;
    }
}

Định nghĩa lớp LoggingProductServiceDecorator:

public class LoggingProductServiceDecorator(IProductService decoratedProductService, 
                                            ILogger<LoggingProductServiceDecorator> logger) : IProductService
{
    public async Task<int> AddProduct(Product product)
    {
        logger.LogInformation($"Start insert {product.Name} with info {product.Description}", product);

        var productId = await decoratedProductService.AddProduct(product);

        logger.LogInformation($"Insert {product.Name} success!", product);

        return productId;
    }
}

Tiếp theo là đăng ký với DI container, dựa vào nguyên lý Listkov (Liskov Substitution Principle - LSP) có thể dễ dàng tạo sự Wrap giữa Decorator và lớp cơ bản ProductService:

services.AddScoped<IProductRepository, ProductRepository>(); 
services.AddSingleton<IValidator<Product>, ProductValidator>();

services.AddScoped<ProductService>();

services.AddScoped<IProductService>(provider => {
    var baseService = provider.GetRequiredService<ProductService>(); 
    var validators = provider.GetRequiredService<IEnumerable<IValidator<Product>>>(); 
    
    var validationDecorator = new ValidationBookServiceDecorator(baseService, validators);

   var logger = provider.GetRequiredService<ILogger<LoggingProductServiceDecorator>>();
    return new LoggingBookServiceDecorator(validationDecorator);
});

Mã nguồn bài viết bạn có thể lấy tại đây

Sau khi triển khai xong decorator vào chức năng thêm Product, ta sẽ có kết quả như sau:

  • Khi dữ liệu Product không hợp lệ khi không truyền trường Name lên: image.png

  • Khi dữ liệu Product hợp lệ và ghi logging: image.png

Kết luận

Mẫu thiết kế Decorator là một mẫu thiết kế cho phép mở rộng hoặc thay đổi hành vi của đối tượng một cách linh hoạt, mà không làm thay đổi cấu trúc của các đối tượng khác cùng lớp. Nó cho phép bạn wrap đối tượng trong một hoặc nhiều lớp decorator để thay đổi hoặc mở rộng hành vi của đối tượng đó vào thời điểm chạy. Nó đặc biệt hữu ích khi bạn cần thêm các vấn đề bên ngoài (cross-cutting concerns) như logging, validation, caching vào một đối tượng mà không muốn thay đổi mã nguồn hiện tại của đối tượng đó. Việc thêm tính năng này sẽ tuân thủ chặt chẽ theo nguyên tắc SOLID.

Tham khảo

Decorator Design Pattern


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í