A Injeção de Dependência deve custar o Encapsulamento?


128

Se bem entendi, o mecanismo típico para injeção de dependência é injetar através do construtor de uma classe ou através de uma propriedade pública (membro) da classe.

Isso expõe a dependência que está sendo injetada e viola o princípio de encapsulamento OOP.

Estou correto na identificação dessa troca? Como você lida com esse problema?

Veja também a minha resposta à minha própria pergunta abaixo.


7
esta é uma pergunta muito inteligente imho
dfa

5
Responder a essa pergunta primeiro requer uma discussão sobre o que significa encapsulamento . ;)
Jeff Sternal

2
O encapsulamento é mantido pelas interfaces. Eles expõem as características essenciais do objeto e ocultam detalhes como dependências. Isso nos permite 'abrir' as classes até certo ponto, a fim de fornecer uma configuração mais flexível.
Lawrence Wagerfield

Respostas:


62

Há outra maneira de analisar esse problema que você pode achar interessante.

Quando usamos injeção de IoC / dependência, não estamos usando conceitos de POO. É certo que estamos usando uma linguagem OO como o 'host', mas as idéias por trás da IoC vêm da engenharia de software orientada a componentes, não da OO.

O software componente é sobre gerenciamento de dependências - um exemplo de uso comum é o mecanismo de montagem do .NET. Cada montagem publica a lista de montagens a que faz referência, e isso torna muito mais fácil reunir (e validar) as peças necessárias para um aplicativo em execução.

Ao aplicar técnicas semelhantes em nossos programas de OO via IoC, nosso objetivo é tornar os programas mais fáceis de configurar e manter. A publicação de dependências (como parâmetros do construtor ou o que for) é uma parte essencial disso. O encapsulamento não se aplica, pois no mundo orientado a componentes / serviços, não há um 'tipo de implementação' para o qual os detalhes vazem.

Infelizmente, atualmente, nossas linguagens não segregam os conceitos refinados e orientados a objetos dos conceitos orientados a componentes de granularidade mais grossa; portanto, essa é uma distinção que você deve ter apenas em mente :)


18
Encapsulamento não é apenas uma terminologia sofisticada. É algo real com benefícios reais e não importa se você considera seu programa "orientado a componentes" ou "orientado a objetos". O encapsulamento deve proteger o estado do seu objeto / componente / serviço / o que quer que seja alterado de maneira inesperada, e a IoC retira parte dessa proteção, portanto, definitivamente há uma troca.
Ron Inbar

1
Os argumentos fornecidos por meio de um construtor ainda se enquadram no domínio das maneiras esperadas pelas quais o objeto pode ser "alterado": elas são explicitamente expostas e os invariantes ao seu redor são impostos. Ocultar informações é o melhor termo para o tipo de privacidade a que você está se referindo, @RonInbar, e nem sempre é benéfico (torna a massa mais difícil de desembaraçar ;-)).
Nicholas Blumhardt

2
O ponto principal da OOP é que o emaranhado de massas é separado em classes separadas e você só precisa mexer com ela se for o comportamento dessa classe específica que você deseja modificar (é assim que a OOP mitiga a complexidade). Uma classe (ou módulo) encapsula seus internos enquanto expõe uma interface pública conveniente (é assim que o OOP facilita a reutilização). Uma classe que expõe suas dependências por meio de interfaces cria complexidade para seus clientes e é menos reutilizável como resultado. Também é inerentemente mais frágil.
Neutrino 25/10

1
Não importa para que ponto de vista, parece-me que o DI mina seriamente alguns dos benefícios mais valiosos do POO, e ainda não encontrei uma situação em que o encontrei realmente usado de maneira a resolver um problema real. problema existente.
Neutrino 25/10

1
Aqui está outra maneira de enquadrar o problema: imagine se os assemblies .NET escolheram "encapsulamento" e não declararam em quais outros assemblies eles confiavam. Seria uma situação louca, ler documentos e apenas esperar que algo funcionasse depois de carregá-lo. Declarar dependências nesse nível permite que as ferramentas automatizadas lidem com a composição em larga escala do aplicativo. Você precisa apertar os olhos para ver a analogia, mas forças semelhantes se aplicam no nível do componente. Existem trade-offs e YMMV como sempre :-)
Nicholas Blumhardt 26/10

29

É uma boa pergunta - mas, em algum momento, o encapsulamento em sua forma mais pura precisa ser violado para que o objeto tenha sua dependência cumprida. Algum provedor da dependência deve saber que o objeto em questão requer um Fooe que o provedor precisa ter uma maneira de fornecer oFoo o objeto.

Classicamente, este último caso é tratado como você diz, por meio de argumentos construtores ou métodos setter. No entanto, isso não é necessariamente verdade - eu sei que as versões mais recentes do framework Spring DI em Java, por exemplo, permitem anotar campos particulares (por exemplo, com@Autowired ) e a dependência será definida por meio de reflexão sem a necessidade de expor a dependência por meio de qualquer uma das classes métodos / construtores públicos. Esse pode ser o tipo de solução que você estava procurando.

