[Pesquisar este blog]

terça-feira, 23 de maio de 2017

Os Princípios SOLID::DIP (parte VI)

Com o objetivo orientar nas atividades de projeto e desenvolvimento de sistemas que possam ter o maior ciclo de vida possível, ou seja, sistemas concebidos de maneira a exibir tanto funcionamento adequado, como facilidades para sua própria manutenção, Robert Martin enunciou cinco princípios cujo acrônimo SOLID é bastante conhecido.
Estes cinco princípios são:
Single Responsability Principle
Open-Closed Principle
Liskov`s Substitution Principle
Interface Segregation Principle
Dependency Inversion Principle

Este post finaliza esta pequena série de artigos, abordando o DIP ou Princípio da inversão de dependência.

DIP::Dependency Inversion Principle

Dependa de abstrações. Não dependa de elementos concretos
Para Martin (1996), a aplicação rigorosa dos princípios Open-Closed (OCP) e da substituição de Liskov (LSP) podem ser generalizadas num outro princípio que denominou Princípio da Inversão de Dependência (Dependency Inversion Principle) ou apenas DIP.

O Princípio da Inversão de Dependência determina que um módulo de alto nível não deve depender de outros módulos de baixo nível. Aqui a ideia de nível se refere à abstração contida nestes módulos. Desta forma, um módulo com certo nível de abstração não deveria depender de outros, menos abstratos que ele próprio, ou seja, as abstrações nele contidas não devem depender de detalhes ou especificidades da implementação de outros módulos. Resumidamente, abstrações não devem depender de detalhes; enquanto os detalhes devem depender de abstrações.

Isso requer que, no projeto e implementação de qualquer módulo ou componente de software sejam utilizadas abstrações do próprio nível, assim sendo:
  • Interfaces devem depender de outras interfaces;
  • Classes concretas não devem ser adicionadas às assinaturas das interfaces;
  • Enquanto as interfaces podem (e devem) ser usadas na assinatura de métodos de quaisquer classes.

A questão aqui é que trabalhar com classes concretas leva, invariavelmente, a um acoplamento maior do que aquele obtido com o uso das interfaces que estariam presentes nestas mesmas classes. A programação dirigida a interfaces deseja, portanto: reduzir o acoplamento, exatamente como prescrito pelo OCP e pelo LSP; e tornar o código mais reusável, tanto por meio de sua extensão, como pela substituição de seus elementos.

Considere as duas interfaces Reader e Writer, dadas a seguir, que são abstrações de operações possíveis envolvendo a leitura e escrita de caracteres em algum dispositivo de entrada e saída (E/S) não específico.

public interface Reader {
  char getchar();
}
public interface Writer {
  void putchar(char c);
}

Considere também as realizações esquemáticas (implementações) das interfaces Reader e Writer, que poderiam constituir formas concretas das operações de leitura e escrita de caracteres determinadas por tais interfaces, mas agora em dispositivos de E/S específicos, como o teclado para entrada (leitura) e a impressora para saída (escrita).

public class Keyboard implements Reader {
  public char getchar()
  { /* código efetivo */ }
}

public class Printer implements Writer {
  public void putchar(char c)
  { /* código efetivo */ }
}

Qualquer programa que utilize instâncias diretamente obtidas das classes Keyboard e Printer acabam por estar acoplados ao código efetivo dos métodos getchar() e putchar() implementados nestas classes. Caso se deseje utilizar operações de leitura ou escrita diferentes, ou associadas a outros dispositivos periféricos, a substituição destas classes seria mais complexa, assim como sua modificação poderia criar problemas em outras partes do código que dependem de sua funcionalidade. O exemplo que segue, a classe ConcreteCharCopier, exibe tais fragilidades:

public class ConcreteCharCopier {
  public void copy(Keyboard reader, Printer writer)
  {
    int c;
    while ((c = reader.getchar()) != EOF) {
      writer.putchar();
    }
  }
}

Esta classe poderia ser usada assim:

Keyboard keyboard = new Keyboard(); // uma implementação de Reader
Printer printer = new Printer(); // uma implementação de Writer
// classe definida por meio de elementos concretos
ConcreteCharCopier ccc = new ConcreteCharCopier();
// uso da operação de cópia
ccc.copy(keyboard, printer);

Se for requerida uma mudança onde a leitura dos caracteres não é mais obtida do teclado, não é possível substituir o objeto correspondente ao parâmetro de tipo Keyboard, por outro de tipo diferente, gerando um impasse: se método copy(Keyboard, Printer) for alterado para que a entrada seja outra, por exemplo copy(Disk, Printer), todos os locais onde a versão original foi utilizada deverão ser alterados, o que provavelmente não será adequado. Outra possibilidade é a adição de uma segunda versão desta operação por meio da sobrecarga, mas a solução é temporária, pois implica que novas operações deverão ser adicionadas para cada novo dispositivo existente. Modificar a classe Keyboard para efetuar a leitura de outro periférico (o que é, por si só, uma heresia) leva ao mesmo impasse inicial.

Todos os problemas desta modificação estão associados ao simples fato de que a operação de cópia desejada é uma abstração que ficou dependente de elementos concretos.

Quando um programa depende das abstrações contidas em interfaces, tais como as ilustradas em Reader e Writer, passa a não depender de como tais abstrações são implementadas, ou seja, passa abstrair detalhes das implementações dos serviços dos quais depende.

Uma implementação adequada da operação de cópia poderia ser como na classe AbstractCharCopier, que segue:

public class AbstractCharCopier {
  public void copy(Reader reader, Writer writer)
  {
    int c;
    while ((c = reader.getchar()) != EOF) {
      writer.putchar();
    }
  }
}

A pequena mudança efetuada na maneira com que os objetos que implementam as funcionalidades de leitura e escrita de caracteres são passados para operação, faz toda a diferença. Agora a operação de cópia tem assinatura copy(Reader, Writer), de maneira que quaisquer dispositivos de entrada e de saída possam ser combinados e utilizados, sem requerer modificações neste método. Basta que os objetos supridos implementem as interfaces Reader e Writer, como no trecho que segue.

Keyboard keyboard = new Keyboard(); // uma implementação de Reader
Disk disk = new Disk(); // uma nova implementação de Reader
Printer printer = new Printer(); // uma implementação de Writer
// classe definida por meio de abstrações
AbstractCharCopier acc = new AbstractCharCopier();
// uso da operação de cópia
acc.copy(keyboard, printer);
acc.copy(disk, printer);

Note os princípios da substituição de Liskov (LSP) e do aberto-fechado (OCP) em ação: a substituição de um tipo (Keyboard) por outro (Disk) não provoca qualquer efeito colateral indesejado, pois ambas as classes implementam a mesma interface (que se comporta como um supertipo aqui). De modo análogo, os tipos existentes podem ser estendidos para suprir novas necessidades, sem a necessidade de alteração dos tipos existentes (no caso, Keyboard, Printer e, até mesmo, as interfaces Reader e Writer).

A ideia central deste princípio é isolar as classes por meio de uma ou mais camadas de interfaces, as quais definem as abstrações das quais tais classes dependem, separando-as dos detalhes de suas implementações, que podem então mudar livremente.

A figura abaixo ilustra o mecanismo preconizado por este princípio.


Isso reduz o acoplamento e facilita mudanças no projeto.

Considerações finais

Construir código modular, apesar de ser um bom e importante começo, não significa, necessariamente, ter um bom projeto.

Projetar bem, para o funcionamento do sistema e sua futura manutenção, inclui gerenciar todas as dependências dos componentes deste sistema, pois dependências não controladas podem comprometer, em definitivo, um projeto de software, no momento que alterações se fazem necessárias para correção de bugs ou atendimento de mudanças na especificação do sistema.

Os princípios SOLID são diretrizes valiosas para programadores e projetistas de software, pois:
- O código se torna mais e melhor testável (Test Driven Design – TDD, que não envolve apenas o teste do software, mas seu projeto);
- Devem ser seguidos sempre que possível (bom senso nunca é demais), trazendo os vários e substanciais ganhos comentados ao longo desta série de artigos.

Finalmente, o aprendizado contínuo e consistente é a única maneira para que, de fato, se possa buscar a excelência.

Para Saber Mais

  • LARMAN, Craig. Utilizando UML e padrões: uma introdução à análise e ao projeto orientados à objetos e ao Processo Unificado. Porto Alegre: Bookman, 2007.
  • MARTIN, R. C.; et. al. Clean Code: a handbook of agile software craftsmanship. Boston: Pearson Education, 2009.
  • MARTIN, R. C. The Clean Coder. Upper Sadle River: Prentice-Hall, 2011.
  • MARTIN, R. C. Principles of OOD. Disponível em http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod, recuperado em 17/02/2017.
  • MARTIN, R. C. Get a SOLID start. Disponível em http://objectmentor.com, recuperado em 17/02/2017.
  • PAGE-JONES, Meilir. Fundamentos do Desenho Orientado a Objetos. São Paulo: Makron Books, 2001.
  • SOMMERVILLE, I. Software Engineering. 9th. Ed. Boston: Addison-Wesley, 2011.