캡슐화를 중단하지 않고 Dependency Injection을 사용할 수 있습니까?


15

내 솔루션과 프로젝트는 다음과 같습니다.

  • 서점 (솔루션)
    • BookStore.Coupler (프로젝트)
      • Bootstrapper.cs
    • BookStore.Domain (프로젝트)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • 서점 인프라 (프로젝트)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (프로젝트)
      • Global.asax
    • BookStore.BatchProcesses (프로젝트)
      • Program.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<>));
    }
}

CreateBookCommandValidator.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);
}

CreateBookCommandHandler.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);
    }
}

ValidationCommandHandlerDecorator.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);
    }
}

Global.asax

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

Program.cs

// Pretty much the same as the Global.asax

문제에 대한 긴 설정으로 인해 죄송합니다. 실제 문제를 자세히 설명하는 것 외에는 이것을 설명하는 더 좋은 방법이 없습니다.

CreateBookCommandValidator를 만들고 싶지 않습니다 public. 오히려 그럴 internal것이지만 내가 만들면 internalDI 컨테이너에 등록 할 수 없습니다. 내부적으로 만들고 싶은 이유는 IValidate <> 구현에 대한 개념을 가져야하는 유일한 프로젝트가 BookStore.Domain 프로젝트에 있기 때문입니다. 다른 프로젝트는 IValidator <>를 사용해야하며 CompositeValidator를 해결해야 모든 유효성 검사를 수행 할 수 있습니다.

캡슐화를 중단하지 않고 의존성 주입을 사용하려면 어떻게해야합니까? 아니면 내가 이것에 대해 잘못 가고 있습니까?


단지 nitpick : 사용중인 것은 올바른 명령 패턴이 아니므로 명령을 호출하는 것은 잘못된 정보 일 수 있습니다. 또한 CreateBookCommandHandler는 LSP를 위반하는 것처럼 보입니다. 객체를 전달하면 CreateBookCommand에서 파생되는 결과는 무엇입니까? 그리고 여러분이 여기서하고있는 일은 실제로 Anemic Domain Model 반 패턴입니다. 저장과 같은 것은 도메인 내부에 있어야하며 유효성 검사는 엔티티의 일부 여야합니다.
Euphoric

1
@ 유포 릭 : 맞습니다. 이것은 명령 패턴 이 아닙니다 . 실제로 OP는 명령 / 핸들러 패턴 과 같은 다른 패턴을 따릅니다 .
Steven

좋은 답변이 너무 많아서 더 많은 답변으로 표시 할 수 있었으면 좋겠다. 도와 주셔서 감사합니다.
Evan Larsen

@Euphoric, 프로젝트 레이아웃을 다시 생각한 후에 CommandHandlers가 도메인에 있어야한다고 생각합니다. 내가 왜 인프라 프로젝트에 넣었는지 모르겠습니다. 감사.
Evan Larsen

답변:


11

CreateBookCommandValidator대중을 만드는 것은 캡슐화를 위반하지 않습니다.

캡슐화는 클래스 내에서 구조화 된 데이터 객체의 값 또는 상태를 숨겨서 권한이없는 당사자가 클래스에 직접 액세스하는 것을 방지하는 데 사용됩니다 ( wikipedia )

귀하 CreateBookCommandValidator는 데이터 멤버에 대한 액세스를 허용하지 않으며 (현재는없는 것 같습니다) 캡슐화를 위반하지 않습니다.

이 클래스를 공개로 설정한다고해서 다음과 같은 이유로 다른 원칙 (예 : SOLID 원칙)을 위반하지는 않습니다 .

  • 그 클래스는 잘 정의 된 단일 책임을 가지므로 단일 책임 원칙을 따릅니다.
  • 한 줄의 코드를 변경하지 않고도 시스템에 새로운 유효성 검사기를 추가 할 수 있으므로 열기 / 닫기 원칙을 따릅니다.
  • 이 클래스가 구현하는 IValidator <T> 인터페이스는 좁고 (한 멤버 만) 인터페이스 분리 원칙을 따릅니다.
  • 소비자는 해당 IValidator <T> 인터페이스에만 의존하므로 Dependency Inversion Principle을 따릅니다.

CreateBookCommandValidator클래스가 라이브러리 외부에서 직접 소비되지 않는 경우 에만 내부를 만들 수 있지만 단위 테스트는이 클래스 (및 시스템의 거의 모든 클래스)의 중요한 소비자이기 때문에 거의 그렇지 않습니다.

