Quando NÃO aplicar o Princípio de Inversão de Dependência?

43

Atualmente, estou tentando descobrir o SOLID. Portanto, o Princípio de Inversão de Dependência significa que quaisquer duas classes devem se comunicar por meio de interfaces, não diretamente. Exemplo: Se class Ativer um método, que espera um ponteiro para um objeto do tipo class B, esse método deve realmente esperar um objeto do tipo abstract base class of B. Isso também ajuda no processo Abrir / Fechar.

Desde que entendi corretamente, minha pergunta seria: é uma boa prática aplicar isso a todas as interações de classe ou devo tentar pensar em termos de camadas ?

A razão de eu ser cético é porque estamos pagando algum preço por seguir esse princípio. Diga, eu preciso implementar o recurso Z. Após análise, concluo que a funcionalidade Zconsiste de funcionalidade A, Be C. I criar uma fachada de classe Z, que, por meio de interfaces, usa classes A, Be C. I começar a codificar a implementação e em algum momento eu percebo essa tarefa Zna verdade consiste de funcionalidade A, Be D. Agora preciso descartar a Cinterface, o Cprotótipo de classe e escrever uma Dinterface e uma classe separadas . Sem interfaces, apenas a classe precisaria ser substituída.

Em outras palavras, para mudar alguma coisa, preciso alterar 1. o chamador 2. a interface 3. a declaração 4. a implementação. Em uma implementação diretamente acoplada em python, eu precisaria alterar apenas a implementação.