Dito isto, também não acho que a injeção de construtores seja um problema. Eu sempre achei que os objetos deveriam ser totalmente válidos após a construção, de forma que tudo o que eles precisassem para desempenhar sua função (ou seja, estar em um estado válido) deveria ser fornecido pelo construtor de qualquer maneira. Se você tem um objeto que exige que um colaborador funcione, parece-me bom que o construtor anuncie publicamente esse requisito e garanta que ele seja cumprido quando uma nova instância da classe for criada.

Idealmente, ao lidar com objetos, você interage com eles por meio de uma interface, e quanto mais você faz isso (e tem dependências conectadas por meio do DI), menos você realmente precisa lidar com os construtores. Na situação ideal, seu código não lida com nem cria instâncias concretas de classes; então ele recebe um IFooDI direto, sem se preocupar com o que o construtor deFooImpl indica que precisa fazer seu trabalho e, de fato, sem nem mesmo estar ciente da FooImplexistência do mesmo. Deste ponto de vista, o encapsulamento é perfeito.

Essa é uma opinião, é claro, mas, na minha opinião, o DI não viola necessariamente o encapsulamento e, de fato, pode ajudá-lo, centralizando todo o conhecimento necessário dos internos em um só lugar. Isso não é apenas uma coisa boa por si só, mas ainda melhor este lugar está fora da sua própria base de código, portanto, nenhum código que você escreve precisa saber sobre as dependências das classes.


2
Bons pontos. Aconselho não usar o @Autowired em campos particulares; isso torna a classe difícil de testar; como você injeta zombarias ou tocos?
Lumpynose

4
Discordo. O DI viola o encapsulamento e isso pode ser evitado. Por exemplo, usando um ServiceLocator, que obviamente não precisa saber nada sobre a classe do cliente; ele só precisa saber sobre as implementações da dependência do Foo. Mas a melhor coisa, na maioria dos casos, é simplesmente usar o operador "novo".
217 Rogério Rogério

5
@Rogerio - Provavelmente qualquer estrutura DI atua exatamente como o ServiceLocator que você descreve; o cliente não sabe nada específico sobre implementações Foo, e a ferramenta DI não sabe nada específico sobre o cliente. E usar "novo" é muito pior para violar o encapsulamento, pois você precisa conhecer não apenas a classe de implementação exata, mas também as classes e instâncias exatas de todas as dependências necessárias.
Andrzej Doyle

4
O uso de "new" para instanciar uma classe auxiliar, que geralmente nem é pública, promove o encapsulamento. A alternativa de DI seria tornar pública a classe auxiliar e adicionar um construtor ou setter público na classe cliente; ambas as alterações quebrariam o encapsulamento fornecido pela classe auxiliar original.
Rogério

1
"É uma boa pergunta - mas, em algum momento, o encapsulamento em sua forma mais pura precisa ser violado para que o objeto tenha sua dependência cumprida" A premissa básica de sua resposta é simplesmente falsa. Como @ Rogério afirma renovar internamente uma dependência, e qualquer outro método em que um objeto satifique internamente suas próprias dependências não viola o encapsulamento.
Neutrino 25/10

17

Isso expõe a dependência que está sendo injetada e viola o princípio de encapsulamento OOP.

Bem, francamente falando, tudo viola o encapsulamento. :) É um tipo de princípio sensível que deve ser bem tratado.

Então, o que viola o encapsulamento?

Herança faz .

"Como a herança expõe uma subclasse a detalhes da implementação de seus pais, é comum dizer que 'a herança quebra o encapsulamento'". (Gang of Four 1995: 19)

Programação orientada a aspectos sim . Por exemplo, você registra o retorno de chamada onMethodCall () e isso oferece uma ótima oportunidade de injetar código na avaliação normal do método, adicionando efeitos colaterais estranhos, etc.

Declaração de amigo em C ++ faz .

A extensão de classe no Ruby faz . Apenas redefina um método de string em algum lugar depois que uma classe de string foi totalmente definida.

Bem, muitas coisas fazem .

O encapsulamento é um princípio bom e importante. Mas não é o único.

switch (principle)
{
      case encapsulation:
           if (there_is_a_reason)
      break!
}

3
"Esses são meus princípios, e se você não gosta deles ... bem, eu tenho outros." (Groucho Marx)
Ron Inbar 31/03

2
Eu acho que esse é o ponto. É injeção dependente vs encapsulamento. Portanto, use apenas injeção dependente quando houver benefícios significativos. É o DI em todos os lugares que dá DI um nome ruim
Richard Tingle

Não tenho certeza do que esta resposta está tentando dizer ... Que não há problema em violar o encapsulamento ao fazer DI, que está "sempre ok" porque seria violado de qualquer maneira, ou apenas que DI pode ser um motivo para violar o encapsulamento? Em outra nota, atualmente não há mais necessidade de confiar em construtores ou propriedades públicas para injetar dependências; em vez disso, podemos injetar em campos anotados particulares , o que é mais simples (menos código) e preserva o encapsulamento. Assim, podemos tirar proveito de ambos os princípios ao mesmo tempo.
Rogério

