O princípio da inversão de dependência em Java

O princípio da inversão de dependência em Java

1. Overview

O Princípio de Inversão de Dependência (DIP) faz parte da coleção de princípios de programação orientada a objetos popularmente conhecidos comoSOLID.

Basicamente, o DIP é um paradigma de programação simples - mas poderoso - que podemos usarto implement well-structured, highly-decoupled, and reusable software components.

Neste tutorial,we’ll explore different approaches for implementing the DIP — one in Java 8, and one in Java 11 usandoJPMS (Java Platform Module System).

2. Injeção de dependência e inversão de controle não são implementações DIP

Em primeiro lugar, vamos fazer uma distinção fundamental para acertar o básico:the DIP is neither dependency injection (DI) nor inversion of control (IoC). Mesmo assim, todos eles trabalham muito bem juntos.

Simplificando, o DI é criar componentes de software para declarar explicitamente suas dependências ou colaboradores por meio de suas APIs, em vez de adquiri-los sozinhos.

Sem o DI, os componentes de software são fortemente acoplados entre si. Conseqüentemente, eles são difíceis de reutilizar, substituir, simular e testar, o que resulta em projetos rígidos.

With DI, the responsibility of providing the component dependencies and wiring object graphs is transferred from the components to the underlying injection framework. Dessa perspectiva, DI é apenas uma maneira de atingir IoC.

Por outro lado,IoC is a pattern in which the control of the flow of an application is reversed. Com as metodologias de programação tradicionais, nosso código personalizado tem o controle do fluxo de um aplicativo. Por outro lado,with IoC, the control is transferred to an external framework or container.

The framework is an extendable codebase, which defines hook points for plugging in our own code.

Por sua vez, a estrutura retorna nosso código por meio de uma ou mais subclasses especializadas, usando implementações de interfaces e através de anotações. The Spring framework é um bom exemplo desta última abordagem.

3. Fundamentos de DIP

Para entender a motivação por trás do DIP, vamos começar com sua definição formal, dada por Robert C. Martin em seu livro,Agile Software Development: Principles, Patterns, and Practices:

  1. Módulos de alto nível não devem depender de módulos de baixo nível. Ambos devem depender de abstrações.

  2. Abstrações não devem depender de detalhes. Os detalhes devem depender de abstrações.

Então, está claro que no núcleo,the DIP is about inverting the classic dependency between high-level and low-level components by abstracting away the interaction between them.

No desenvolvimento de software tradicional, os componentes de alto nível dependem dos de baixo nível. Assim, é difícil reutilizar os componentes de alto nível.

3.1. Opções de design e o DIP

Vamos considerar uma classeStringProcessor simples que obtém um valorString usando um componenteStringReader e o escreve em outro lugar usando um componenteStringWriter:

public class StringProcessor {

    private final StringReader stringReader;
    private final StringWriter stringWriter;

    public StringProcessor(StringReader stringReader, StringWriter stringWriter) {
        this.stringReader = stringReader;
        this.stringWriter = stringWriter;
    }

    public void printString() {
        stringWriter.write(stringReader.getValue());
    }
}

Embora a implementação da classeStringProcessor seja básica, existem várias opções de design que podemos fazer aqui.

Vamos dividir cada escolha de design em itens separados, para entender claramente como cada um pode impactar o design geral:

  1. StringReader and StringWriter, the low-level components, are concrete classes placed in the same package.StringProcessor, o componente de alto nível é colocado em um pacote diferente. StringProcessor depende deStringReadereStringWriter. Não há inversão de dependências, portanto,StringProcessor não pode ser reutilizado em um contexto diferente.

  2. StringReader and StringWriter are interfaces placed in the same package along with the implementations. StringProcessor agora depende de abstrações, mas os componentes de baixo nível não. Ainda não alcançamos a inversão de dependências.

  3. StringReader and StringWriter are interfaces placed in the same package together with StringProcessor. Agora,StringProcessor possui a propriedade explícita das abstrações. StringProcessor, StringReader,eStringWriter todos dependem de abstrações. Conseguimos a inversão das dependências de cima para baixo, abstraindo a interação entre os componentes.StringProcessor agora pode ser reutilizado em um contexto diferente.

  4. StringReader and StringWriter are interfaces placed in a separate package from StringProcessor. Conseguimos a inversão das dependências e também é mais fácil substituir as implementações deStringReadereStringWriter. StringProcessor também pode ser reutilizado em um contexto diferente.