Vorac
fonte
13
A inversão de dependência é simplesmente uma técnica, portanto só deve ser aplicada quando necessário ... não há limite para o grau em que pode ser aplicada; portanto, se você a aplicar em qualquer lugar, você acaba com o lixo: como em qualquer outra situação técnica específica.
24515 Frank Hileman
Simplificando, a aplicação de alguns princípios de design de software depende de poder refatorar impiedosamente quando os requisitos estão mudando. Desses, acredita-se que a parte da interface capture melhor os invariantes contratuais do design, enquanto o código-fonte (implementação) deve tolerar mudanças mais frequentes.
rwong 26/02
@rwong Uma interface captura invariantes contratuais apenas se você usar um idioma que suporte invariantes contratuais. Em linguagens comuns (Java, C #), uma interface é simplesmente um conjunto de assinaturas de API. Adicionar interfaces supérfluas apenas degrada um design.
Frank Hileman
Eu diria que você entendeu errado. O DIP visa evitar dependências em tempo de compilação de um componente de "alto nível" para um de "baixo nível", a fim de permitir a reutilização do componente de alto nível em outros contextos, nos quais você usaria uma implementação diferente para o baixo componente de nível; isso é feito através da criação de um tipo abstrato no alto nível, que é implementado pelos componentes de baixo nível; portanto, os componentes de alto e baixo nível dependem dessa abstração. No final, os componentes de alto e baixo nível se comunicam através de uma interface, mas essa não é a essência do DIP.
Rogério

Respostas:

87

Em muitos cartuns ou outras mídias, as forças do bem e do mal são frequentemente ilustradas por um anjo e um demônio sentados nos ombros do personagem. Em nossa história aqui, em vez do bem e do mal, temos o SOLID em um ombro e YAGNI (você não vai precisar!) Sentado no outro.

Os princípios do SOLID levados ao máximo são mais adequados para sistemas corporativos enormes, complexos e ultra-configuráveis. Para sistemas menores ou mais específicos, não é apropriado tornar tudo ridiculamente flexível, pois o tempo que você gasta abstraindo coisas não será um benefício.

Às vezes, passar interfaces em vez de classes concretas significa, por exemplo, que você pode facilmente trocar a leitura de um arquivo para um fluxo de rede. No entanto, para uma grande quantidade de projetos de software, esse tipo de flexibilidade nem sempre será necessário, e você também pode apenas passar em classes de arquivos concretas e chamá-lo por dia e poupar suas células cerebrais.

Parte da arte do desenvolvimento de software é ter uma boa noção do que é provável que mude com o passar do tempo e do que não é. Para as coisas que provavelmente mudarão, use as interfaces e outros conceitos do SOLID. Para as coisas que não funcionam, use o YAGNI e apenas passe tipos concretos, esqueça as classes de fábrica, esqueça todo o tempo de execução da instalação e configuração, etc. e esqueça muitas abstrações do SOLID. Na minha experiência, a abordagem YAGNI provou estar correta com muito mais frequência do que não.

whatsisname
fonte
19
Minha primeira introdução ao SOLID foi há cerca de 15 anos, em um novo sistema que estávamos construindo. Todos nós bebemos o homem Kool Aid. Se alguém mencionasse algo que soasse como YAGNI, nós seríamos "Pfffft ... plebeus". Tive a honra (horror?) De ver esse sistema evoluir ao longo da próxima década. Tornou-se uma bagunça pesada que ninguém conseguia entender, nem mesmo nós fundadores. Os arquitetos adoram o SOLID. As pessoas que realmente ganham a vida amam YAGNI. Nem é perfeito, mas YAGNI está mais perto da perfeição e deve ser o seu padrão se você não sabe o que está fazendo. :-)
Calphool
11
@ NWard Sim, fizemos isso em um projeto. Enlouqueceu com isso. Agora, nossos testes são impossíveis de ler ou manter, em parte por causa de zombarias. Além disso, devido à injeção de dependência, é uma dor na parte traseira navegar pelo código quando você está tentando descobrir algo. O SOLID não é uma bala de prata. YAGNI não é uma bala de prata. O teste automatizado não é uma bala de prata. Nada pode impedi-lo de fazer o trabalho duro de pensar sobre o que está fazendo e tomar decisões sobre se isso vai ajudar ou dificultar o seu trabalho ou o de outra pessoa.
Jpmc26
22
Muito sentimento anti-SÓLIDO aqui. SÓLIDO e YAGNI não são duas extremidades de um espectro. Eles são como as coordenadas X e Y em um gráfico. Um bom sistema possui muito pouco código supérfluo (YAGNI) E segue os princípios do SOLID.
26515 Stephen
31
Meh, (a) eu discordo de que SOLID = empresa e (b) o ponto principal do SOLID é que tendemos a ser extremamente pobres previsores do que será necessário. Eu tenho que concordar com @ Stephen aqui. YAGNI diz que não devemos tentar antecipar requisitos futuros que não estão claramente enunciados hoje. O SOLID diz que devemos esperar que o design evolua ao longo do tempo e aplique certas técnicas simples para facilitar. Eles não são mutuamente exclusivos; ambas são técnicas para se adaptar às mudanças nos requisitos. Os problemas reais acontecem quando você tenta projetar para requisitos pouco claros ou muito distantes.
Aaronaught
8
"você pode facilmente trocar a leitura de um arquivo para um fluxo de rede" - este é um bom exemplo em que uma descrição simplificada demais de DI leva as pessoas a se desviarem. Às vezes, as pessoas pensam (com efeito): "esse método usará um File, portanto, será necessário um IFiletrabalho concluído". Então eles não podem substituir facilmente um fluxo de rede, porque exigiram demais a interface e há operações no IFilemétodo que nem sequer usam, que não se aplicam aos soquetes, portanto, um soquete não pode ser implementado IFile. Uma das coisas DI não é uma bala de prata para, está inventando as abstrações direito (interfaces) :-)
Steve Jessop
11

Nas palavras dos leigos:

A aplicação do DIP é fácil e divertida . Não conseguir o design correto na primeira tentativa não é motivo suficiente para desistir completamente do DIP.

  • Geralmente, os IDEs ajudam você a refatorar esse tipo, alguns até permitem extrair a interface de uma classe já implementada
  • É quase impossível acertar o design da primeira vez
  • O fluxo de trabalho normal envolve alterar e repensar as interfaces nos primeiros estágios do desenvolvimento
  • À medida que o desenvolvimento evolui, ele amadurece e você terá menos motivos para modificar as interfaces
  • Em um estágio avançado, as interfaces (o design) estarão maduras e dificilmente mudarão.
  • A partir desse momento, você começa a colher os benefícios, pois seu aplicativo está aberto para aumentar.

Por outro lado, a programação com interfaces e OOD pode trazer de volta a alegria ao ofício às vezes obsoleto da programação.

