0
0
Lập trình
Sơn Tùng Lê
Sơn Tùng Lê103931498422911686980

Giải Quyết Vấn Đề Nổ Phụ Thuộc Trong C#

Đăng vào 1 ngày trước

• 7 phút đọc

Giới Thiệu

Trong phát triển phần mềm, việc quản lý phụ thuộc là một thách thức lớn, đặc biệt là khi số lượng phụ thuộc trong một class tăng lên. Trong bài viết này, chúng ta sẽ khám phá khái niệm "nổ phụ thuộc" và cách giải quyết vấn đề này trong dịch vụ C#. Chúng ta sẽ sử dụng ví dụ cụ thể để minh họa và đưa ra các thực tiễn tốt nhất để tránh tình trạng này.

Vấn Đề Nổ Phụ Thuộc

Khi một dịch vụ trong C# có quá nhiều phụ thuộc được tiêm vào thông qua constructor, chúng ta sẽ gặp phải tình trạng gọi là "nổ phụ thuộc" (constructor dependency explosion). Điều này không chỉ làm cho code trở nên khó hiểu mà còn làm khó khăn trong việc bảo trì và mở rộng sau này.

Ví Dụ Về Nổ Phụ Thuộc

Xem xét đoạn code sau:

csharp Copy
public class PedidoService
{
    private readonly IClienteRepository _clienteRepository;
    private readonly IPedidoRepository _pedidoRepository;
    private readonly IEstoqueService _estoqueService;
    private readonly IPagamentoService _pagamentoService;
    private readonly IEmailService _emailService;
    private readonly INotificacaoService _notificacaoService;
    private readonly ILogger _logger;
    private readonly IFreteService _freteService;
    private readonly IMapper _mapper;
    private readonly IAuditoriaService _auditoriaService;

    public PedidoService(
        IClienteRepository clienteRepository,
        IPedidoRepository pedidoRepository,
        IEstoqueService estoqueService,
        IPagamentoService pagamentoService,
        IEmailService emailService,
        INotificacaoService notificacaoService,
        ILogger logger,
        IFreteService freteService,
        IMapper mapper,
        IAuditoriaService auditoriaService)
    {
        _clienteRepository = clienteRepository;
        _pedidoRepository = pedidoRepository;
        _estoqueService = estoqueService;
        _pagamentoService = pagamentoService;
        _emailService = emailService;
        _notificacaoService = notificacaoService;
        _logger = logger;
        _freteService = freteService;
        _mapper = mapper;
        _auditoriaService = auditoriaService;
    }

    public void ProcessarPedido(PedidoDto dto)
    {
        var cliente = _clienteRepository.ObterPorId(dto.ClienteId);

        if (cliente == null)
        {
            _logger.Log("Cliente không tìm thấy");
            return;
        }

        var pedido = _mapper.Map<Pedido>(dto);

        _estoqueService.ReservarItens(pedido.Itens);

        var valorFrete = _freteService.CalcularFrete(dto.EnderecoEntrega);
        pedido.AdicionarFrete(valorFrete);

        var pagamentoOk = _pagamentoService.Processar(pedido);

        if (!pagamentoOk)
        {
            _notificacaoService.Enviar("Lỗi trong quá trình thanh toán");
            return;
        }

        _pedidoRepository.Salvar(pedido);

        _emailService.EnviarConfirmacao(cliente.Email);
        _auditoriaService.Registrar("Đơn hàng được tạo", pedido.Id.ToString());
    }
}

Đoạn code trên cho thấy một dịch vụ với nhiều phụ thuộc, điều này có thể dẫn đến khó khăn trong việc bảo trì và kiểm thử.

Các Giải Pháp Thông Thường

Một trong những giải pháp có thể nghĩ đến là sử dụng sự kiện và để mỗi handler tự giải quyết một phần của quy trình, khiến cho PedidoService chỉ lo về việc lưu trữ đơn hàng. Tuy nhiên, nếu xảy ra một ngoại lệ trong quá trình này, sẽ có những vấn đề nhất định mà chúng ta cần giải quyết để đảm bảo tính toàn vẹn của ứng dụng.

Vấn Đề Từ Giải Pháp Đơn Giản