A herança, em princípio, não viola o encapsulamento, embora isso ocorra se a classe pai for mal escrita. Os outros pontos que você levanta são um paradigma de programação bastante marginal e vários recursos de linguagem que têm pouco a ver com arquitetura ou design.
Neutrino 25/10

13

Sim, o DI viola o encapsulamento (também conhecido como "ocultação de informações").

Mas o verdadeiro problema surge quando os desenvolvedores o usam como desculpa para violar os princípios do KISS (Mantenha-o Curto e Simples) e do YAGNI (Você não precisa disso).

Pessoalmente, prefiro soluções simples e eficazes. Eu uso principalmente o operador "novo" para instanciar dependências com estado, quando e onde elas forem necessárias. É simples, bem encapsulado, fácil de entender e fácil de testar. Então por que não?


Pensar no futuro não é terrível, mas concordo Mantenha-o simples, estúpido, especialmente se você não vai precisar! Vi desenvolvedores desperdiçar ciclos porque eles estão projetando algo demais para ser à prova do futuro e, com base em palpites, nem mesmo em requisitos comerciais conhecidos / suspeitos.
Jacob McKay

5

Um bom recipiente / sistema de injeção de depenância permitirá a injeção do construtor. Os objetos dependentes serão encapsulados e não precisam ser expostos publicamente. Além disso, usando um sistema DP, nenhum dos seus códigos "conhece" os detalhes de como o objeto é construído, possivelmente incluindo o objeto que está sendo construído. Há mais encapsulamento nesse caso, já que quase todo o seu código não é apenas protegido do conhecimento dos objetos encapsulados, mas nem participa da construção dos objetos.

Agora, suponho que você esteja comparando com o caso em que o objeto criado cria seus próprios objetos encapsulados, provavelmente em seu construtor. Meu entendimento sobre DP é que queremos tirar essa responsabilidade do objeto e entregá-la a outra pessoa. Para esse fim, o "alguém mais", que é o contêiner DP neste caso, possui um conhecimento íntimo que "viola" o encapsulamento; o benefício é que ele próprio extrai esse conhecimento do objeto. Alguém tem que ter. O restante do seu aplicativo não.

Eu pensaria dessa maneira: O contêiner / sistema de injeção de dependência viola o encapsulamento, mas seu código não. De fato, seu código está mais "encapsulado" do que nunca.


3
Se você tem uma situação em que o objeto cliente PODE instanciar suas dependências diretamente, por que não fazer isso? É definitivamente a coisa mais simples a se fazer e não reduz necessariamente a testabilidade. Além da simplicidade e do melhor encapsulamento, isso também facilita ter objetos com estado em vez de singletons sem estado.
Rogério

1
Além do que @ Rogério disse, também é potencialmente significativamente mais eficiente. Nem toda classe criada na história do mundo precisava ter cada uma de suas dependências instanciadas por toda a vida útil do objeto proprietário. Um objeto que usa DI perde o controle mais básico de suas próprias dependências, a saber, sua vida útil.
Neutrino 25/10

5

Como Jeff Sternal apontou em um comentário para a pergunta, a resposta depende inteiramente de como você define o encapsulamento .

Parece haver dois campos principais do que significa encapsulamento:

  1. Tudo relacionado ao objeto é um método em um objeto. Assim, um Fileobjeto pode ter métodos para Save, Print, Display, ModifyText, etc.
  2. Um objeto é seu próprio mundinho e não depende de comportamento externo.

Essas duas definições estão em contradição direta entre si. Se um Fileobjeto puder ser impresso sozinho, isso dependerá muito do comportamento da impressora. Por outro lado, se ele apenas sabe sobre algo que pode ser impresso para ele (uma IFilePrinterou alguma interface), o Fileobjeto não precisa saber nada sobre impressão e, portanto, trabalhar com ele trará menos dependências para o objeto.

Portanto, a injeção de dependência interromperá o encapsulamento se você usar a primeira definição. Mas, francamente, não sei se gosto da primeira definição - ela claramente não aumenta (se o fizesse, o MS Word seria uma classe grande).

Por outro lado, a injeção de dependência é quase obrigatória se você estiver usando a segunda definição de encapsulamento.


Eu definitivamente concordo com você sobre a primeira definição. Ele também viola o SoC, que é indiscutivelmente um dos pecados capitais da programação e provavelmente uma das razões pelas quais não é escalável.
Marcus Stade

4

Não viola o encapsulamento. Você está fornecendo um colaborador, mas a turma decide como é usada. Contanto que você siga Tell, não pergunte que as coisas estão bem. Acho a injeção de construtor preferível, mas os setters podem ser bons, desde que sejam inteligentes. Ou seja, eles contêm lógica para manter os invariantes que a classe representa.


1
Porque ... não? Se você possui um agente de log e uma classe que precisa do agente, passar o agente para essa classe não viola o encapsulamento. E isso é tudo a injeção de dependência é.
jrockway