De todos os cenários acima, apenas os itens 3 e 4 são implementações válidas do DIP.

3.2. Definindo a propriedade das abstrações

O item 3 é uma implementação direta do DIP,where the high-level component and the abstraction(s) are placed in the same package. Portanto,the high-level component owns the abstractions. Nesta implementação, o componente de alto nível é responsável por definir o protocolo abstrato pelo qual ele interage com os componentes de baixo nível.

Da mesma forma, o item 4 é uma implementação DIP mais dissociada. Nesta variante do padrão,neither the high-level component nor the low-level ones have the ownership of the abstractions.

As abstrações são colocadas em uma camada separada, o que facilita a troca dos componentes de baixo nível. Ao mesmo tempo, todos os componentes são isolados um do outro, o que gera um encapsulamento mais forte.

3.3. Escolhendo o nível certo de abstração

Na maioria dos casos, a escolha das abstrações que os componentes de alto nível usarão deve ser bastante direta, mas com uma ressalva a ser observada: o nível de abstração.

No exemplo acima, usamos DI para injetar um tipoStringReader na classeStringProcessor. Isso seriaas long as the level of abstraction of StringReader is close to the domain of StringProcessor eficaz.

Por outro lado, estaríamos simplesmente perdendo os benefícios intrínsecos do DIP seStringReader for, por exemplo, um objetoFile que lê um valorString de um arquivo. Nesse caso, o nível de abstração deStringReader seria muito menor que o nível do domínio deStringProcessor.

Simplificando,the level of abstraction that the high-level components will use to interoperate with the low-level ones should be always close to the domain of the former.

4. Implementações Java 8

Já analisamos em profundidade os principais conceitos do DIP, então agora vamos explorar algumas implementações práticas do padrão em Java 8.

4.1. Implementação DIP Direta

Vamos criar um aplicativo de demonstração que busca alguns clientes da camada de persistência e os processa de alguma forma adicional.

O armazenamento subjacente da camada geralmente é um banco de dados, mas para manter o código simples, usaremos aqui umMap simples.

Vamos começar pordefining the high-level component:

public class CustomerService {

    private final CustomerDao customerDao;

    // standard constructor / getter

    public Optional findById(int id) {
        return customerDao.findById(id);
    }

    public List findAll() {
        return customerDao.findAll();
    }
}

Como podemos ver, a classeCustomerService implementa os métodosfindById()efindAll(), que buscam clientes da camada de persistência usando uma implementação simples deDAO. Claro, poderíamos ter encapsulado mais funcionalidade na classe, mas vamos mantê-lo assim para simplificar.

Nesse caso,the CustomerDao type is the abstraction queCustomerService usa para consumir o componente de baixo nível.

Uma vez que esta é uma implementação DIP direta, vamos definir a abstração como uma interface no mesmo pacote deCustomerService:

public interface CustomerDao {

    Optional findById(int id);

    List findAll();

}

Ao colocar a abstração no mesmo pacote do componente de alto nível, tornamos o componente responsável por possuir a abstração. Este detalhe de implementação éwhat really inverts the dependency between the high-level component and the low-level one.

Além disso,the level of abstraction of CustomerDao is close to the one of*CustomerService*, que também é necessário para uma boa implementação de DIP.

Agora, vamos criar o componente de baixo nível em um pacote diferente. Neste caso, é apenas uma implementação básica deCustomerDao:

public class SimpleCustomerDao implements CustomerDao {

    // standard constructor / getter

    @Override
    public Optional findById(int id) {
        return Optional.ofNullable(customers.get(id));
    }

    @Override
    public List findAll() {
        return new ArrayList<>(customers.values());
    }
}

Finalmente, vamos criar um teste de unidade para verificar a funcionalidade da classeCustomerService:

@Before
public void setUpCustomerServiceInstance() {
    var customers = new HashMap();
    customers.put(1, new Customer("John"));
    customers.put(2, new Customer("Susan"));
    customerService = new CustomerService(new SimpleCustomerDao(customers));
}