Nếu có một ngoại lệ xảy ra trong dịch vụ tính phí vận chuyển, điều này có thể dẫn đến việc hàng vẫn còn bị giữ lại trong kho mà không có đơn hàng nào được tạo ra, gây ra sự không nhất quán trong trạng thái của ứng dụng.

Giải Pháp Tối Ưu

Thay vì để các bước trong quy trình lẫn lộn, chúng ta có thể sử dụng một cơ chế giao dịch chuỗi (chain transaction mechanism) để đảm bảo rằng nếu một bước thất bại, tất cả các bước sẽ bị rollback. Điều này sẽ giúp chúng ta duy trì được tính toàn vẹn của dữ liệu trong ứng dụng.

Ví Dụ Về Cơ Chế Giao Dịch Chuỗi

csharp Copy
public interface ITransactionStep<TInput> where TInput : notnull
{
    Result<Error> Execute(ref TInput input);
    Result<Error> Rollback(Result<Error> error);
}

public abstract class TransactionStepBase<TInput>(ITransactionStep<TInput>? next) : ITransactionStep<TInput> where TInput : notnull
{
    public Result<Error> Execute(ref TInput input)
    {
        var result = ExecuteInternal(ref input);
        if(result && next is not null)
            return next.Execute(ref input);

        if(result && next is null)
            return result;        

        return Rollback(result);
    }

    protected abstract Result<Error> ExecuteInternal(ref TInput input);
    public abstract Result<Error> Rollback(Result<Error> error);
}

Trong đoạn code trên, chúng ta định nghĩa một interface cho các bước giao dịch và một lớp trừu tượng làm cơ sở cho từng bước cụ thể.

Thực Hiện Một Bước Kiểm Tra Khách Hàng

csharp Copy
public sealed class CustomerValidationStep(CustomerDataAccess customerDataAccess,
                                           OrderStockReservationStep next) : TransactionStepBase<OrderSubmissionBag>(next)
{
    protected override Result<Error> ExecuteInternal(ref OrderSubmissionBag input)
    {
        try
        {
            var customer = customerDataAccess.GetById(input.Request.CustomerId);
            if (customer)
            {
                input.Customer = customer.Get();
                return Result<Error>.Ok();
            }

            return Result<Error>.Error(new Error($"Khách hàng với id {input.Request.CustomerId} không được tìm thấy"));
        }
        catch(Exception ex)
        {
            return Rollback(new Error(ex.Message));
        }
    }

    public override Result<Error> Rollback (Result<Error> error) =>
        error;
}

Trong ví dụ này, bước kiểm tra khách hàng có thể xử lý bất kỳ lỗi nào xảy ra và thực hiện rollback nếu cần thiết.

Kết Luận

Tránh nổ phụ thuộc không chỉ giúp mã nguồn trở nên rõ ràng hơn mà còn giúp duy trì tính dễ đọc và dễ kiểm thử. Với giải pháp mà chúng tôi đã đề xuất, mỗi bước của quy trình là tự chứa và không phụ thuộc vào các bước khác, giúp chúng ta xóa bỏ tình trạng nổ phụ thuộc và duy trì tính nhất quán của trạng thái ứng dụng.

Hy vọng bạn đã tìm thấy thông tin trong bài viết này hữu ích. Hãy cho tôi biết cảm nhận của bạn hoặc đặt câu hỏi qua các kênh liên lạc của tôi!

Câu Hỏi Thường Gặp (FAQ)

1. Nổ phụ thuộc là gì?
Nổ phụ thuộc là tình trạng khi một class trong C# có quá nhiều phụ thuộc được inject, gây khó khăn trong việc duy trì và mở rộng mã nguồn.

2. Tại sao cần tránh nổ phụ thuộc?
Tránh nổ phụ thuộc giúp mã nguồn dễ đọc, dễ bảo trì và dễ kiểm thử hơn.

3. Làm thế nào để giải quyết vấn đề này?
Bạn có thể sử dụng cơ chế giao dịch chuỗi để đảm bảo tính toàn vẹn của dữ liệu và giảm thiểu số lượng phụ thuộc trong constructor.

Hy vọng bài viết đã cung cấp cho bạn cái nhìn sâu sắc về cách quản lý phụ thuộc trong C#. Hãy tiếp tục theo dõi để có thêm nhiều bài viết hữu ích khác!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào