Tôi có thể sử dụng Dependency Injection mà không phá vỡ Encapsulation không?


15

Đây là Giải pháp và dự án của tôi:

  • BookStore (giải pháp)
    • BookStore.Coupler (dự án)
      • Bootstrapper.cs
    • BookStore.Domain (dự án)
      • Tạo BookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infr Hạ tầng (dự án)
      • Tạo BookCommandHandler.cs
      • Xác nhậnCommandHandlerDecorator.cs
    • BookStore.Web (dự án)
      • Toàn cầu
    • BookStore.BatchProcesses (dự án)
      • Chương trình.cs

Bootstrapper.cs :

public static class Bootstrapper.cs 
{
    // I'm using SimpleInjector as my DI Container
    public static void Initialize(Container container) 
    {
        container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>), typeof(CreateBookCommandHandler).Assembly);
        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>));
        container.RegisterManyForOpenGeneric(typeof(IValidate<>),
            AccessibilityOption.PublicTypesOnly,
            (serviceType, implTypes) => container.RegisterAll(serviceType, implTypes),
            typeof(IValidate<>).Assembly);
        container.RegisterSingleOpenGeneric(typeof(IValidator<>), typeof(CompositeValidator<>));
    }
}

Tạo BookCommandValidator.cs

public class CreateBookCommandValidator : IValidate<CreateBookCommand>
{
    public IEnumerable<IValidationResult> Validate(CreateBookCommand book)
    {
        if (book.Author == "Evan")
        {
            yield return new ValidationResult<CreateBookCommand>("Evan cannot be the Author!", p => p.Author);
        }
        if (book.Price < 0)
        {
            yield return new ValidationResult<CreateBookCommand>("The price can not be less than zero", p => p.Price);
        }
    }
}

CompositeValidator.cs

public class CompositeValidator<T> : IValidator<T>
{
    private readonly IEnumerable<IValidate<T>> validators;

    public CompositeValidator(IEnumerable<IValidate<T>> validators)
    {
        this.validators = validators;
    }

    public IEnumerable<IValidationResult> Validate(T instance)
    {
        var allResults = new List<IValidationResult>();

        foreach (var validator in this.validators)
        {
            var results = validator.Validate(instance);
            allResults.AddRange(results);
        }
        return allResults;
    }
}

IValidate.cs

public interface IValidate<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

IValidator.cs

public interface IValidator<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

ICommandHandler.cs

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

Tạo BookCommandHandler.cs

public class CreateBookCommandHandler : ICommandHandler<CreateBookCommand>
{
    private readonly IBookStore _bookStore;

    public CreateBookCommandHandler(IBookStore bookStore)
    {
        _bookStore = bookStore;
    }

    public void Handle(CreateBookCommand command)
    {
        var book = new Book { Author = command.Author, Name = command.Name, Price = command.Price };
        _bookStore.SaveBook(book);
    }
}

Xác nhậnCommandHandlerDecorator.cs

public class ValidationCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;
    private readonly IValidator<TCommand> validator;

    public ValidationCommandHandlerDecorator(ICommandHandler<TCommand> decorated, IValidator<TCommand> validator)
    {
        this.decorated = decorated;
        this.validator = validator;
    }

    public void Handle(TCommand command)
    {
        var results = validator.Validate(command);

        if (!results.IsValid())
        {
            throw new ValidationException(results);
        }

        decorated.Handle(command);
    }
}

Toàn cầu

// inside App_Start()
var container = new Container();
Bootstrapper.Initialize(container);
// more MVC specific bootstrapping to the container. Like wiring up controllers, filters, etc..

Chương trình.cs

// Pretty much the same as the Global.asax

Xin lỗi vì thiết lập lâu cho vấn đề, tôi không có cách nào tốt hơn để giải thích vấn đề này ngoài việc nêu chi tiết vấn đề thực sự của tôi.

Tôi không muốn tạo CreatBookCommandValidator của mình public. Tôi thà như vậy internalnhưng nếu tôi làm được internalthì tôi sẽ không thể đăng ký với DI Container của mình. Lý do tôi muốn nó là nội bộ là vì dự án duy nhất nên có khái niệm về các triển khai IValidate <> của tôi nằm trong dự án BookStore.Domain. Bất kỳ dự án nào khác chỉ cần tiêu thụ IValidator <> và CompositeValidator phải được giải quyết sẽ đáp ứng tất cả các xác nhận.

Làm cách nào tôi có thể sử dụng Dependency Injection mà không phá vỡ đóng gói? Hay tôi đi sai về điều này?