Algumas pessoas dizem que isso acrescenta complexidade, mas acho que o opossito é verdadeiro. Mesmo para pequenos projetos. Isso facilita o teste / zombaria. Faz com que seu código tenha menos caseinstruções ou aninhadas ifs. Reduz a complexidade ciclomática e faz você pensar de maneiras novas. Torna a programação mais semelhante ao design e fabricação do mundo real.

Tulains Córdova
fonte
5
Não sei quais linguagens ou IDE estão sendo usados ​​pelo OP, mas no VS 2013 é ridiculamente simples trabalhar com interfaces, extrair interfaces e implementá-las, e é fundamental se o TDD for usado. Não há custos adicionais de desenvolvimento para o desenvolvimento usando esses princípios.
27515 stephenbayer
Por que essa resposta está falando sobre DI se a pergunta era sobre DIP? DIP é um conceito dos anos 90, enquanto DI é de 2004. Eles são muito diferentes.
Rogério
1
(Meu comentário anterior foi feito para outra resposta; ignore.) "Programar em interfaces" é muito mais geral que o DIP, mas não se trata de fazer com que todas as classes implementem uma interface separada. E isso só facilita o "teste / zombaria" se as ferramentas de teste / zombaria sofrem sérias limitações.
Rogério
@ Rogério Geralmente, ao usar DI, nem todas as classes implementam uma interface separada. Uma interface sendo implementada por várias classes é comum.
Tulains Córdova
@ Rogério Corrigi minha resposta, toda vez que mencionei DI eu quis dizer DIP.
Tulains Córdova
9

Use inversão de dependência onde isso fizer sentido.

Um contra-exemplo extremo é a classe "string" incluída em muitos idiomas. Representa um conceito primitivo, essencialmente uma matriz de caracteres. Supondo que você possa alterar essa classe principal, não faz sentido usar o DI aqui, porque você nunca precisará trocar o estado interno por outra coisa.

Se você possui um grupo de objetos usados ​​internamente em um módulo que não são expostos a outros módulos ou reutilizados em qualquer lugar, provavelmente não vale a pena o esforço para usar o DI.

Há dois lugares em que o DI deve ser usado automaticamente na minha opinião:

  1. Em módulos projetados para extensão. Se todo o objetivo de um módulo é estendê-lo e alterar o comportamento, faz todo o sentido incluir o DI desde o início.

  2. Nos módulos que você está refatorando para fins de reutilização de código. Talvez você tenha codificado uma classe para fazer alguma coisa, depois perceba que, com um refator, você pode aproveitar esse código em outro lugar e é necessário fazê-lo . Esse é um ótimo candidato para DI e outras alterações de extensibilidade.

As chaves aqui são usadas onde for necessário, pois introduzirão complexidade extra e certifique-se de medir as necessidades através de requisitos técnicos (ponto um) ou revisão quantitativa de código (ponto dois).

A DI é uma ótima ferramenta, mas, como qualquer ferramenta *, pode ser usada em excesso ou mal.

* Exceção à regra acima: uma serra alternativa é a ferramenta perfeita para qualquer trabalho. Se não corrigir o problema, ele será removido. Permanentemente.


fonte
3
E se "o seu problema" for um buraco na parede? Uma serra não a removeria; isso tornaria as coisas piores. ;)
Mason Wheeler
3
@MasonWheeler com um poderoso e divertido de usar serra, "buraco na parede" poderia se transformar em "porta" que é um bem útil :-)
1
Você não pode usar uma serra para fazer um patch para o buraco?
JeffO 25/02
Embora existam algumas vantagens em ter um tipo não extensível pelo usuário String, há muitos casos em que representações alternativas seriam úteis se o tipo tivesse um bom conjunto de operações virtuais (por exemplo, copie uma substring para uma parte especificada de a short[], relate se substring contém ou pode conter apenas ASCII, tente copiar uma substring que se acredita conter apenas ASCII para uma parte especificada de um byte[], etc.) É uma pena que estruturas não tenham seus tipos de cadeias implementando interfaces úteis relacionadas a cadeias.
supercat
1
Por que essa resposta está falando sobre DI se a pergunta era sobre DIP? DIP é um conceito dos anos 90, enquanto DI é de 2004. Eles são muito diferentes.
Rogério
5