3
Eu acho que você não entende o encapsulamento. Por exemplo, faça uma aula de data ingênua. Internamente, pode ter variáveis ​​de instância de dia, mês e ano. Se eles fossem expostos como setters simples, sem lógica, isso quebraria o encapsulamento, pois eu poderia fazer algo como definir mês para 2 e dia para 31. Por outro lado, se os setters forem inteligentes e verificarem os invariantes, tudo estará bem. . Observe também que, na última versão, eu poderia mudar o armazenamento para dias desde 1/1/1970, e nada que usasse a interface precisava estar ciente disso, desde que eu reescrevesse adequadamente os métodos dia / mês / ano.
21139 Jason Watkins

2
O DI definitivamente viola o encapsulamento / ocultação de informações. Se você transformar uma dependência interna privada em algo exposto na interface pública da classe, então, por definição, você quebrou o encapsulamento dessa dependência.
217 Rogério Rogério

2
Eu tenho um exemplo concreto em que acho que o encapsulamento está comprometido pelo DI. Eu tenho um FooProvider que obtém "dados foo" do banco de dados e um FooManager que os armazena em cache e calcula as coisas na parte superior do provedor. Eu tive consumidores do meu código indo ao FooProvider por engano para obter dados, onde eu preferia que eles fossem encapsulados, para que eles soubessem apenas do FooManager. Este é basicamente o gatilho da minha pergunta original.
urig

1
@ Rogerio: Eu diria que o construtor não faz parte da interface pública, pois é usado apenas na raiz da composição. Assim, a dependência é apenas "vista" pela raiz da composição. A única responsabilidade da raiz da composição é conectar essas dependências. Portanto, o uso de injeção de construtor não quebra nenhum encapsulamento.
Jay Sullivan

4

Isso é semelhante à resposta votada, mas quero pensar em voz alta - talvez outros vejam as coisas dessa maneira também.

  • OO clássico usa construtores para definir o contrato de "inicialização" público para os consumidores da classe (ocultando TODOS os detalhes da implementação; também conhecido como encapsulamento). Este contrato pode garantir que, após a instanciação, você tenha um objeto pronto para uso (ou seja, nenhuma etapa adicional de inicialização seja lembrada (esquecida) pelo usuário).

  • (construtor) O DI inegavelmente quebra o encapsulamento sangrando os detalhes da implementação por meio dessa interface pública do construtor. Enquanto ainda considerarmos o construtor público responsável por definir o contrato de inicialização para os usuários, criamos uma violação horrível do encapsulamento.

Exemplo teórico:

A classe Foo possui 4 métodos e precisa de um número inteiro para a inicialização, portanto, seu construtor se parece com Foo (tamanho int) e fica imediatamente claro para os usuários da classe Foo que eles devem fornecer um tamanho na instanciação para que o Foo funcione.

Digamos que essa implementação específica do Foo também precise de um IWidget para fazer seu trabalho. A injeção de construtores dessa dependência nos faria criar um construtor como Foo (tamanho int, widget IWidget)

O que me irrita é que agora temos um construtor que mistura dados de inicialização com dependências - uma entrada é do interesse do usuário da classe ( tamanho ), a outra é uma dependência interna que serve apenas para confundir o usuário e é uma implementação detalhe ( widget ).

O parâmetro size NÃO é uma dependência - é simples um valor de inicialização por instância. A IoC é excelente para dependências externas (como widget), mas não para inicialização interna do estado.

Pior ainda, e se o Widget for necessário apenas para 2 dos 4 métodos nesta classe; Talvez esteja ocorrendo uma sobrecarga de instanciação no Widget, mesmo que ele não possa ser usado!

Como comprometer / conciliar isso?

Uma abordagem é mudar exclusivamente para interfaces para definir o contrato de operação; e abolir o uso de construtores pelos usuários. Para ser consistente, todos os objetos precisariam ser acessados ​​apenas através de interfaces e instanciados apenas através de alguma forma de resolvedor (como um contêiner IOC / DI). Somente o contêiner consegue instanciar as coisas.

Isso cuida da dependência do Widget, mas como inicializamos o "tamanho" sem recorrer a um método de inicialização separado na interface Foo? Usando esta solução, perdemos a capacidade de garantir que uma instância do Foo seja totalmente inicializada no momento em que você obtém a instância. Que chatice, porque eu realmente gosto da idéia e da simplicidade da injeção de construtores.

Como obtenho a inicialização garantida neste mundo de DI, quando a inicialização é MAIS do que APENAS dependências externas?


Atualização: Acabei de perceber que o Unity 2.0 suporta o fornecimento de valores para parâmetros do construtor (como inicializadores de estado), enquanto ainda usa o mecanismo normal para a IoC de dependências durante o resolve (). Talvez outros contêineres também suportem isso? Isso resolve a dificuldade técnica de misturar o estado init e DI em um construtor, mas ainda viola o encapsulamento!
shawnT

Eu te ouço. Fiz essa pergunta porque também sinto que duas coisas boas (DI e encapsulamento) acontecem uma à custa da outra. BTW, no seu exemplo, onde apenas 2 dos 4 métodos precisam do IWidget, isso indicaria que os outros 2 pertencem a um componente IMHO diferente.
urig