@Test
public void givenCustomerServiceInstance_whenCalledFindById_thenCorrect() {
    assertThat(customerService.findById(1)).isInstanceOf(Optional.class);
}

@Test
public void givenCustomerServiceInstance_whenCalledFindAll_thenCorrect() {
    assertThat(customerService.findAll()).isInstanceOf(List.class);
}

@Test
public void givenCustomerServiceInstance_whenCalledFindByIdWithNullCustomer_thenCorrect() {
    var customers = new HashMap();
    customers.put(1, null);
    customerService = new CustomerService(new SimpleCustomerDao(customers));
    Customer customer = customerService.findById(1).orElseGet(() -> new Customer("Non-existing customer"));
    assertThat(customer.getName()).isEqualTo("Non-existing customer");
}

O teste de unidade exercita a APICustomerService. E também mostra como injetar manualmente a abstração no componente de alto nível. Na maioria dos casos, usaríamos algum tipo de contêiner DI ou estrutura para fazer isso.

Além disso, o diagrama a seguir mostra a estrutura do nosso aplicativo de demonstração, de uma perspectiva de pacote de alto nível para baixo:

image

4.2. Implementação Alternativa de DIP

Como discutimos antes, é possível usar uma implementação DIP alternativa, onde colocamos os componentes de alto nível, as abstrações e os de baixo nível em pacotes diferentes.

Por razões óbvias, essa variante é mais flexível, produz um melhor encapsulamento dos componentes e facilita a substituição dos componentes de baixo nível.

Claro, implementar esta variante do padrão se resume a apenas colocarCustomerService,MapCustomerDao,eCustomerDao em pacotes separados.

Portanto, um diagrama é suficiente para mostrar como cada componente é organizado com esta implementação:

image

5. Implementação modular Java 11

É bastante fácil refatorar nosso aplicativo de demonstração em um modular.

Essa é uma maneira muito boa de demonstrar como o JPMS aplica as melhores práticas de programação, incluindo forte encapsulamento, abstração e reutilização de componentes por meio do DIP.

Não precisamos reimplementar nossos componentes de amostra do zero. Portanto,modularizing our sample application is just a matter of placing each component file in a separate module, along with the corresponding module descriptor.

Esta é a aparência da estrutura modular do projeto:

project base directory (could be anything, like dipmodular)
|- com.example.dip.services
   module-info.java
     |- com
       |- example
         |- dip
           |- services
             CustomerService.java
|- com.example.dip.daos
   module-info.java
     |- com
       |- example
         |- dip
           |- daos
             CustomerDao.java
|- com.example.dip.daoimplementations
    module-info.java
      |- com
        |- example
          |- dip
            |- daoimplementations
              SimpleCustomerDao.java
|- com.example.dip.entities
    module-info.java
      |- com
        |- example
          |- dip
            |- entities
              Customer.java
|- com.example.dip.mainapp
    module-info.java
      |- com
        |- example
          |- dip
            |- mainapp
              MainApplication.java

5.1. O módulo de componente de alto nível

Vamos começar colocando a classeCustomerService em seu próprio módulo.

Vamos criar este módulo no diretório raizcom.example.dip.services,e adicionar o descritor do módulo,module-info.java:

module com.example.dip.services {
    requires com.example.dip.entities;
    requires com.example.dip.daos;
    uses com.example.dip.daos.CustomerDao;
    exports com.example.dip.services;
}

Por razões óbvias, não entraremos em detalhes sobre como funciona o JPMS. Mesmo assim, é claro ver as dependências do módulo apenas olhando para as diretivasrequires.

O detalhe mais relevante que vale a pena observar aqui é a diretivauses. Ele afirma quethe module is a client module que consome uma implementação da interfaceCustomerDao.

Claro, ainda precisamos colocar o componente de alto nível, a classeCustomerService, neste módulo. Portanto, dentro do diretório raizcom.example.dip.services, vamos criar a seguinte estrutura de diretório semelhante a um pacote:com/example/dip/services.

Finalmente, vamos colocar o arquivoCustomerService.java nesse diretório.

5.2. O Módulo de Abstração

