Posso usar a injeção de dependência sem interromper o encapsulamento?

14

Aqui está minha solução e projetos:

  • BookStore (solução)
    • BookStore.Coupler (projeto)
      • Bootstrapper.cs
    • BookStore.Domain (projeto)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (projeto)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (projeto)
      • Global.asax
    • BookStore.BatchProcesses (projeto)
      • 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

Desculpe pela longa configuração do problema, não tenho uma maneira melhor de explicar isso além de detalhar meu problema real.

Eu não quero fazer meu CreateBookCommandValidator public . Eu preferiria que fosse internal , mas se eu fizer isso internal , então eu não poderei registrá-lo com o meu contêiner DI. A razão pela qual eu gostaria que fosse interno é porque o único projeto que deveria ter noção do meu validar < > implementações estão no projeto BookStore.Domain. Qualquer outro projeto só precisa consumir IValidator < > e o CompositeValidator deve ser resolvido, o que preencherá todas as validações.

Como posso usar a Injeção de Dependência sem interromper o encapsulamento? Ou eu estou indo sobre tudo isso errado?

    
por Evan Larsen 30.12.2013 / 22:50
fonte

5 respostas

11

Tornar o CreateBookCommandValidator public não viola o encapsulamento, pois

Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties direct access to them (wikipedia)

Seu CreateBookCommandValidator não permite acesso a seus membros de dados (atualmente, parece não ter nenhum), portanto, não está violando o encapsulamento.

Tornar esta turma pública não viola nenhum outro princípio (como os princípios SOLID ) porque:

  • Essa classe tem uma responsabilidade única e bem definida e, portanto, segue o Princípio da Responsabilidade Única.
  • Adicionar novos validadores ao sistema pode ser feito sem alterar uma única linha de código e, portanto, você segue o Princípio Aberto / Fechado.
  • Esse IValidador < T > interface que esta classe implementa é estreita (tem apenas um membro) e segue o Princípio de Segregação de Interface.
  • Seus consumidores só dependem desse IValidador < T > interface e, portanto, siga o Princípio de Inversão de Dependência.

Você só pode fazer o CreateBookCommandValidator interno se a classe não for consumida diretamente de fora da biblioteca, mas isso raramente é o caso, já que seus testes unitários são um importante consumidor desta classe (e quase todas as classes Seu sistema).

Embora você possa tornar a aula interna e usar [InternalsVisibleTo] para permitir que o projeto de teste de unidade acesse os componentes internos do seu projeto, por que se incomodar?

A razão mais importante para tornar as aulas internas é impedir que as partes externas (que você não tem controle) dependam dessa classe, porque isso impediria que você mudasse o futuro para essa classe sem quebrar nada. Em outras palavras, isso só é válido quando você está criando uma biblioteca reutilizável (como uma biblioteca de injeção de dependência). Na verdade, o Injetor Simples contém material interno e seu projeto de teste de unidade testa esses componentes internos.

No entanto, se você não estiver criando um projeto reutilizável, esse problema não existe. Ele não existe, porque você pode alterar os projetos que dependem dele e os outros desenvolvedores em sua equipe terão que seguir suas diretrizes. E uma diretriz simples serve: Programe para uma abstração; não uma implementação (o Princípio de Inversão de Dependência).

Portanto, para encurtar a história, não torne essa aula interna, a menos que você esteja escrevendo uma biblioteca reutilizável.

Mas se você ainda quiser fazer essa classe interna, você ainda pode registrá-la com o Simple Injector sem nenhum problema como este:

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

A única coisa a ter certeza é que todos os seus validadores têm um construtor público, mesmo que sejam internos. Se você realmente quer que seus tipos tenham um construtor interno (não sei realmente porque você gostaria disso), você pode sobrescrever o Comportamento de Resolução do Construtor .

UPDATE

Desde Simple Injector v2.6 , o comportamento padrão de RegisterManyForOpenGeneric é registrar tanto o público quanto o interno tipos. Portanto, o fornecimento de AccessibilityOption.AllTypes é agora redundante e a seguinte declaração registrará os tipos públicos e internos:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
    
por 31.12.2013 / 13:02
fonte
8

Não é grande coisa que a classe CreateBookCommandValidator seja pública.

Se você precisar criar instâncias fora da biblioteca que a define, é uma abordagem bastante natural expor a classe pública e contar com os clientes usando apenas essa classe como uma implementação de IValidate<CreateBookCommand> . (Simplesmente expor um tipo não significa que o encapsulamento está quebrado, apenas torna um pouco mais fácil para os clientes quebrar o encapsulamento).

Caso contrário, se você realmente quer forçar os clientes a não conhecerem a classe, você também pode usar um método public static factory em vez de expor a classe, por exemplo:

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

Quanto ao registro em seu contêiner DI, todos os contêineres DI que conheço permitem a construção usando um método de fábrica estática.

    
por 30.12.2013 / 23:49
fonte
5

Você pode declarar CreateBookCommandValidator como internal e aplicar o InternalsVisibleToAttribute para torná-lo visível para o BookStore.Coupler assembly. Isso também ajuda com frequência ao fazer testes unitários.

    
por 30.12.2013 / 23:49
fonte
4

Você pode torná-lo interno e usar o InternalVisibleToAttribute msdn.link para que sua estrutura / projeto de teste possa acessá-lo.

Eu tive um problema relacionado - > link .

Aqui está um link para outra pergunta do Stackoverflow sobre o problema:

E finalmente um artigo na Web.

    
por 31.12.2013 / 03:55
fonte
1

Outra opção é torná-lo público, mas colocá-lo em outra montagem.

Essencialmente, você tem um assembly de interfaces de serviço, um assembly de implementos de serviço (que faz referência a interfaces de serviço), um assembly de consumidor de serviços (que faz referência a interfaces de serviço) e um assembly de registrador IOC (que faz referência a interfaces de serviço e implementações de serviço). juntá-los).

Devo salientar que nem sempre esta é a solução mais adequada, mas vale a pena considerar.

    
por 01.01.2014 / 03:46
fonte