3

O encapsulamento puro é um ideal que nunca pode ser alcançado. Se todas as dependências estivessem ocultas, você não precisaria de DI. Pense dessa maneira: se você realmente possui valores privados que podem ser internalizados no objeto, digamos, por exemplo, o valor inteiro da velocidade de um objeto carro, então você não tem dependência externa e não precisa inverter ou injetar essa dependência. Esses tipos de valores de estado interno que são operados exclusivamente por funções privadas são o que você deseja encapsular sempre.

Mas se você estiver construindo um carro que deseja um certo tipo de objeto de motor, terá uma dependência externa. Você pode instanciar esse mecanismo - por exemplo, o novo GMOverHeadCamEngine () - internamente no construtor do objeto car, preservando o encapsulamento, mas criando um acoplamento muito mais insidioso a uma classe concreta GMOverHeadCamEngine, ou você pode injetá-lo, permitindo que o objeto Car funcione de forma independente (e muito mais robusta) em, por exemplo, uma interface IEngine sem a dependência concreta. Não importa se você usa um contêiner do COI ou um DI simples para atingir esse objetivo - o ponto é que você tem um carro que pode usar muitos tipos de motores sem ser acoplado a nenhum deles, tornando sua base de código mais flexível e flexível. menos propenso a efeitos colaterais.

A DI não é uma violação do encapsulamento, é uma maneira de minimizar o acoplamento quando o encapsulamento é necessariamente interrompido, como é óbvio em praticamente todos os projetos de OOP. A injeção de uma dependência em uma interface externamente minimiza os efeitos colaterais do acoplamento e permite que suas classes permaneçam independentes da implementação.


3

Depende se a dependência é realmente um detalhe de implementação ou algo que o cliente deseja / precisa conhecer de uma maneira ou de outra. Uma coisa que é relevante é qual nível de abstração a classe está direcionada. aqui estão alguns exemplos:

Se você possui um método que usa o cache sob o capô para acelerar as chamadas, o objeto de cache deve ser um Singleton ou algo assim e não deve ser injetado. O fato de o cache estar sendo usado é um detalhe de implementação que os clientes da sua classe não precisam se preocupar.

Se sua classe precisar gerar fluxos de dados, provavelmente faz sentido injetar o fluxo de saída para que a classe possa produzir facilmente os resultados para uma matriz, um arquivo ou para qualquer outro lugar que outra pessoa possa enviar os dados.

Para uma área cinzenta, digamos que você tenha uma classe que faz algumas simulações de monte carlo. Precisa de uma fonte de aleatoriedade. Por um lado, o fato de precisar disso é um detalhe de implementação, pois o cliente realmente não se importa exatamente de onde vem a aleatoriedade. Por outro lado, como os geradores de números aleatórios do mundo real fazem trocas entre grau de aleatoriedade, velocidade etc. que o cliente pode querer controlar e o cliente pode querer controlar a propagação para obter um comportamento repetitivo, a injeção pode fazer sentido. Nesse caso, sugiro oferecer uma maneira de criar a classe sem especificar um gerador de números aleatórios e usar um Singleton local de encadeamento como padrão. Se / quando surgir a necessidade de um controle mais preciso, forneça outro construtor que permita a injeção de uma fonte aleatória.


2

Eu acredito na simplicidade. A aplicação de Injeção de IOC / Dependecy nas classes Domain não melhora, exceto tornando o código muito mais difícil de manter, por meio de arquivos xml externos que descrevem a relação. Muitas tecnologias, como o EJB 1.0 / 2.0 e o struts 1.1, estão revertendo, reduzindo as coisas colocadas no XML e tentando colocá-las no código como uma anotação, etc.

O IOC possui benefícios quando o objeto dependente não está pronto para criação em tempo de compilação. Isso pode acontecer na maioria dos componentes da arquitetura de nível abstrato de infra-estrutura, tentando estabelecer uma estrutura de base comum que pode precisar funcionar para diferentes cenários. Nesses lugares, o uso do COI faz mais sentido. Ainda assim, isso não torna o código mais simples / sustentável.

Como todas as outras tecnologias, isso também possui PROs e CONs. Minha preocupação é que implementemos as mais recentes tecnologias em todos os lugares, independentemente do melhor uso do contexto.


2

O encapsulamento é quebrado apenas se uma classe tiver a responsabilidade de criar o objeto (que requer conhecimento de detalhes de implementação) e depois usar a classe (que não requer conhecimento desses detalhes). Vou explicar o porquê, mas primeiro uma rápida anaologia do carro:

Quando eu estava dirigindo meu velho Kombi de 1971, eu podia pressionar o acelerador e ele foi (um pouco) mais rápido. Eu não precisava saber o porquê, mas os caras que construíram o Kombi na fábrica sabiam exatamente o porquê.

Mas voltando à codificação. Encapsulamento está "ocultando um detalhe de implementação de algo usando essa implementação". O encapsulamento é uma coisa boa porque os detalhes da implementação podem mudar sem que o usuário da classe saiba.