Parece-me que falta à pergunta original parte do ponto do DIP.

A razão de eu ser cético é que estamos pagando algum preço por seguir esse princípio. Digamos, preciso implementar o recurso Z. Após a análise, concluo que o recurso Z consiste nas funcionalidades A, B e C. Crio uma classe fascada Z, que, por meio de interfaces, usa as classes A, B e C. Começo a codificar o implementação e, em algum momento, percebo que a tarefa Z realmente consiste nas funcionalidades A, B e D. Agora, preciso descartar a interface C, o protótipo da classe C e escrever a interface e a classe D separadas. Sem interfaces, apenas a classe iria precisar ser substituída.

Para realmente tirar proveito do DIP, você deve criar a classe Z primeiro e chamar a funcionalidade das classes A, B e C (que ainda não foram desenvolvidas). Isso fornece a API para as classes A, B e C. Em seguida, você cria as classes A, B e C e preenche os detalhes. Você efetivamente deve criar as abstrações necessárias ao criar a classe Z, com base inteiramente no que a classe Z precisa. Você pode até escrever testes em torno da classe Z antes que as classes A, B ou C sejam escritas.

Lembre-se de que o DIP diz que "Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações".

Depois de determinar o que a classe Z precisa e a maneira como deseja obter o que precisa, você poderá preencher os detalhes. Claro, às vezes é necessário fazer alterações na classe Z, mas 99% das vezes não será esse o caso.

Nunca haverá uma classe D porque você descobriu que Z precisa de A, B e C antes de serem escritas. Uma mudança nos requisitos é uma história completamente diferente.

Stephen
fonte
5

A resposta curta é "quase nunca", mas existem, de fato, alguns lugares onde o DIP não faz sentido:

  1. Fábricas ou construtores, cujo trabalho é criar objetos. Estes são essencialmente os "nós das folhas" em um sistema que abraça totalmente a IoC. Em algum momento, algo precisa realmente criar seus objetos e não pode depender de mais nada para fazer isso. Em muitos idiomas, um contêiner de IoC pode fazer isso por você, mas às vezes você precisa fazer da maneira antiga.

  2. Implementações de estruturas de dados e algoritmos. Geralmente, nesses casos, as principais características que você otimiza (como tempo de execução e complexidade assintótica) dependem do uso de tipos de dados específicos. Se você estiver implementando uma tabela de hash, realmente precisará saber que está trabalhando com uma matriz para armazenamento, não uma lista vinculada, e apenas a própria tabela sabe como alocar adequadamente as matrizes. Você também não deseja passar em uma matriz mutável e fazer com que o chamador quebre sua tabela de hash mexendo no conteúdo.

  3. Classes de modelo de domínio . Eles implementam sua lógica de negócios e (na maioria das vezes) faz sentido ter uma implementação, porque (na maioria das vezes) você está apenas desenvolvendo o software para uma empresa. Embora algumas classes de modelo de domínio possam ser construídas usando outras classes de modelo de domínio, isso geralmente ocorrerá caso a caso. Como os objetos de modelo de domínio não incluem nenhuma funcionalidade que possa ser zombada de maneira útil, não há benefícios de testabilidade ou manutenção no DIP.

  4. Quaisquer objetos que são fornecidos como uma API externa e precisam criar outros objetos, cujos detalhes de implementação você não deseja expor publicamente. Isso se enquadra na categoria geral de "o design da biblioteca é diferente do design do aplicativo". Uma biblioteca ou estrutura pode fazer uso liberal de DI internamente, mas eventualmente terá que fazer algum trabalho real, caso contrário, não é uma biblioteca muito útil. Digamos que você esteja desenvolvendo uma biblioteca de rede; você realmente não deseja que o consumidor possa fornecer sua própria implementação de um soquete. Você pode usar internamente uma abstração de um soquete, mas a API que você expõe aos chamadores criará seus próprios soquetes.

  5. Testes unitários e duplos. Falsificações e tocos devem fazer uma coisa e fazê-lo simplesmente. Se você tem um falso que é complexo o suficiente para se preocupar em fazer ou não a injeção de dependência, provavelmente é muito complexo (talvez porque esteja implementando uma interface também muito complexa).

