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

15

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 outra maneira de explicar isso além de detalhar meu problema real.

Não quero criar meu CreateBookCommandValidator public. Eu prefiro que seja, internalmas se eu fizer, internalnão poderei registrá-lo no meu DI Container. A razão pela qual eu gostaria que fosse interno é porque o único projeto que deveria ter noção das minhas implementações IValidate <> está no projeto BookStore.Domain. Qualquer outro projeto precisa apenas consumir IValidator <> e o CompositeValidator deve ser resolvido para cumprir todas as validações.

Como posso usar a injeção de dependência sem quebrar o encapsulamento? Ou estou fazendo tudo errado?

Evan Larsen
fonte
Apenas um resumo: O que você está usando não é um padrão de comando correto; portanto, chamá-lo de comando pode ser uma informação incorreta. Além disso, o CreateBookCommandHandler parece estar quebrando o LSP: o que acontecerá, se você passar um objeto, derivado do CreateBookCommand? E acho que o que você está fazendo aqui é na verdade antipadrão do Modelo de Domínio Anêmico. Coisas como salvar devem estar dentro do domínio e a validação deve fazer parte da entidade.
Euphoric
1
@Euphoric: Está correto. Este não é o padrão de comando . De fato, o OP segue um padrão diferente: o padrão de comando / manipulador .
31413 Steven
Havia tantas boas respostas que eu gostaria de ter marcado mais como resposta. Obrigado a todos por sua ajuda.
Evan Larsen
@Euphoric, depois de repensar o layout do projeto, acho que os CommandHandlers devem estar no domínio. Não sei por que os coloquei no projeto de infraestrutura. Obrigado.
Evan Larsen

Respostas:

11

Tornar CreateBookCommandValidatorpúblico não viola o encapsulamento, pois

O encapsulamento é usado para ocultar os valores ou o estado de um objeto de dados estruturado dentro de uma classe, impedindo que partes não autorizadas acessem diretamente a eles ( wikipedia )

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

A divulgação desta classe não viola nenhum outro princípio (como os princípios do SOLID ), porque:

  • Essa classe tem uma única responsabilidade bem definida e, portanto, segue o Princípio da Responsabilidade Única.
  • A adição de novos validadores ao sistema pode ser feita sem alterar uma única linha de código e, portanto, você segue o Princípio Aberto / Fechado.
  • A interface IValidator <T> que essa classe implementa é estreita (possui apenas um membro) e segue o Princípio de Segregação da Interface.
  • Seus consumidores dependem apenas dessa interface IValidator <T> e, portanto, seguem o Princípio de inversão de dependência.

Você só pode tornar CreateBookCommandValidatorinterno se a classe não for consumida diretamente de fora da biblioteca, mas isso quase nunca acontece, pois seus testes de unidade são um consumidor importante dessa classe (e quase todas as classes em seu sistema).

Embora você possa tornar a classe interna e usar [InternalsVisibleTo] para permitir que o projeto de teste da unidade acesse as internas do seu projeto, por que se preocupar?

O motivo mais importante para tornar as classes internas é impedir que partes externas (sobre as quais você não tem controle) dependam dessa classe, porque isso impediria que você fizesse alterações futuras nessa 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). De fato, o Simple Injector 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 da sua equipe terão que seguir suas diretrizes. E uma diretriz simples fará: Programar para uma abstração; não é uma implementação (o Princípio da inversão de dependência).

Resumindo a história, não torne essa classe interna, a menos que você esteja escrevendo uma biblioteca reutilizável.

Mas se você ainda deseja tornar essa classe interna, ainda pode registrá-la no Simple Injector sem nenhum problema como este:

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

A única coisa a garantir é que todos os seus validadores tenham um construtor público, mesmo que sejam internos. Se você realmente deseja que seus tipos tenham um construtor interno (realmente não sei por que você desejaria isso), você pode substituir o Comportamento de resolução do construtor .

ATUALIZAR

Desde o Simple Injector v2.6 , o comportamento padrão de RegisterManyForOpenGenericé registrar os tipos público e interno. Portanto, o fornecimento AccessibilityOption.AllTypesagora é redundante e a seguinte declaração registra os tipos público e interno:

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

Não é grande coisa que a CreateBookCommandValidatorclasse 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 apenas usando essa classe como 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 quebrarem o encapsulamento).

Caso contrário, se você realmente deseja forçar os clientes a não conhecerem a classe, também pode usar um método público de fábrica estática 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 estático de fábrica.

jhominal
fonte
Sim, obrigado. Eu estava pensando originalmente a mesma coisa antes de criar este post. Eu estava pensando em criar uma classe Factory que retornaria a implementação apropriada IValidate <>, mas se alguma das implementações IValidate <> tivesse alguma dependência, provavelmente ficaria bem peludo rapidamente.
Evan Larsen
@EvanLarsen Why? Se a IValidate<>implementação tiver dependências, coloque-as como parâmetros no método de fábrica.
Jhominal 31/12/13
5

Você pode declarar CreateBookCommandValidatorcomo internalaplicar o InternalsVisibleToAttribute para torná-lo visível para o BookStore.Couplerassembly. Isso também costuma ajudar ao fazer testes de unidade.

Olivier Jacot-Descombes
fonte
Eu não tinha ideia da existência desse atributo. Obrigado
Evan Larsen
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.

Johannes
fonte
Eu não tinha ideia da existência desse atributo. Obrigado
Evan Larsen
1

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

Portanto, basicamente, você tem um conjunto de interfaces de serviço, um conjunto de implementações de serviço (que faz referência a interfaces de serviço), um conjunto de consumidor de serviço (que faz referência a interfaces de serviço) e um conjunto de registrador IOC (que faz referência a interfaces de serviço e implementações de serviço para conectá-los) )

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

pdr
fonte
Isso removeria o pequeno risco de segurança de tornar os internos visíveis?
Johannes
1
@ Johannes: Risco de segurança? Se você está contando com modificadores de acesso para oferecer segurança, precisa se preocupar. Você pode obter acesso a qualquer método via reflexão. Mas remove o acesso fácil / incentivado a internos colocando a implementação em outro assembly que não é referenciado.
PDR