Ao usar injeção de dependência, a injeção de construtor é usada para construir objetos de tipo de serviço (em oposição a objetos de entidade / valor que modelam o estado). Quaisquer variáveis ​​de membro no objeto de tipo de serviço representam detalhes de implementação que não devem vazar. por exemplo, número da porta do soquete, credenciais do banco de dados, outra classe a ser chamada para executar criptografia, cache, etc.

O construtor é relevante quando a classe está sendo criada inicialmente. Isso acontece durante a fase de construção enquanto o contêiner de DI (ou fábrica) conecta todos os objetos de serviço. O contêiner de DI conhece apenas detalhes de implementação. Ele sabe tudo sobre os detalhes da implementação, como os da fábrica Kombi conhecem sobre as velas de ignição.

No tempo de execução, o objeto de serviço criado é chamado apon para realizar algum trabalho real. No momento, o chamador do objeto não sabe nada sobre os detalhes da implementação.

Essa sou eu levando meu Kombi para a praia.

Agora, de volta ao encapsulamento. Se os detalhes da implementação forem alterados, a classe que usa essa implementação em tempo de execução não precisará ser alterada. O encapsulamento não está quebrado.

Também posso dirigir meu carro novo para a praia. O encapsulamento não está quebrado.

Se os detalhes da implementação mudarem, o contêiner de DI (ou fábrica) precisa ser alterado. Você nunca estava tentando ocultar os detalhes de implementação da fábrica em primeiro lugar.


Como você testaria sua fábrica? E isso significa que o cliente precisa conhecer a fábrica para adquirir um carro em funcionamento, o que significa que você precisa de uma fábrica para cada outro objeto em seu sistema.
Rodrigo Ruiz

2

Tendo lutado um pouco mais com o assunto, estou agora na opinião de que a Injeção de Dependência (neste momento) viola o encapsulamento até certo ponto. Não me interpretem mal - acho que o uso de injeção de dependência vale a pena na maioria dos casos.

O motivo pelo qual a DI viola o encapsulamento fica claro quando o componente em que você está trabalhando deve ser entregue a uma parte "externa" (pense em escrever uma biblioteca para um cliente).

Quando meu componente exige que os subcomponentes sejam injetados por meio do construtor (ou propriedades públicas), não há garantia para

"impedindo que os usuários definam os dados internos do componente em um estado inválido ou inconsistente".

Ao mesmo tempo, não se pode dizer que

"os usuários do componente (outras partes do software) precisam apenas saber o que o componente faz e não podem se tornar dependentes dos detalhes de como o fazem" .

Ambas as citações são de Wikipedia .

Para dar um exemplo específico: preciso fornecer uma DLL do lado do cliente que simplifique e oculte a comunicação com um serviço WCF (essencialmente uma fachada remota). Como depende de três classes de proxy WCF diferentes, se eu adotar a abordagem de DI, sou forçado a expô-las por meio do construtor. Com isso, exponho as partes internas da minha camada de comunicação que estou tentando ocultar.

Geralmente sou a favor do DI. Neste exemplo (extremo) em particular, isso me parece perigoso.


2

O DI viola o encapsulamento para objetos não compartilhados - ponto final. Objetos compartilhados têm uma vida útil fora do objeto que está sendo criado e, portanto, devem ser AGREGADOS no objeto que está sendo criado. Objetos particulares ao objeto que está sendo criado devem ser COMPOSIDOS no objeto criado - quando o objeto criado é destruído, ele leva o objeto composto. Vamos tomar o corpo humano como exemplo. O que é composto e o que é agregado. Se usássemos DI, o construtor do corpo humano teria centenas de objetos. Muitos dos órgãos, por exemplo, são (potencialmente) substituíveis. Mas, eles ainda são compostos no corpo. As células sanguíneas são criadas no corpo (e destruídas) todos os dias, sem a necessidade de influências externas (exceto proteínas). Assim, as células sanguíneas são criadas internamente pelo corpo - o novo BloodCell ().

Os advogados da DI argumentam que um objeto NUNCA deve usar o novo operador. Essa abordagem "purista" não só viola o encapsulamento, mas também o Princípio de Substituição de Liskov para quem está criando o objeto.


1

Eu lutei com essa noção também. A princípio, o 'requisito' de usar o contêiner de DI (como o Spring) para instanciar um objeto parecia saltar através de aros. Mas, na realidade, não é realmente um aro - é apenas outra maneira 'publicada' de criar objetos de que preciso. Certamente, o encapsulamento está "quebrado" porque alguém "fora da classe" sabe do que precisa, mas na verdade não é o resto do sistema que sabe disso - é o contêiner DI. Nada de mágico acontece de maneira diferente porque o DI 'sabe' que um objeto precisa de outro.

De fato, fica ainda melhor - concentrando-me em Fábricas e Repositórios, nem preciso saber que o DI está envolvido! Isso para mim coloca a tampa de volta no encapsulamento. Uau!


