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 A tiver um método, que espera um ponteiro para um objeto do tipo class B , esse método deve, na verdade, esperar um objeto do tipo abstract base class of B . Isso ajuda também no Open / Close.

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

A razão pela qual eu sou cético é porque estamos pagando algum preço por seguir esse princípio. Digamos que eu precise implementar o recurso Z . Após a análise, concluo que o recurso Z consiste na funcionalidade A , B e C . Eu crio uma classe fachada Z , que, por meio de interfaces, usa as classes A , B e C . Começo a codificar a implementação e, em algum momento, percebo que a tarefa Z consiste na funcionalidade 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, somente a classe precisaria ser substituída.

Em outras palavras, para mudar alguma coisa, eu preciso mudar 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 somente a implementação.

    
por Vorac 25.02.2015 / 19:26
fonte

7 respostas

86

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

Princípios sólidos aplicados ao máximo são mais adequados para sistemas empresariais 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 as coisas não será um benefício.

A passagem de interfaces, em vez de classes concretas, às vezes significa, por exemplo, que você pode facilmente trocar a leitura de um arquivo por um fluxo de rede. No entanto, para uma grande quantidade de projetos de software, esse tipo de flexibilidade simplesmente não é necessário, e você pode simplesmente passar classes de arquivos concretas e chamá-las por dia e poupar suas células cerebrais .

Parte da arte do desenvolvimento de software é ter uma boa noção do que provavelmente mudará 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, use o YAGNI e apenas passe tipos de concreto, esqueça as classes de fábrica, esqueça toda a conexão de tempo de execução e configuração, etc, e esqueça muitas das abstrações do SOLID. Na minha experiência, a abordagem YAGNI provou ser correta muito mais vezes do que não é.

    
por 25.02.2015 / 20:27
fonte
11

Nas palavras do leigo:

Aplicar o DIP é fácil e divertido . Não receber o desenho logo na primeira tentativa não é razão suficiente para desistir completamente do DIP.

  • Geralmente os IDEs ajudam você a fazer esse tipo de refatoração, alguns até permitem extrair a interface de uma classe já implementada
  • É quase impossível acertar o design na primeira vez
  • O fluxo de trabalho normal envolve mudar e repensar as interfaces nos primeiros estágios de desenvolvimento
  • À medida que o desenvolvimento evolui, ele amadurece e você terá menos razões 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 aproveitar os benefícios, pois seu aplicativo está aberto para expansão.

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

Algumas pessoas dizem que isso aumenta a complexidade, mas acho que o opossite é verdadeiro. Mesmo para pequenos projetos. Isso torna o teste / simulação mais fácil. Isso faz com que seu código tenha menos, se houver, case declarações ou aninhado ifs . Reduz a complexidade ciclomática e faz você pensar de maneiras novas. Torna a programação mais parecida com o design e a fabricação do mundo real.

    
por 25.02.2015 / 19:31
fonte
9

Use inversão de dependências onde isso faz sentido.

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

Se você tem um grupo de objetos usados internamente em um módulo que não é exposto a outros módulos ou reutilizado em qualquer lugar, provavelmente não vale a pena usar o DI.

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

  1. Nos módulos projetados para extensão. Se todo o propósito de um módulo é estendê-lo e mudar o comportamento, faz todo o sentido associar o DI desde o início.

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

As chaves aqui são usadas onde são necessárias porque introduzirão complexidade extra, e certifique-se de medir essa necessidade através de requisitos técnicos (ponto um) ou revisão de código quantitativo (ponto dois).

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

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

    
por 25.02.2015 / 19:58
fonte
5

Parece-me que a questão original está faltando parte do ponto do DIP.

The reason I am skeptical is because we are paying some price for following this principle. Say, I need to implement feature Z. After analysis, I conclude that feature Z consists of functionality A, B and C. I create a fascade class Z, that, through interfaces, uses classes A, B and C. I begin coding the implementation and at some point I realize that task Z actually consists of functionality A, B and D. Now I need to scrap the C interface, the C class prototype and write separate D interface and class. Without interfaces, only the class would wave needed to be replaced.

Para realmente tirar proveito do DIP, você criaria a classe Z primeiro e a chamaria da funcionalidade das classes A, B e C (que ainda não foram desenvolvidas). Isto dá-lhe a API para as classes A, B e C. Então você vai e cria as classes A, B e C e preenche os detalhes. Você deve efetivamente 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 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 saber o que a classe Z precisa e a maneira como ela deseja obter o que precisa, você pode preencher os detalhes. Claro, algumas vezes as mudanças precisam ser feitas na classe Z, mas 99% do tempo não será o caso.

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

    
por 26.02.2015 / 02:02
fonte
5

A resposta curta é "quase nunca", mas há, 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 abrange totalmente o 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 IoC pode fazer isso para você, mas às vezes é necessário fazê-lo à moda antiga.

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

  3. Classes de modelo de domínio . Eles implementam sua lógica de negócios e (na maior parte do tempo) só faz sentido ter uma implementação, porque (na maior parte do tempo) 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, geralmente isso será feito caso a caso. Como os objetos de modelo de domínio não incluem nenhuma funcionalidade que possa ser ridicularizada, não há benefício de testabilidade ou capacidade de manutenção para o DIP.

  4. Todos os 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 "design de biblioteca é diferente do design do aplicativo". Uma biblioteca ou framework 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 quer não querer que o consumidor seja capaz de 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. Teste de unidade e teste duplas. Fakes e stubs devem fazer uma coisa e fazer isso 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 que também é complexa demais).

Pode haver mais; estes são os que eu vejo em uma base um pouco frequente.

    
por 26.02.2015 / 06:20
fonte
2

Alguns sinais de que você pode estar aplicando o DIP em um nível muito pequeno, em que 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 de um para um (violando o princípio DRY)
  • você freqüentemente altera 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 F3 no Eclipse / F12 no Visual Studio levando você para a interface em vez da classe real

Se isso é o que você está vendo, talvez seja melhor ter apenas Z chamado C diretamente e pular a interface.

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

Veja também esta postagem do blog .

    
por 04.11.2015 / 22:48
fonte
0

Se você sempre inverter suas dependências, todas as suas dependências 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, é o que você ainda tem (realmente), apenas invertido. Que é onde você obtém o problema de que cada alteração em uma implementação também precisa alterar sua interface.

O ponto de inversão de dependência é que você seletivamente inverte as dependências que estão tornando as coisas emaranhadas. Os que deveriam ir de A para B para C ainda o fazem, os que iam de C para A que agora vão de A para C.

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

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

The essence of applying the Dependency Inversion Principle correctly is this:

Split the code/service/… you depend on into an interface and implementation. The interface restructures the dependency in the jargon of the code using it, the implementation implements it in terms of its underlying techniques.

The implementation remains where it is. But the interface has a different function now (and uses a different jargon/language), describing something the using code can do. Move it to that package. By not placing the interface and implementation in the same package, the (direction of the) dependency is inverted from user→implementation to implementation→user.

    
por 25.02.2015 / 23:32
fonte