Pode haver mais; estes são os que eu vejo com certa frequência.

Aaronaught
fonte
Que tal "sempre que você estiver trabalhando em uma linguagem dinâmica"?
26715 Kevin
Não? Faço bastante trabalho em JavaScript e ainda se aplica igualmente bem lá. O "O" e o "I" no SOLID podem ficar um pouco embaçados.
Aaronaught 28/02
Huh ... Acho que os tipos de primeira classe do Python combinados com a tipagem do pato o tornam um pouco menos necessário.
28415 Kevin
O DIP não tem absolutamente nada a ver com o sistema de tipos. E de que maneira os "tipos de primeira classe" são exclusivos do python? Quando você deseja testar algo isoladamente, deve substituir o teste em dobro por suas dependências. Essas duplas de teste podem ser implementações alternativas de uma interface (em linguagens estaticamente tipadas) ou podem ser objetos anônimos ou tipos alternativos que possuam os mesmos métodos / funções (digitação de pato). Nos dois casos, você ainda precisa de uma maneira de substituir uma instância.
Aaronaught 28/02
1
O @Kevin python dificilmente foi o primeiro idioma a possuir digitação dinâmica ou zombarias soltas. Também é completamente irrelevante. A questão não é qual é o tipo de um objeto, mas como / onde esse objeto é criado. Quando um objeto cria suas próprias dependências, você é forçado a testar por unidade os detalhes da implementação, fazendo coisas horríveis como remover construtores de classes das quais a API pública não menciona. E esquecer os testes, misturar comportamento e construção de objetos simplesmente leva a um acoplamento rígido. A digitação com patos não resolve nenhum desses problemas.
#
2

Alguns sinais de que você pode estar aplicando o DIP em um nível muito micro, onde não está fornecendo valor:

  • você tem um par C / CImpl ou IC / C, com apenas uma única implementação dessa interface
  • as assinaturas em sua interface e implementação correspondem uma a uma (violando o princípio DRY)
  • você muda frequentemente C e CImpl ao mesmo tempo.
  • C é interno ao seu projeto e não é compartilhado fora do projeto como uma biblioteca.
  • você está frustrado com a F3 no Eclipse / F12 no Visual Studio, levando você à interface em vez da classe real

Se é isso que você está vendo, é melhor ter Z chamando C diretamente e pular a interface.

Além disso, não penso na decoração de métodos por uma estrutura de injeção de dependência / proxy dinâmico (Spring, Java EE) da mesma maneira que o verdadeiro SOLID DIP - isso é mais como um detalhe de implementação de como a decoração de métodos funciona nessa pilha de tecnologia. A comunidade Java EE considera uma melhoria que você não precisa de pares Foo / FooImpl como costumava fazer ( referência ). Por outro lado, o Python suporta decoração de funções como um recurso de linguagem de primeira classe.

Veja também esta postagem no blog .

wrschneider
fonte
0

Se você sempre inverter suas dependências, todas elas estarão de cabeça para baixo. O que significa que se você começou com um código confuso com um nó de dependências, é isso que você ainda tem (realmente), apenas invertido. É aí que você obtém o problema de que todas as alterações em uma implementação também precisam mudar sua interface.

O ponto de inversão de dependência é que você inverte seletivamente as dependências que estão deixando as coisas complicadas. Os que devem ir de A a B para C ainda o fazem, são os que estavam passando de C para A que agora passam de A para C.

O resultado deve ser um gráfico de dependência livre de ciclos - um DAG. Existem várias ferramentas que verificarão essa propriedade e desenharão o gráfico.

Para uma explicação mais completa, consulte este artigo :

A essência da aplicação correta do Princípio de Inversão de Dependência é esta:

Divida o código / serviço /… do qual você depende em uma interface e implementação. A interface reestrutura a dependência no jargão do código que a utiliza, a implementação a implementa em termos de suas técnicas subjacentes.

A implementação permanece onde está. Mas a interface agora tem uma função diferente (e usa um jargão / idioma diferente), descrevendo algo que o código em uso pode fazer. Mova-o para esse pacote. Ao não colocar a interface e a implementação no mesmo pacote, a dependência (direção da) é invertida de usuário → implementação para implementação → usuário.

soru
fonte