1
Enquanto o DI estiver encarregado de toda a cadeia de instanciação, concordo que ocorra o encapsulamento. Mais ou menos, porque as dependências ainda são públicas e podem ser abusadas. Mas quando em algum lugar "em cima" da cadeia alguém precisa instanciar um objeto sem que eles usem DI (talvez eles sejam "terceiros"), fica confuso. Eles estão expostos às suas dependências e podem ser tentados a abusar deles. Ou eles podem não querer saber sobre eles.
28809 urig

1

PS. Ao fornecer injeção de dependência, você não necessariamente quebra o encapsulamento . Exemplo:

obj.inject_dependency(  factory.get_instance_of_unknown_class(x)  );

O código do cliente ainda não conhece os detalhes da implementação.


Como no seu exemplo é injetado algo? (Exceto para a nomeação de seu setter função)
foo

1

Talvez essa seja uma maneira ingênua de pensar sobre isso, mas qual é a diferença entre um construtor que aceita um parâmetro inteiro e um construtor que aceita um serviço como parâmetro? Isso significa que definir um número inteiro fora do novo objeto e alimentá-lo no objeto quebra o encapsulamento? Se o serviço for usado apenas dentro do novo objeto, não vejo como isso quebraria o encapsulamento.

Além disso, ao usar algum tipo de recurso de fiação automática (Autofac para C #, por exemplo), ele torna o código extremamente limpo. Ao criar métodos de extensão para o construtor Autofac, consegui recortar MUITO código de configuração de DI que eu teria que manter ao longo do tempo à medida que a lista de dependências aumentava.


Não se trata de valor versus serviço. Trata-se de inversão de controle - se o construtor da classe configura a classe ou se esse serviço assume a configuração da classe. Para o qual ele precisa conhecer os detalhes de implementação dessa classe, você tem outro lugar para manter e sincronizar.
foo

1

Eu acho que é evidente que pelo menos o DI enfraquece significativamente o encapsulamento. Além disso, aqui estão algumas outras desvantagens da DI a serem consideradas.

  1. Isso torna o código mais difícil de reutilizar. Um módulo que um cliente pode usar sem precisar fornecer explicitamente dependências é obviamente mais fácil de usar do que aquele em que o cliente precisa descobrir de alguma forma quais são as dependências desse componente e depois disponibilizá-las. Por exemplo, um componente criado originalmente para ser usado em um aplicativo ASP pode esperar que suas dependências sejam fornecidas por um contêiner de DI que forneça instâncias de objeto com vida útil relacionada a solicitações HTTP do cliente. Talvez não seja fácil reproduzir em outro cliente que não seja fornecido com o mesmo contêiner DI incorporado que o aplicativo ASP original.

  2. Isso pode tornar o código mais frágil. As dependências fornecidas pela especificação da interface podem ser implementadas de maneiras inesperadas, o que gera uma classe inteira de erros de tempo de execução que não são possíveis com uma dependência concreta resolvida estaticamente.

  3. Isso pode tornar o código menos flexível, no sentido de que você pode ter menos opções sobre como deseja que ele funcione. Nem toda classe precisa ter todas as suas dependências em existência durante toda a vida útil da instância proprietária, mas com muitas implementações de DI, você não tem outra opção.

Com isso em mente, acho que a pergunta mais importante se torna: "será que uma dependência específica precisa ser especificada externamente? ". Na prática, raramente achei necessário fazer uma dependência fornecida externamente apenas para dar suporte aos testes.

Onde uma dependência realmente precisa ser fornecida externamente, isso normalmente sugere que a relação entre os objetos é uma colaboração e não uma dependência interna; nesse caso, o objetivo apropriado é o encapsulamento de cada classe, em vez do encapsulamento de uma classe dentro da outra. .

Na minha experiência, o principal problema com relação ao uso de DI é que, se você inicia com uma estrutura de aplicativo com DI integrada ou adiciona suporte a DI à sua base de código, por algum motivo, as pessoas assumem que, como você tem suporte a DI, esse deve ser o correto maneira de instanciar tudo . Eles nem sequer se incomodam em fazer a pergunta "essa dependência precisa ser especificada externamente?". E pior, eles também começam a tentar forçar todo mundo a usar o suporte de DI para tudo também.

O resultado disso é que inexoravelmente sua base de código começa a se transformar em um estado em que a criação de qualquer instância de qualquer coisa em sua base de código requer resmas de configuração obtusa de contêiner de DI e a depuração de algo é duas vezes mais difícil, porque você tem a carga de trabalho extra de tentar identificar como e onde tudo foi instanciado.

Então, minha resposta para a pergunta é esta. Use o DI onde você pode identificar um problema real que ele resolve para você, que você não pode resolver de maneira mais simples.


0

Concordo que, ao extremo, o DI pode violar o encapsulamento. Normalmente, o DI expõe dependências que nunca foram verdadeiramente encapsuladas. Aqui está um exemplo simplificado emprestado dos Singletons de Miško Hevery : Mentirosos Patológicos :

Você começa com um teste de cartão de crédito e escreve um teste de unidade simples.

@Test
public void creditCard_Charge()
{
    CreditCard c = new CreditCard("1234 5678 9012 3456", 5, 2008);
    c.charge(100);
}

No próximo mês, você recebe uma fatura de US $ 100. Por que você foi cobrado? O teste de unidade afetou um banco de dados de produção. Internamente, o CreditCard faz chamadas Database.getInstance(). A refatoração do CreditCard para que seja necessário um DatabaseInterfaceem seu construtor expõe o fato de que há dependência. Mas eu argumentaria que a dependência nunca foi encapsulada, pois a classe CreditCard causa efeitos colaterais visíveis externamente. Se você quiser testar o CreditCard sem refatorar, certamente poderá observar a dependência.

@Before
public void setUp()
{
    Database.setInstance(new MockDatabase());
}

@After
public void tearDown()
{
    Database.resetInstance();
}

Não acho que valha a pena se a exposição do banco de dados como uma dependência reduz o encapsulamento, porque é um bom design. Nem todas as decisões de DI serão tão diretas. No entanto, nenhuma das outras respostas mostra um exemplo contrário.


1
Os testes de unidade geralmente são escritos pelo autor da classe; portanto, não há problema em especificar as dependências no caso de teste, do ponto de vista técnico. Quando a classe do cartão de crédito for alterada posteriormente para usar uma API da Web como o PayPal, o usuário precisará alterar tudo se estiver desativado. Os testes de unidade geralmente são feitos com um conhecimento profundo do assunto que está sendo testado (não é esse o ponto?), Então acho que os testes são mais uma exceção do que um exemplo típico.
Kizzx2 20/09/10

1
O objetivo do DI é evitar as alterações que você descreveu. Se você alternasse de uma API da Web para o PayPal, não alteraria a maioria dos testes porque eles usariam um MockPaymentService e CreditCard seriam construídos com um PaymentService. Você teria apenas alguns testes analisando a interação real entre o CreditCard e um PaymentService real, para que mudanças futuras sejam muito isoladas. Os benefícios são ainda maiores para gráficos de dependência mais profundos (como uma classe que depende do CreditCard).
Craig P. Motlin

@Craig p. Motlin Como o CreditCardobjeto pode mudar de uma WebAPI para PayPal, sem que nada externo à classe precise ser alterado?
Ian Boyd

@Ian Mencionei que o CreditCard deve ser refatorado para levar um DatabaseInterface em seu construtor que o protege de alterações nas implementações do DatabaseInterfaces. Talvez isso precise ser ainda mais genérico e levar um StorageInterface do qual a WebAPI poderia ser outra implementação. O PayPal está no nível errado, porque é uma alternativa ao CreditCard. Tanto o PayPal quanto o CreditCard poderiam implementar o PaymentInterface para proteger outras partes do aplicativo de fora deste exemplo.
Craig P. Motlin

@ kizzx2 Em outras palavras, não faz sentido dizer que um cartão de crédito deve usar o PayPal. São alternativas na vida real.
Craig P. Motlin

0

Eu acho que é uma questão de escopo. Ao definir o encapsulamento (sem informar como), você deve definir qual é a funcionalidade encapsulada.

  1. Classificar como está : o que você está encapsulando é a única responsabilidade da classe. O que sabe fazer. Por exemplo, classificando. Se você injeta algum comparador para pedidos, digamos, clientes, isso não faz parte do encapsulado: quicksort.

  2. Funcionalidade configurada : se você deseja fornecer uma funcionalidade pronta para uso, não está fornecendo a classe QuickSort, mas uma instância da classe QuickSort configurada com um Comparador. Nesse caso, o código responsável pela criação e configuração que deve estar oculto do código do usuário. E esse é o encapsulamento.

Quando você está programando classes, implementando responsabilidades únicas em classes, está usando a opção 1.

Quando você está programando aplicativos, é algo que faz algum trabalho concreto útil, e você usa repetidamente a opção 2.

Esta é a implementação da instância configurada:

<bean id="clientSorter" class="QuickSort">
   <property name="comparator">
      <bean class="ClientComparator"/>
   </property>
</bean>

É assim que outro código de cliente o usa:

<bean id="clientService" class"...">
   <property name="sorter" ref="clientSorter"/>
</bean>

Ele é encapsulado porque, se você alterar a implementação (alterar a clientSorterdefinição do bean), não interrompe o uso do cliente. Talvez, ao usar arquivos xml com todos escritos juntos, você esteja vendo todos os detalhes. Mas acredite, o código do cliente ( ClientService) não sabe nada sobre seu classificador.


0

Provavelmente vale a pena mencionar que Encapsulationé um pouco dependente da perspectiva.

public class A { 
    private B b;

    public A() {
        this.b = new B();
    }
}


public class A { 
    private B b;

    public A(B b) {
        this.b = b;
    }
}

Da perspectiva de alguém que trabalha na Aclasse, no segundo exemplo, Asabe muito menos sobre a natureza dathis.b

Considerando que sem DI

new A()

vs

new A(new B())

A pessoa que olha esse código sabe mais sobre a natureza do Asegundo exemplo.

Com o DI, pelo menos todo esse conhecimento vazado está em um só lugar.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.