Qual é o princípio de inversão de dependência e por que é importante?
Qual é o princípio de inversão de dependência e por que é importante?
Respostas:
Confira este documento: O princípio da inversão de dependência .
Diz basicamente:
Por que isso é importante, resumindo: as mudanças são arriscadas e, dependendo de um conceito em vez de de uma implementação, você reduz a necessidade de alterações nos sites de chamada.
Efetivamente, o DIP reduz o acoplamento entre diferentes partes do código. A idéia é que, embora existam muitas maneiras de implementar, digamos, uma instalação de registro, a maneira como você a utilizaria deve ser relativamente estável no tempo. Se você pode extrair uma interface que represente o conceito de log, essa interface deve ser muito mais estável no tempo do que sua implementação, e os sites de chamada devem ser muito menos afetados pelas alterações que você pode fazer enquanto mantém ou estende esse mecanismo de log.
Ao também fazer a implementação depender de uma interface, você tem a possibilidade de escolher, em tempo de execução, qual implementação é mais adequada para seu ambiente específico. Dependendo dos casos, isso pode ser interessante também.
Os livros Desenvolvimento Ágil de Software, Princípios, Padrões e Práticas e Princípios Ágeis, Padrões e Práticas em C # são os melhores recursos para entender completamente os objetivos e motivações originais por trás do Princípio da Inversão da Dependência. O artigo "O Princípio da Inversão da Dependência" também é um bom recurso, mas, devido ao fato de ser uma versão condensada de um rascunho que acabou entrando nos livros mencionados anteriormente, deixa de lado uma importante discussão sobre o conceito de propriedade de pacote e interface, que é essencial para distinguir esse princípio do conselho mais geral de "programar para uma interface, não para uma implementação" encontrada no livro Design Patterns (Gamma, et. al).
Para fornecer um resumo, o Princípio de Inversão de Dependência tem como principal objetivo reverter a direção convencional das dependências de componentes de "nível superior" para componentes de "nível inferior", de modo que os componentes de "nível inferior" dependam das interfaces pertencentes aos componentes de "nível superior" . (Nota: o componente "nível superior" aqui refere-se ao componente que requer dependências / serviços externos, não necessariamente sua posição conceitual dentro de uma arquitetura em camadas.) Ao fazer isso, o acoplamento não é reduzido tanto quanto é desviado dos componentes que são teoricamente menos valioso para componentes que são teoricamente mais valiosos.
Isso é obtido projetando componentes cujas dependências externas são expressas em termos de uma interface para a qual uma implementação deve ser fornecida pelo consumidor do componente. Em outras palavras, as interfaces definidas expressam o que é necessário para o componente, não como você o usa (por exemplo, "INeedSomething", não "IDoSomething").
O que o Princípio de Inversão de Dependência não se refere é a simples prática de abstrair dependências através do uso de interfaces (por exemplo, MyService → [ILogger ⇐ Logger]). Enquanto isso separa um componente dos detalhes de implementação específicos da dependência, ele não inverte o relacionamento entre o consumidor e a dependência (por exemplo, [MyService → IMyServiceLogger] ⇐ Logger.
A importância do Princípio da Inversão de Dependências pode ser reduzida a um objetivo singular de poder reutilizar componentes de software que dependem de dependências externas para uma parte de sua funcionalidade (registro, validação etc.)
Dentro desse objetivo geral de reutilização, podemos delinear dois subtipos de reutilização:
Usando um componente de software em vários aplicativos com implementações de subdependência (por exemplo, você desenvolveu um contêiner de DI e deseja fornecer log, mas não deseja acoplar seu contêiner a um logger específico, de modo que todos que usam seu contêiner também use a biblioteca de log escolhida).
Usando componentes de software em um contexto em evolução (por exemplo, você desenvolveu componentes de lógica de negócios que permanecem os mesmos em várias versões de um aplicativo em que os detalhes da implementação estão evoluindo).
Com o primeiro caso de reutilização de componentes em vários aplicativos, como em uma biblioteca de infraestrutura, o objetivo é fornecer uma necessidade básica de infraestrutura para seus consumidores sem acoplar seus consumidores a subdependências de sua própria biblioteca, uma vez que a dependência de tais dependências exige que você consumidores exigirem as mesmas dependências também. Isso pode ser problemático quando os consumidores da sua biblioteca optarem por usar uma biblioteca diferente para as mesmas necessidades de infraestrutura (por exemplo, NLog vs. log4net) ou se optarem por usar uma versão posterior da biblioteca necessária que não seja compatível com a versão anterior. exigido pela sua biblioteca.
Com o segundo caso de reutilização de componentes da lógica de negócios (ou seja, "componentes de nível superior"), o objetivo é isolar a implementação do domínio principal do seu aplicativo das necessidades em constante mudança dos detalhes da sua implementação (por exemplo, alterar / atualizar bibliotecas de persistência, bibliotecas de mensagens , estratégias de criptografia etc.). Idealmente, alterar os detalhes de implementação de um aplicativo não deve quebrar os componentes que encapsulam a lógica de negócios do aplicativo.
Nota: Alguns podem se opor a descrever esse segundo caso como reutilização real, raciocinando que componentes como componentes de lógica de negócios usados em um único aplicativo em evolução representam apenas um único uso. A idéia aqui, no entanto, é que cada alteração nos detalhes de implementação do aplicativo renderize um novo contexto e, portanto, um caso de uso diferente, embora os objetivos finais possam ser distinguidos como isolamento versus portabilidade.
Embora seguir o Princípio da inversão de dependência neste segundo caso possa oferecer algum benefício, observe-se que seu valor aplicado a linguagens modernas como Java e C # é muito reduzido, talvez a ponto de ser irrelevante. Como discutido anteriormente, o DIP envolve a separação completa dos detalhes da implementação em pacotes separados. No entanto, no caso de um aplicativo em evolução, a simples utilização de interfaces definidas em termos do domínio de negócios evitará a necessidade de modificar componentes de nível superior devido às necessidades variáveis dos componentes de detalhes da implementação, mesmo que os detalhes da implementação acabem residindo no mesmo pacote . Essa parte do princípio reflete aspectos que eram pertinentes à linguagem em vista quando o princípio foi codificado (ou seja, C ++) que não são relevantes para as linguagens mais recentes. Dito isto,
Uma discussão mais longa desse princípio, no que se refere ao uso simples de interfaces, injeção de dependência e o padrão de interface separada, pode ser encontrada aqui . Além disso, uma discussão sobre como o princípio se relaciona a linguagens de tipo dinâmico, como JavaScript, pode ser encontrada aqui .
Quando projetamos aplicativos de software, podemos considerar as classes de baixo nível, as classes que implementam operações básicas e primárias (acesso ao disco, protocolos de rede, ...) e as classes de alto nível, as classes que encapsulam lógica complexa (fluxos de negócios, ...).
Os últimos contam com classes de baixo nível. Uma maneira natural de implementar essas estruturas seria escrever classes de baixo nível e assim que as tivermos que escrever classes complexas de alto nível. Como as classes de alto nível são definidas em termos de outras, essa parece ser a maneira lógica de fazê-lo. Mas este não é um design flexível. O que acontece se precisarmos substituir uma classe de baixo nível?
O Princípio de Inversão de Dependência afirma que:
Esse princípio procura "inverter" a noção convencional de que os módulos de alto nível no software devem depender dos módulos de nível inferior. Aqui, os módulos de alto nível possuem a abstração (por exemplo, decidindo os métodos da interface) que são implementados pelos módulos de nível inferior. Tornando assim os módulos de nível inferior dependentes dos módulos de nível superior.
A inversão de dependência bem aplicada oferece flexibilidade e estabilidade no nível de toda a arquitetura do seu aplicativo. Isso permitirá que seu aplicativo evolua com mais segurança e estabilidade.
Tradicionalmente, uma interface do usuário de arquitetura em camadas dependia da camada de negócios e, por sua vez, da camada de acesso a dados.
Você precisa entender a camada, o pacote ou a biblioteca. Vamos ver como seria o código.
Teríamos uma biblioteca ou pacote para a camada de acesso a dados.
// DataAccessLayer.dll
public class ProductDAO {
}
E outra lógica de negócios da camada de biblioteca ou pacote que depende da camada de acesso a dados.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
A inversão de dependência indica o seguinte:
Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
Abstrações não devem depender de detalhes. Os detalhes devem depender de abstrações.
Quais são os módulos de alto nível e baixo nível? Pensando em módulos como bibliotecas ou pacotes, o módulo de alto nível seria aquele que tradicionalmente possui dependências e o baixo nível do qual eles dependem.
Em outras palavras, o nível alto do módulo seria onde a ação é invocada e o nível baixo, onde a ação é executada.
Uma conclusão razoável a partir desse princípio é que não deve haver dependência entre concreções, mas deve haver uma dependência de uma abstração. Mas, de acordo com a abordagem adotada, podemos aplicar mal a dependência da dependência de investimentos, mas uma abstração.
Imagine que adaptamos nosso código da seguinte maneira:
Teríamos uma biblioteca ou pacote para a camada de acesso a dados que define a abstração.
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
E outra lógica de negócios da camada de biblioteca ou pacote que depende da camada de acesso a dados.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
Embora dependamos de uma abstração, a dependência entre negócios e acesso a dados permanece a mesma.
Para obter inversão de dependência, a interface de persistência deve ser definida no módulo ou pacote em que essa lógica ou domínio de alto nível está e não no módulo de baixo nível.
Primeiro defina o que é a camada de domínio e a abstração de sua comunicação é definida como persistência.
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
Depois que a camada de persistência depende do domínio, é possível inverter agora se uma dependência for definida.
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(fonte: xurxodev.com )
É importante assimilar bem o conceito, aprofundando o objetivo e os benefícios. Se permanecermos mecanicamente e aprendermos o repositório de casos típico, não conseguiremos identificar onde podemos aplicar o princípio da dependência.
Mas por que invertemos uma dependência? Qual é o objetivo principal além de exemplos específicos?
Isso geralmente permite que as coisas mais estáveis, que não dependem de coisas menos estáveis, mudem com mais frequência.
É mais fácil alterar o tipo de persistência, o banco de dados ou a tecnologia para acessar o mesmo banco de dados do que a lógica do domínio ou as ações projetadas para se comunicar com persistência. Por esse motivo, a dependência é revertida, pois é mais fácil alterar a persistência se essa alteração ocorrer. Dessa forma, não precisaremos alterar o domínio. A camada de domínio é a mais estável de todas, e é por isso que não deve depender de nada.
Mas não há apenas este exemplo de repositório. Existem muitos cenários em que esse princípio se aplica e há arquiteturas baseadas nesse princípio.
Existem arquiteturas nas quais a inversão de dependência é essencial para sua definição. Em todos os domínios, é o mais importante e são as abstrações que indicam o protocolo de comunicação entre o domínio e o restante dos pacotes ou bibliotecas.
Na arquitetura Limpa, o domínio está localizado no centro e, se você olhar na direção das setas indicando dependência, fica claro quais são as camadas mais importantes e estáveis. As camadas externas são consideradas ferramentas instáveis, portanto, evite depender delas.
(fonte: 8thlight.com )
Isso acontece da mesma maneira com a arquitetura hexagonal, onde o domínio também está localizado na parte central e as portas são abstrações de comunicação do dominó para o exterior. Aqui, novamente, é evidente que o domínio é o mais estável e a dependência tradicional é invertida.
Para mim, o Princípio de Inversão de Dependência, conforme descrito no artigo oficial , é realmente uma tentativa equivocada de aumentar a reutilização de módulos que são inerentemente menos reutilizáveis, bem como uma maneira de solucionar um problema na linguagem C ++.
O problema no C ++ é que os arquivos de cabeçalho geralmente contêm declarações de campos e métodos particulares. Portanto, se um módulo C ++ de alto nível incluir o arquivo de cabeçalho para um módulo de baixo nível, isso dependerá dos detalhes reais da implementação desse módulo. E isso, obviamente, não é uma coisa boa. Mas isso não é um problema nas línguas mais modernas usadas hoje em dia.
Módulos de alto nível são inerentemente menos reutilizáveis que módulos de baixo nível, porque os primeiros são normalmente mais específicos de aplicativos / contexto do que os segundos. Por exemplo, um componente que implementa uma tela de interface do usuário é de nível mais alto e também muito (completamente?) Específico para o aplicativo. Tentar reutilizar esse componente em um aplicativo diferente é contraproducente e só pode levar ao excesso de engenharia.
Portanto, a criação de uma abstração separada no mesmo nível de um componente A que depende de um componente B (que não depende de A) pode ser feita apenas se o componente A for realmente útil para reutilização em diferentes aplicativos ou contextos. Se não for esse o caso, aplicar o DIP seria um design ruim.
Basicamente diz:
A classe deve depender de abstrações (por exemplo, interface, classes abstratas), não de detalhes específicos (implementações).
Boas respostas e bons exemplos já são dados por outras pessoas aqui.
A razão pela qual o DIP é importante é porque garante o princípio OO "design fracamente acoplado".
Os objetos no seu software NÃO devem entrar em uma hierarquia onde alguns são os de nível superior, dependentes de objetos de nível inferior. As alterações nos objetos de nível inferior serão propagadas para os objetos de nível superior, o que torna o software muito frágil para alterações.
Você deseja que seus objetos de 'nível superior' sejam muito estáveis e não sejam frágeis para alterações; portanto, é necessário inverter as dependências.
Uma maneira muito mais clara de declarar o Princípio da Inversão da Dependência é:
Seus módulos que encapsulam lógica de negócios complexa não devem depender diretamente de outros módulos que encapsulam a lógica de negócios. Em vez disso, eles devem depender apenas de interfaces para dados simples.
Ou seja, em vez de implementar sua classe Logic
como as pessoas costumam fazer:
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
você deve fazer algo como:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
e DataFromDependency
deve viver no mesmo módulo que Logic
, não com Dependency
.
Por que fazer isso?
Dependency
muda, você não precisa mudar Logic
.Logic
faz é uma tarefa muito mais simples: ela opera apenas no que parece um ADT.Logic
agora pode ser mais facilmente testado. Agora você pode instanciar diretamente Data
com dados falsos e transmiti-los. Não há necessidade de zombarias ou andaimes de teste complexos.DataFromDependency
, que referencia diretamente Dependency
, estiver no mesmo módulo que Logic
, então o Logic
módulo ainda depende diretamente do Dependency
módulo no momento da compilação. De acordo com a explicação do tio Bob sobre o princípio , evitar esse é o ponto principal do DIP. Em vez disso, para seguir o DIP, Data
deve estar no mesmo módulo que Logic
, mas DataFromDependency
deve estar no mesmo módulo que Dependency
.
Inversão de controle (IoC) é um padrão de design em que um objeto recebe sua dependência de uma estrutura externa, em vez de solicitar uma dependência de estrutura.
Exemplo de pseudocódigo usando a pesquisa tradicional:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
Código semelhante usando IoC:
class Service {
Database database;
init(database) {
this.database = database;
}
}
Os benefícios da IoC são:
O ponto de inversão de dependência é criar software reutilizável.
A idéia é que, em vez de dois códigos confiando um no outro, eles confiem em alguma interface abstraída. Depois, você pode reutilizar uma das partes sem a outra.
A maneira mais comum de conseguir isso é através de um contêiner de inversão de controle (IoC) como o Spring em Java. Nesse modelo, as propriedades dos objetos são configuradas por meio de uma configuração XML, em vez de os objetos saírem e encontrarem sua dependência.
Imagine esse pseudocódigo ...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass depende diretamente da classe Service e da classe ServiceLocator. Ele precisa dos dois se você quiser usá-lo em outro aplicativo. Agora imagine isso ...
public class MyClass
{
public IService myService;
}
Agora, o MyClass conta com uma única interface, a interface IService. Permitimos que o contêiner de IoC realmente definisse o valor dessa variável.
Portanto, agora, o MyClass pode ser facilmente reutilizado em outros projetos, sem trazer a dependência dessas outras duas classes.
Melhor ainda, você não precisa arrastar as dependências do MyService, as dependências dessas dependências e o ... bem, você entendeu.
Se considerarmos que um funcionário de "alto nível" de uma corporação é pago pela execução de seus planos e que esses planos são entregues pela execução agregada de muitos planos de funcionários de "baixo nível", poderíamos dizer geralmente é um plano terrível se a descrição do plano do funcionário de alto nível for associada de alguma forma ao plano específico de qualquer funcionário de nível inferior.
Se um executivo de alto nível tem um plano para "melhorar o tempo de entrega" e indica que um funcionário da linha de remessa deve tomar café e fazer alongamentos todas as manhãs, esse plano é altamente acoplado e tem baixa coesão. Mas se o plano não mencionar nenhum funcionário específico e, de fato, exigir apenas "uma entidade que pode executar o trabalho está preparada para trabalhar", então o plano será fracamente acoplado e mais coeso: os planos não se sobrepõem e podem ser facilmente substituídos . Os contratados, ou robôs, podem facilmente substituir os funcionários e o plano de alto nível permanece inalterado.
"Alto nível" no princípio de inversão de dependência significa "mais importante".
Eu posso ver que uma boa explicação foi dada nas respostas acima. No entanto, eu quero fornecer algumas explicações fáceis com um exemplo simples.
O Princípio de Inversão de Dependência permite que o programador remova as dependências codificadas permanentemente para que o aplicativo se torne fracamente acoplado e extensível.
Como conseguir isso: através da abstração
Sem inversão de dependência:
class Student {
private Address address;
public Student() {
this.address = new Address();
}
}
class Address{
private String perminentAddress;
private String currentAdrress;
public Address() {
}
}
No trecho de código acima, o objeto de endereço é codificado. Em vez disso, se podemos usar a inversão de dependência e injetar o objeto de endereço passando pelo método construtor ou setter. Vamos ver.
Com inversão de dependência:
class Student{
private Address address;
public Student(Address address) {
this.address = address;
}
//or
public void setAddress(Address address) {
this.address = address;
}
}
Inversão de dependência: depende de abstrações, não de concreções.
Inversão de controle: Principal vs Abstração e como o Principal é a cola dos sistemas.
Estas são algumas boas postagens falando sobre isso:
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
Digamos que temos duas classes: Engineer
e Programmer
:
O engenheiro de classe depende da classe Programmer, como abaixo:
class Engineer () {
fun startWork(programmer: Programmer){
programmer.work()
}
}
class Programmer {
fun work(){
//TODO Do some work here!
}
}
Neste exemplo, a classe Engineer
depende da nossa Programmer
classe. O que acontecerá se eu precisar alterar o Programmer
?
Obviamente, eu preciso mudar Engineer
também. (Uau, neste momento também OCP
é violado)
Então, o que temos para limpar essa bagunça? A resposta é abstração, na verdade. Por abstração, podemos remover a dependência entre essas duas classes. Por exemplo, eu posso criar um Interface
para a classe Programmer e, a partir de agora, toda classe que deseja usar Programmer
tem que usar a sua Interface
. Em seguida, alterando a classe Programmer, não precisamos alterar nenhuma classe que a utilizou. usava.
Nota: DependencyInjection
pode nos ajudar a fazer DIP
e SRP
também.
Além da enxurrada de respostas geralmente boas, gostaria de adicionar uma pequena amostra para demonstrar boas práticas versus práticas ruins. E sim, eu não sou de jogar pedras!
Digamos, você quer um pequeno programa para converter uma string no formato base64 via E / S do console. Aqui está a abordagem ingênua:
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-level I/O functionality.
* Hence, you'll have to fiddle with BadEncoder whenever you want to change
* the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
* problems with I/O shouldn't break the encoder!
*/
BadEncoder.Run();
}
}
public static class BadEncoder
{
public static void Run()
{
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
}
}
O DIP basicamente diz que os componentes de alto nível não devem depender da implementação de baixo nível, onde "nível" é a distância da E / S, de acordo com Robert C. Martin ("Arquitetura limpa"). Mas como você sai dessa situação difícil? Simplesmente tornando o codificador central dependente apenas de interfaces, sem se preocupar em como elas são implementadas:
class Program
{
static void Main(string[] args)
{
/* Demo of the Dependency Inversion Principle (= "High-level functionality
* should not depend upon low-level implementations"):
* You can easily implement new I/O methods like
* ConsoleReader, ConsoleWriter without ever touching the high-level
* Encoder class!!!
*/
GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter()); }
}
public static class GoodEncoder
{
public static void Run(IReadable input, IWriteable output)
{
output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
}
}
public interface IReadable
{
string ReadInput();
}
public interface IWriteable
{
void WriteOutput(string txt);
}
public class ConsoleReader : IReadable
{
public string ReadInput()
{
return Console.ReadLine();
}
}
public class ConsoleWriter : IWriteable
{
public void WriteOutput(string txt)
{
Console.WriteLine(txt);
}
}
Observe que você não precisa tocar GoodEncoder
para alterar o modo de E / S - essa classe está satisfeita com as interfaces de E / S que conhece; qualquer implementação de baixo nível IReadable
e IWriteable
nunca a incomodará.
GoodEncoder
seu segundo exemplo. Para criar um exemplo DIP, é necessário introduzir uma noção do que "possui" as interfaces que você extraiu aqui - e, em particular, colocá-las no mesmo pacote que o GoodEncoder enquanto suas implementações permanecem fora.
Princípio de Inversão de Dependência (DIP) diz que
i) Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.
ii) As abstrações nunca devem depender de detalhes. Os detalhes devem depender de abstrações.
Exemplo:
public interface ICustomer
{
string GetCustomerNameById(int id);
}
public class Customer : ICustomer
{
//ctor
public Customer(){}
public string GetCustomerNameById(int id)
{
return "Dummy Customer Name";
}
}
public class CustomerFactory
{
public static ICustomer GetCustomerData()
{
return new Customer();
}
}
public class CustomerBLL
{
ICustomer _customer;
public CustomerBLL()
{
_customer = CustomerFactory.GetCustomerData();
}
public string GetCustomerNameById(int id)
{
return _customer.GetCustomerNameById(id);
}
}
public class Program
{
static void Main()
{
CustomerBLL customerBLL = new CustomerBLL();
int customerId = 25;
string customerName = customerBLL.GetCustomerNameById(customerId);
Console.WriteLine(customerName);
Console.ReadKey();
}
}
Nota: A classe deve depender de abstrações como interface ou classes abstratas, não de detalhes específicos (implementação da interface).