Chỉ là một nitpick: Những gì bạn đang sử dụng không phải là một mẫu lệnh chính xác, vì vậy gọi nó là lệnh có thể là thông tin sai. Ngoài ra, CreatBookCommandHandler có vẻ như đang phá vỡ LSP: điều gì sẽ xảy ra, nếu bạn vượt qua đối tượng, điều đó xuất phát từ CreatBookCommand? Và tôi nghĩ rằng những gì bạn đang làm ở đây thực sự là antipotype Mô hình miền thiếu máu. Những thứ như tiết kiệm nên ở trong miền và xác nhận phải là một phần của thực thể.
Euphoric

1
@Euphoric: Đúng vậy. Đây không phải là mẫu lệnh . Thực tế, OP tuân theo một mẫu khác: mẫu lệnh / trình xử lý .
Steven

Có rất nhiều câu trả lời hay, tôi ước mình có thể đánh dấu nhiều hơn là câu trả lời. Cảm ơn mọi người đã giúp đỡ.
Evan Larsen

@Euphoric, sau khi suy nghĩ lại bố cục dự án tôi nghĩ CommandHandlers nên ở trong Miền. Không chắc chắn tại sao tôi đưa chúng vào dự án Cơ sở hạ tầng. Cảm ơn.
Evan Larsen

Câu trả lời:


11

Làm cho CreateBookCommandValidatorcông chúng không vi phạm đóng gói, kể từ khi

Đóng gói được sử dụng để ẩn các giá trị hoặc trạng thái của một đối tượng dữ liệu có cấu trúc bên trong một lớp, ngăn chặn các bên trái phép truy cập trực tiếp vào chúng ( wikipedia )

Bạn CreateBookCommandValidatorkhông cho phép truy cập vào các thành viên dữ liệu của mình (hiện tại dường như không có bất kỳ) vì vậy nó không vi phạm đóng gói.

Công khai lớp này không vi phạm bất kỳ nguyên tắc nào khác (chẳng hạn như các nguyên tắc RẮN ), bởi vì:

  • Lớp đó có một trách nhiệm được xác định rõ và do đó tuân theo Nguyên tắc Trách nhiệm duy nhất.
  • Thêm trình xác nhận mới vào hệ thống có thể được thực hiện mà không cần thay đổi một dòng mã và do đó bạn tuân theo Nguyên tắc Mở / Đóng.
  • Giao diện IValidator <T> mà lớp này triển khai là hẹp (chỉ có một thành viên) và tuân theo Nguyên tắc phân chia giao diện.
  • Người tiêu dùng của bạn chỉ phụ thuộc vào giao diện IValidator <T> đó và do đó tuân theo Nguyên tắc đảo ngược phụ thuộc.

Bạn chỉ có thể tạo CreateBookCommandValidatornội bộ nếu lớp không được tiêu thụ trực tiếp từ bên ngoài thư viện, nhưng điều này hầu như không xảy ra, vì các bài kiểm tra đơn vị của bạn là người tiêu dùng quan trọng của lớp này (và hầu hết mọi lớp trong hệ thống của bạn).

Mặc dù bạn có thể đặt lớp bên trong và sử dụng [InternalsVisibleTo] để cho phép dự án thử nghiệm đơn vị truy cập vào bên trong dự án của bạn, tại sao phải bận tâm?

Lý do quan trọng nhất để khiến các lớp bên trong là để ngăn các bên ngoài (mà bạn không có quyền kiểm soát) phải phụ thuộc vào lớp đó, bởi vì điều đó sẽ ngăn bạn thay đổi tương lai sang lớp đó mà không phá vỡ bất cứ điều gì. Nói cách khác, điều này chỉ giữ khi bạn đang tạo một thư viện có thể sử dụng lại (chẳng hạn như thư viện tiêm phụ thuộc). Như một vấn đề thực tế, Simple In phun chứa nội dung bên trong và dự án thử nghiệm đơn vị của nó kiểm tra các phần bên trong này.

Tuy nhiên, nếu bạn không tạo dự án có thể sử dụng lại, vấn đề này không tồn tại. Nó không tồn tại, bởi vì bạn có thể thay đổi các dự án phụ thuộc vào nó và các nhà phát triển khác trong nhóm của bạn sẽ phải tuân theo hướng dẫn của bạn. Và một hướng dẫn đơn giản sẽ làm: Lập trình cho một sự trừu tượng; không phải là một thực hiện (Nguyên tắc đảo ngược phụ thuộc).

Câu chuyện dài quá ngắn, đừng biến lớp này thành nội bộ trừ khi bạn đang viết một thư viện có thể sử dụng lại.

Nhưng nếu bạn vẫn muốn biến lớp này thành nội bộ, bạn vẫn có thể đăng ký nó với Simple Injection mà không gặp vấn đề nào như thế này:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    AccessibilityOption.AllTypes,
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