클래스를 내부로 만들고 [InternalsVisibleTo]를 사용하여 단위 테스트 프로젝트가 프로젝트 내부에 액세스하도록 허용 할 수 있지만 왜 귀찮습니까?

수업을 내부적으로 만드는 가장 중요한 이유는 외부 사용자 (제어 권한이없는)가 해당 수업에 대한 의존을 피하는 것입니다. 즉, 재사용 가능한 라이브러리 (예 : 종속성 주입 라이브러리)를 만들 때만 유지됩니다. 실제로 Simple Injector에는 내부 내용이 포함되어 있으며 단위 테스트 프로젝트는 이러한 내부를 테스트합니다.

그러나 재사용 가능한 프로젝트를 작성하지 않으면이 문제가 존재하지 않습니다. 존재하지 않습니다. 프로젝트에 의존하는 프로젝트를 변경할 수 있기 때문에 팀의 다른 개발자가 가이드 라인을 따라야합니다. 그리고 하나의 간단한 지침은 : 추상화로 프로그램; 구현이 아님 (Dependency Inversion Principle).

간단히 말해, 재사용 가능한 라이브러리를 작성하지 않는 한이 클래스를 내부적으로 만들지 마십시오.

그러나이 클래스를 내부적으로 만들고 싶다면 다음과 같은 문제없이 Simple Injector에 등록 할 수 있습니다.

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

유일하게 확인해야 할 것은 모든 유효성 검사기가 내부에도 불구하고 공개 생성자를 가지고 있다는 것입니다. 타입에 내부 생성자가 있기를 원한다면 (왜 원하는지 모르겠다) Constructor Resolution Behavior를 재정의 할 수 있습니다 .

최신 정보

Simple Injector v2.6 이후 기본 동작은 RegisterManyForOpenGeneric공용 및 내부 유형을 모두 등록하는 것입니다. 따라서 공급 AccessibilityOption.AllTypes은 이제 중복되며 다음 명령문은 공용 및 내부 유형을 모두 등록합니다.

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

8

CreateBookCommandValidator수업이 공개 되는 것은 큰 문제가 아닙니다 .

라이브러리 외부에서 인스턴스를 정의하는 인스턴스를 작성해야하는 경우, 공개 클래스를 공개하고 해당 클래스를 구현으로 만 사용하는 클라이언트에 의존하는 것은 매우 자연스러운 접근법입니다 IValidate<CreateBookCommand>. (유형 만 노출한다고해서 캡슐화가 깨 졌다는 의미는 아니며 클라이언트가 캡슐화를 좀 더 쉽게 깨뜨릴 수 있음).

그렇지 않으면 클라이언트가 클래스에 대해 알지 못하게하려면 클래스를 노출시키는 대신 공개 정적 팩토리 메소드를 사용할 수도 있습니다.

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

DI 컨테이너에 등록하는 것과 관련하여 내가 아는 모든 DI 컨테이너는 정적 팩토리 방법을 사용하여 구성을 허용합니다.


예, 감사합니다.이 게시물을 만들기 전에 원래 같은 생각을하고있었습니다. 적절한 IValidate <> 구현을 반환하는 Factory 클래스를 만들려고 생각했지만 IValidate <> 구현 중 하나에 종속성이 있으면 꽤 빨리 털이 생길 수 있습니다.
Evan Larsen

@EvanLarsen 왜? IValidate<>구현에 종속성 이있는 경우 이러한 종속성을 팩토리 메소드의 매개 변수로 사용하십시오.
jhominal December



1

다른 옵션은 공개하지만 다른 어셈블리에 넣는 것입니다.

따라서 기본적으로 서비스 인터페이스 어셈블리, 서비스 구현 어셈블리 (서비스 인터페이스를 참조), 서비스 소비자 어셈블리 (서비스 인터페이스를 참조) 및 IOC 등록자 어셈블리 (서비스 인터페이스와 서비스 구현을 모두 참조하여 연결하는 서비스)가 있습니다. ).

나는 이것이 항상 가장 적절한 해결책은 아니지만, 고려해야 할 가치가 있다고 강조해야한다.


내부를 보이게하는 심각한 보안 위험을 제거 할 수 있습니까?
Johannes

1
@Johannes : 보안 위험? 보안을 위해 액세스 수정 자에 의존하는 경우 걱정할 필요가 있습니다. 리플렉션을 통해 모든 방법에 액세스 할 수 있습니다. 그러나 참조되지 않은 다른 어셈블리에 구현을 배치하여 내부에 대한 쉽고 권장되는 액세스를 제거합니다.
pdr
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.