Da mesma forma, precisamos colocar a interfaceCustomerDao em seu próprio módulo. Portanto, vamos criar o módulo no diretório raizcom.example.dip.daos e adicionar o descritor do módulo:

module com.example.dip.daos {
    requires com.example.dip.entities;
    exports com.example.dip.daos;
}

Agora, vamos navegar até o diretóriocom.example.dip.daos e criar a seguinte estrutura de diretório:com/example/dip/daos. Vamos colocar o arquivoCustomerDao.java nesse diretório.

5.3. O Módulo de Componente de Baixo Nível

Logicamente, precisamos colocar o componente de baixo nível,SimpleCustomerDao, em um módulo separado também. Como esperado, o processo se parece muito com o que acabamos de fazer com os outros módulos.

Vamos criar o novo módulo no diretório raizcom.example.dip.daoimplementations e incluir o descritor do módulo:

module com.example.dip.daoimplementations {
    requires com.example.dip.entities;
    requires com.example.dip.daos;
    provides com.example.dip.daos.CustomerDao with com.example.dip.daoimplementations.SimpleCustomerDao;
    exports com.example.dip.daoimplementations;
}

Em um contexto JPMS,this is a service provider module, uma vez que declara as diretivasprovidesewith.

Nesse caso, o módulo disponibiliza o serviçoCustomerDao para um ou mais módulos consumidores, por meio da implementação deSimpleCustomerDao.

Vamos ter em mente que nosso módulo de consumidor,com.example.dip.services, consome este serviço por meio da diretivauses.

Isso mostra claramentehow simple it is to have a direct DIP implementation with the JPMS, by just defining consumers, service providers, and abstractions in different modules.

Da mesma forma, precisamos colocar o arquivoSimpleCustomerDao.java neste novo módulo. Vamos navegar até o diretóriocom.example.dip.daoimplementations e criar uma nova estrutura de diretório semelhante a um pacote com este nome:com/example/dip/daoimplementations.

Finalmente, vamos colocar o arquivoSimpleCustomerDao.java no diretório.

5.4. O Módulo Entidade

Além disso, temos que criar outro módulo onde podemos colocar a classeCustomer.java. Como fizemos antes, vamos criar o diretório raizcom.example.dip.entities e incluir o descritor do módulo:

module com.example.dip.entities {
    exports com.example.dip.entities;
}

No diretório raiz do pacote, vamos criar o diretóriocom/example/dip/entitiese adicionar o seguinte arquivoCustomer.java:

public class Customer {

    private final String name;

    // standard constructor / getter / toString

}

5.5. O Módulo de Aplicação Principal

Em seguida, precisamos criar um módulo adicional que nos permite definir o ponto de entrada do nosso aplicativo de demonstração. Portanto, vamos criar outro diretório raizcom.example.dip.mainappe colocar nele o descritor do módulo:

module com.example.dip.mainapp {
    requires com.example.dip.entities;
    requires com.example.dip.daos;
    requires com.example.dip.daoimplementations;
    requires com.example.dip.services;
    exports com.example.dip.mainapp;
}

Agora, vamos navegar até o diretório raiz do módulo e criar a seguinte estrutura de diretório:com/example/dip/mainapp. Nesse diretório, vamos adicionar um arquivoMainApplication.java, que simplesmente implementa um métodomain():

public class MainApplication {

    public static void main(String args[]) {
        var customers = new HashMap();
        customers.put(1, new Customer("John"));
        customers.put(2, new Customer("Susan"));
        CustomerService customerService = new CustomerService(new SimpleCustomerDao(customers));
        customerService.findAll().forEach(System.out::println);
    }
}

Finalmente, vamos compilar e executar o aplicativo de demonstração - de dentro de nosso IDE ou de um console de comando.

Como esperado, devemos ver uma lista de objetosCustomer impressos no console quando o aplicativo é inicializado:

Customer{name=John}
Customer{name=Susan}

Além disso, o diagrama a seguir mostra as dependências de cada módulo do aplicativo:

image

6. Conclusão

Neste tutorial,we took a deep dive into the DIP’s key concepts, and we also showed different implementations of the pattern in Java 8 and Java 11, com o último usando o JPMS.

Todos os exemplos deJava 8 DIP implementationeJava 11 implementation estão disponíveis no GitHub.