Điều duy nhất để đảm bảo là tất cả các trình xác nhận của bạn có một hàm tạo công khai, mặc dù chúng là nội bộ. Nếu bạn thực sự muốn các kiểu của mình có một hàm tạo bên trong (không thực sự biết lý do tại sao bạn muốn điều đó), bạn có thể ghi đè lên Hành vi giải quyết của Trình xây dựng .

CẬP NHẬT

Kể từ Simple In phun v2.6 , hành vi mặc định của RegisterManyForOpenGenericlà đăng ký cả loại công khai và loại nội bộ. Vì vậy, việc cung cấp AccessibilityOption.AllTypeshiện đang dư thừa và tuyên bố sau đây sẽ đăng ký cả loại công khai và nội bộ:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

8

Nó không phải là một vấn đề lớn mà CreateBookCommandValidatorlớp học là công khai.

Nếu bạn cần tạo các thể hiện của nó bên ngoài thư viện định nghĩa nó, thì đó là một cách tiếp cận khá tự nhiên để hiển thị lớp công khai và chỉ dựa vào các máy khách sử dụng lớp đó như là một triển khai IValidate<CreateBookCommand>. (Đơn giản chỉ cần phơi bày một loại không có nghĩa là đóng gói bị hỏng, nó chỉ giúp khách hàng dễ dàng phá vỡ đóng gói hơn một chút).

Mặt khác, nếu bạn thực sự muốn buộc khách hàng không biết về lớp, bạn cũng có thể sử dụng phương thức nhà máy tĩnh công khai thay vì để lộ lớp, ví dụ:

public static class Validators
{
    public static IValidate<CreateBookCommand> NewCreateBookCommandValidator()
    {
        return new CreateBookCommnadValidator();
    }
}

Đối với việc đăng ký trong container DI của bạn, tất cả các container DI mà tôi biết cho phép xây dựng bằng cách sử dụng phương pháp tĩnh nhà máy.


Vâng, cảm ơn bạn .. Ban đầu tôi cũng nghĩ giống như vậy trước khi tôi tạo bài đăng này. Tôi đã suy nghĩ về việc tạo ra một lớp Factory sẽ trả lại triển khai IValidate <> thích hợp nhưng nếu bất kỳ triển khai IValidate <> nào có bất kỳ sự phụ thuộc nào, nó có thể sẽ nhanh chóng có lông.
Evan Larsen

@EvanLarsen Tại sao? Nếu việc IValidate<>thực hiện có các phụ thuộc, thì đặt các phụ thuộc này làm tham số cho phương thức xuất xưởng.
jhominal

5

Bạn có thể khai báo CreateBookCommandValidatordưới internaldạng áp dụng InternalsVisibleToAttribution để hiển thị cho BookStore.Couplerhội đồng. Điều này cũng thường giúp khi thực hiện các bài kiểm tra đơn vị.


Tôi không có ý tưởng về sự tồn tại của thuộc tính đó. Cảm ơn bạn
Evan Larsen

4

Bạn có thể đặt nội bộ và sử dụng msV.link InternalVisibleToAttribution để khung / dự án thử nghiệm của bạn có thể truy cập được.

Tôi đã có một vấn đề liên quan -> liên kết .

Đây là một liên kết đến một câu hỏi Stackoverflow khác liên quan đến vấn đề:

Và cuối cùng là một bài viết trên web.


Tôi không có ý tưởng về sự tồn tại của thuộc tính đó. Cảm ơn bạn
Evan Larsen

1

Một lựa chọn khác là làm cho nó công khai nhưng đặt nó trong một hội đồng khác.

Về cơ bản, bạn có một cụm giao diện dịch vụ, một cụm triển khai dịch vụ (tham chiếu các giao diện dịch vụ), một cụm tiêu dùng dịch vụ (tham chiếu các giao diện dịch vụ) và một cụm đăng ký IOC (tham chiếu cả giao diện dịch vụ và triển khai dịch vụ để nối chúng lại với nhau ).

Tôi nên nhấn mạnh, đây không phải lúc nào cũng là giải pháp phù hợp nhất, nhưng nó là một điều đáng để xem xét.


Điều đó sẽ loại bỏ rủi ro bảo mật nghiêm trọng của việc hiển thị nội bộ?
Julian

1
@Johannes: Rủi ro bảo mật? Nếu bạn đang dựa vào các sửa đổi truy cập để cung cấp cho bạn bảo mật, bạn cần phải lo lắng. Bạn có thể có quyền truy cập vào bất kỳ phương pháp thông qua sự phản ánh. Nhưng nó loại bỏ quyền truy cập dễ dàng / được khuyến khích vào bên trong bằng cách đưa việc triển khai vào một hội đồng khác không được tham chiếu.
pdr
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.