Considere que, em um projeto interno da nossa empresa, temos a seguinte classe:
public class EmissorNotaFiscal {
private RegrasDeTributacao tributacao;
private LegislacaoFiscal legislacao;
private NotaFiscalDAO notas;
private EnviadorEmail email;
private EnviadorSMS sms;
//...
public NotaFiscal gera(Fatura fatura) {
List<Imposto> impostos = tributacao.verifica(fatura);
List<Isencao> isencoes = legislacao.analisa(fatura);
NotaFiscal nota = aplica(impostos, isencoes); // método auxiliar
notas.salva(nota);
email.envia(nota);
sms.envia(nota);
return nota;
}
}
Esse exemplo é baseado no livro OO e SOLID para Ninjas (ANICHE, 2015).
Perceba que a classe EmissorNotaFiscal
tem como dependências as classes:
RegrasDeTributacao
LegislacaoFiscal
NotaFiscalDAO
EnviadorEmail
EnviadorSMS
Cada uma dessas classes tem uma responsabilidade bem definida, só um motivo para serem modificadas e, portanto, seguem o SRP.
Mas será que o design do código está bom o bastante?
Há um problema claro: a classe EmissorNotaFiscal
tem muitas dependências.
Uma classe com muitas dependências tem acoplamento com muitas outras classes.
Além disso, o código acaba acoplado também às dependências das dependências e assim por diante. A classe EmissorNotaFiscal
depende indiretamente:
- do Hibernate por meio de
NotaFiscalDAO
- da API do SendGrid por meio de
EnviadorEmail
- da API REST do Twilio por meio de
EnviadorSMS
Mudanças nas dependências, ou nas dependências das dependências, podem acabar se propagando para a classe que as usa.
A flexibilidade e o reuso do código ficam prejudicados.
Acoplamento precisa existir. Uma classe totalmente desacoplada é uma classe inútil.
Só existe uma maneira de evitarmos totalmente o acoplamento: colocar todo o código, incluindo o de bibliotecas, numa mesma classe. Se tudo estiver junto, não há a necessidade de depender de nada externo. Mas isso levaria a uma quebra do SRP, a uma baixíssima coesão e a um pesadelo de manutenção!
Mas será que todo acoplamento é ruim? Não!
Num código Java, desde o primeiro OlaMundo
, dependemos de String
e de System
. Esse acoplamento não chega a ser problemático, não é mesmo? As classes do pacote java.lang
são estáveis: mudam muito pouco. E têm essa característica talvez porque são milhões de projetos que as usam e mudá-las teria um impacto gigantesco. Podemos depender delas tranquilamente. É o acoplamento bom.
Vamos comparar com as dependências da classe EmissorNotaFiscal
. Qual é a chance, por exemplo, das classes RegrasDeTributacao
ou EnviadorSMS
mudarem? É grande! Essas classes são voláteis. Depender de classes voláteis é o acoplamento ruim.
Volátil
FIG Pouco firme; inconstante, mudável, volúvel.
Como minimizar os impactos de mudanças em dependências voláteis?
Usando abstrações! Podemos usar classes abstratas e, preferencialmente, interfaces.
Abstrações são estáveis: mudam muito menos que implementações.
Ao usarmos classes abstratas ou interfaces, o código não depende mais diretamente da dependência volátil e sim da abstração. E a dependência volátil, por sua vez, também depende da abstração, implementando-a. Por isso, podemos dizer que a dependência é invertida.
Porém, em Runtime, as chamadas são realizadas em uma implementação concreta.
Por exemplo, a dependência de EmissorNotaFiscal
à classe EnviadorSMS
poderia ser invertida tendo uma interface como intermediária.
É comum, em projetos Java, colocar o sufixo Impl
na classe de implementação. Então, renomearíamos a classe para EnviadorSMSImpl
e teríamos a interface EnviadorSMS
. Depois discutiremos se o uso de Impl
é uma boa prática ou não.
No artigo Design Principles and Design Patterns (MARTIN, 2000), Uncle Bob declara que devemos:
Depender de abstrações e não de implementações.
Essa ideia é uma velha conhecida:
Programe voltado à interface, não à implementação.
GoF (Gamma & Helm & Johnson & Vlissides) no livro Design Patterns (GAMMA et al., 1994)
Quando desenvolvemos uma aplicação, a parte mais importante do nosso código é a que implementa as regras de negócio.
Porém, grande parcela do código não está relacionada ao negócio e sim a coisas mais técnicas, detalhes de implementação, como UI Web e/ou Mobile, persistência e BD, integração com outros sistemas, frameworks, protocolos, etc.
Por isso, em suas palestras, Uncle Bob costuma a dizer que:
- A Web é um detalhe
- O BD é um detalhe
Deveríamos preparar o nosso código de negócio para "sobreviver" a mudanças completas nesses detalhes. Mas sem esses detalhes, não conseguiríamos expor uma UI ou persistir os dados.
Os detalhes técnicos são importantes, são os mecanismos de entrega das regras de negócio para os usuários.
Na verdade, em seus artigos e livros, Uncle Bob também usa o termo policy (algo como diretriz ou procedimento) para se referir às regras de negócio e outros tipos de procedimentos relacionados ao problema que está sendo resolvido.
Para quem desenvolve o Hibernate, ler um mapeamento ou gerar código SQL pode ser considerado policy. Variações entre os bancos de dados, como AUTO_INCREMENT
ou IDENTITY
na geração de PKs, seriam detalhes.
Uncle Bob, no livro Agile Principles, Patterns, and Practices in C# (MARTIN, 2006), divide o código em de alto nível e de baixo nível.
-
Código de alto nível seria o código que implementa regras de negócio.
-
Código de baixo nível seriam os mecanismos de entrega, os detalhes de implementação mais técnicos.
Das dependências da classe EmissorNotaFiscal
, poderíamos classificar como de alto nível:
RegrasDeTributacao
LegislacaoFiscal
Já as dependências de baixo nível de EmissorNotaFiscal
seriam:
NotaFiscalDAO
EnviadorEmail
EnviadorSMS
Uncle Bob diz, no livro Clean Architecture (MARTIN, 2017), que código de alto nível é aquele mais distante das entradas ou saídas do sistema e, por isso, muda menos frequentemente e por razões mais importantes, relacionadas ao negócio.
Já o código de baixo nível, mais próximo das entradas ou saídas, muda mais frequentemente e com mais urgência.
Pensando numa boa maneira de gerenciar as dependências no nosso código, Uncle Bob definiu o Dependency Inversion Principle (DIP), o Princípio da Inversão de Dependências:
Dependency Inversion Principle (DIP)
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. Detalhes devem depender de abstrações.
Se seguirmos o DIP à risca, tanto o código de alto nível como o código de baixo nível deveria depender de abstrações. Mas, muitas vezes, essa regra é relaxada, para simplificar o código.
Em outras palavras, regras de negócio não devem depender de mecanismos de entrega, mas de abstrações desses detalhes de implementação.
Em relação às dependências da classe EmissorNotaFiscal
, para seguir o DIP, deveríamos criar abstrações para as dependências de baixo nível. Para isso, teríamos que definir:
- a interface
NotaFiscalDAO
como abstração deNotaFiscalDAOImpl
, implementação que depende do Hibernate - a interface
EnviadorEmail
como abstração deEnviadorEmailImpl
, implementação que depende da API do SendGrid - a interface
EnviadorSMS
como abstração deEnviadorSMSImpl
, implementação que depende da API do Twilio
Se você está programando alguma classe qualquer com regras de negócio, e precisa depender de outro módulo, idealmente esse outro módulo deve ser uma abstração.
Maurício Aniche, no livro OO e SOLID para Ninjas (ANICHE, 2015)
O que é um módulo nessa definição do DIP?
No livro Clean Architecture (MARTIN, 2017), Uncle Bob define um módulo como um conjunto coeso de funções e dados ou, de maneira mais simples, um arquivo de código fonte. É um conceito que tenta abranger tanto linguagens OO como estruturadas e funcionais.
Mais adiante no curso, veremos um outro conceito para módulos, mais preciso e específico para a linguagem Java.
As APIs da plataforma Java são repletas de interfaces.
Pense em java.sql.Connection
do JDBC ou javax.jms.Destination
do JMS.
São interfaces altamente estáveis: a probabilidade de serem alteradas é bem baixa.
Será que um código de regra de negócio que depende de algumas dessas interfaces segue o DIP?
Não! Uma abstração de alto nível é descrita em termos de negócio. APIs como o JDBC ou o JMS, mesmo estáveis, são de baixo nível porque são detalhes técnicos, mecanismos de entrega.
Pense em uma arquitetura em 3 camadas: Apresentação depende de Negócio que depende de Persistência.
Essa arquitetura NÃO atende ao DIP: código de alto nível (Negócio) depende de código de baixo nível (Persistência).
Para que atenda, teríamos que inserir abstrações na camada de Negócio para inverter as dependências, fazendo com que Persistência dependa de Negócio e não o contrário.
Como mencionamos anteriormente, para seguir de maneira estrita o DIP, deveríamos fazer com que a camada de Negócio (alto nível) também forneça abstrações para a camada de Apresentação (baixo nível), invertendo também essa dependência. Porém, é comum mantermos uma dependência direta do baixo nível ao alto nível, para que o código fique simplificado.
No livro Clean Architecture (MARTIN, 2017), Uncle Bob define o que chama de regra da dependência, que está relacionada ao DIP:
Dependências devem apontar apenas para dentro, em direção às regras de negócio.
Analisando o Cotuba, depois das refatorações que o levaram em direção ao SRP, podemos classificar os níveis do código como:
- alto nível: as classes
Cotuba
,Ebook
eCapitulo
, que são relativas ao domínio do problema - baixo nível: as classes relativas a UI,
Main
eLeitorOpcoesCLI
, e as classes que implementam detalhes técnicos,RenderizadorMDParaHTML
,GeradorPDF
eGeradorEPUB
Será que o código do Cotuba fere o DIP?
A classe Capitulo
não depende de nada além de String
, uma dependência muito estável. Não a consideramos de baixo nível. Tudo OK.
A classe Ebook
depende de List
e ArrayList
, que são classes bem estáveis e que também não são de baixo nível. Depende também de Path
, da API NIO, que é uma dependência razoavelmente estável e não é de baixo nível. Sem problemas.
Porém, a classe Cotuba
, uma classe de alto nível, depende de várias classes de baixo nível, que são mecanismos de entrega, como RenderizadorMDParaHTML
, GeradorPDF
e GeradorEPUB
.
Precisamos inverter essas dependências!
Coloque o sufixo Impl
nas classes das dependências de baixo nível da classe Cotuba
.
Crie interfaces para essas implementações, fazendo com que Cotuba
dependa dessas interfaces.
- Renomeie as seguintes dependências de
Cotuba
:
- de
RenderizadorMDParaHTML
paraRenderizadorMDParaHTMLImpl
- de
GeradorPDF
paraGeradorPDFImpl
- de
GeradorEPUB
paraGeradorEPUBImpl
- Extraia as interfaces
RenderizadorMDParaHTML
,GeradorPDF
eGeradorEPUB
das respectivas implementações.
Dica: use o menu Refactor > Extract Interface.... Deixem marcadas as opções Use the extracted interface type where possible e Generate '@Override' annotations. Desmarque as demais opções. Não esqueça de selecionar o método a ser declarado na interface e clique em OK.
As classes devem ficar da seguinte maneira, depois dessas refatorações:
####### cotuba.md.RenderizadorMDParaHTML
package cotuba.md;
import java.nio.file.Path;
import java.util.List;
import cotuba.domain.Capitulo;
public interface RenderizadorMDParaHTML {
List<Capitulo> renderiza(Path diretorioDosMD);
}
####### cotuba.md.RenderizadorMDParaHTMLImpl
package cotuba.md;
// imports omitido...
public class RenderizadorMDParaHTMLImpl implements RenderizadorMDParaHTML { // modificado
@Override //inserido
public List<Capitulo> renderiza(Path diretorioDosMD) {
// código omitido...
}
}
####### cotuba.pdf.GeradorPDF
package cotuba.pdf;
import cotuba.domain.Ebook;
public interface GeradorPDF {
void gera(Ebook ebook);
}
####### cotuba.pdf.GeradorPDFImpl
package cotuba.pdf;
// imports omitido...
public class GeradorPDFImpl implements GeradorPDF { // modificado
@Override //inserido
public void gera(Ebook ebook) {
// código omitido...
}
}
####### cotuba.epub.GeradorEPUB
package cotuba.epub;
import cotuba.domain.Ebook;
public interface GeradorEPUB {
void gera(Ebook ebook);
}
####### cotuba.epub.GeradorEPUBImpl
package cotuba.epub;
// imports omitido...
public class GeradorEPUBImpl implements GeradorEPUB { // modificado
@Override //inserido
public void gera(Ebook ebook) {
// código omitido...
}
}
- Na classe
Cotuba
, devemos depender o máximo possível das interfaces.
Se você marcou a opção Use the extracted interface type where possible, da refatoração Extract Interface, isso já deve ter sido feito.
package cotuba.application;
import java.nio.file.Path;
import java.util.List;
import cotuba.domain.Capitulo;
import cotuba.domain.Ebook;
import cotuba.epub.GeradorEPUB; // inserido
import cotuba.epub.GeradorEPUBImpl;
import cotuba.md.RenderizadorMDParaHTML; // inserido
import cotuba.md.RenderizadorMDParaHTMLImpl;
import cotuba.pdf.GeradorPDF; // inserido
import cotuba.pdf.GeradorPDFImpl;
public class Cotuba {
public void executa(String formato, Path diretorioDosMD, Path arquivoDeSaida) {
RenderizadorMDParaHTML renderizador = new RenderizadorMDParaHTMLImpl(); // modificado
List<Capitulo> capitulos = renderizador.renderiza(diretorioDosMD);
Ebook ebook = new Ebook();
ebook.setFormato(formato);
ebook.setArquivoDeSaida(arquivoDeSaida);
ebook.setCapitulos(capitulos);
if ("pdf".equals(formato)) {
GeradorPDF geradorPDF = new GeradorPDFImpl(); // modificado
geradorPDF.gera(ebook);
} else if ("epub".equals(formato)) {
GeradorEPUB geradorEPUB = new GeradorEPUBImpl(); // modificado
geradorEPUB.gera(ebook);
} else {
throw new RuntimeException("Formato do ebook inválido: " + formato);
}
}
}
Repare na seguinte linha da classe Cotuba
:
RenderizadorMDParaHTML renderizador = new RenderizadorMDParaHTMLImpl();
Depender da abstração oferecida pela interface RenderizadorMDParaHTML
é exatamente o que queremos.
Porém, ao instanciar RenderizadorMDParaHTMLImpl
, estamos dependendo da implementação dessa abstração.
Isso é uma violação do DIP: além da abstração, um código de alto nível acabou dependendo diretamente de uma classe de baixo nível.
Um dos lugares mais comuns em que um design de código depende de classes concretas é ao criar instâncias. Por definição, não é possível instanciar abstrações. Portanto, para criar instâncias, é preciso depender de classes concretas.
Uncle Bob, no artigo Design Principles and Design Patterns (MARTIN, 2000) - Tradução livre
Instanciar objetos é um problema comum em OO:
-
há um capítulo do livro Design Patterns (GAMMA et al., 1994) que cataloga diversos Creational Patterns, soluções para a criação de objetos: Abstract Factory, Builder, Factory Method, Prototype e Singleton.
-
Creator, um tipo de objeto que encapsula a criação de outros objetos, é uma das responsabilidades recorrentes descritas por Craig Larman no livro Applying UML and Patterns (LARMAN, 2004) nos princípios GRASP.
Larman diz que uma solução extremamente difundida para a criação de objetos é uma simplificação da Abstract Factory, que o autor chama de Simple Factory, Concrete Factory ou simplesmente Factory.
Uma Factory tem várias vantagens:
- separa a responsabilidade da criação de objetos complexos em objetos auxiliares coesos.
- esconde lógica de instanciação potencialmente complexa.
- permite a introdução de estratégias de gerenciamento de memória que melhoram o desempenho, como cache ou reciclagem de objetos.
Craig Larman no livro Applying UML and Patterns (LARMAN, 2004)
DIP tem a ver com a qualidade das dependências: garantir que regras de negócio não dependam de detalhes de implementação.
Já Dependency Injection (DI) está relacionado com a maneira como um objeto obtém as suas dependências.
Quando usamos DI, um framework ou container (Guice, Spring ou outro) fornece instâncias das dependências para um determinado objeto, ao invés do próprio objeto buscar essas instâncias por meio de Factories.
Como o objeto recebe as suas dependências "de fora" ao invés de buscá-las, DI é um tipo de Inversion of Control (IoC), um conceito mais geral.
Por exemplo, ao registramos um ActionListener
em um JButton
do Swing, que será chamado se, eventualmente, o usuário clicar no botão, também estamos usando IoC.
Um outro nome para IoC é Hollywood Principle, que vem de uma frase comumente dita por produtores de cinema: "Não me ligue, deixa que eu te ligo".
Uma maneira comum em Java de criar uma Factory, é ter um método estático que, quando chamado, retorna uma nova instância da implementação "escondida" por trás de uma abstração.
Em geral, colocamos um sufixo Factory
na classe:
####### cotuba.md.RenderizadorMDParaHTMLFactory
public class RenderizadorMDParaHTMLFactory {
public static RenderizadorMDParaHTML cria() {
return new RenderizadorMDParaHTMLImpl();
}
}
Um fato interessante é que, a partir do Java 8, podemos ter métodos estáticos em interfaces. Portanto, podemos evitar mais uma classe colocando o método cria
na própria interface RenderizadorMDParaHTML
:
####### cotuba.md.RenderizadorMDParaHTML
public interface RenderizadorMDParaHTML {
List<Capitulo> renderiza(Path diretorioDosMD);
public static RenderizadorMDParaHTML cria() {
return new RenderizadorMDParaHTMLImpl();
}
}
Crie métodos estáticos que servirão como Factory
para as implementações das interfaces RenderizadorMDParaHTML
, GeradorPDF
e GeradorEPUB
.
Use esses novos métodos na classe Cotuba
.
- Defina o método
cria
na interfaceRenderizadorMDParaHTML
, que retorna uma nova instância deRenderizadorMDParaHTMLImpl
:
####### cotuba.md.RenderizadorMDParaHTML
public interface RenderizadorMDParaHTML {
List<Capitulo> renderiza(Path diretorioDosMD);
// inserido
public static RenderizadorMDParaHTML cria() {
return new RenderizadorMDParaHTMLImpl();
}
}
- Defina também o método
cria
na interfaceGeradorPDF
, retornando umGeradorPDFImpl
:
####### cotuba.pdf.GeradorPDF
public interface GeradorPDF {
void gera(Ebook ebook);
// inserido
public static GeradorPDF cria() {
return new GeradorPDFImpl();
}
}
- Faça o mesmo para a interface
GeradorEPUB
:
####### cotuba.epub.GeradorEPUB
public interface GeradorEPUB {
void gera(Ebook ebook);
// inserido
public static GeradorEPUB cria() {
return new GeradorEPUBImpl();
}
}
- Na classe
Cotuba
, troque a instanciação deRenderizadorMDParaHTMLImpl
pela chamada do métodocria
da interfaceRenderizadorMDParaHTML
:
####### cotuba.application.Cotuba
R̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶M̶D̶P̶a̶r̶a̶H̶T̶M̶L̶ ̶r̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶ ̶=̶ ̶n̶e̶w̶ ̶R̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶M̶D̶P̶a̶r̶a̶H̶T̶M̶L̶I̶m̶p̶l̶(̶)̶;̶
RenderizadorMDParaHTML renderizador = RenderizadorMDParaHTML.cria();
Limpe também o import:
####### cotuba.application.Cotuba
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶m̶d̶.̶R̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶M̶D̶P̶a̶r̶a̶H̶T̶M̶L̶I̶m̶p̶l̶;̶
- Faça o mesmo para a interface
GeradorPDF
:
####### cotuba.application.Cotuba
G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶ ̶r̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶ ̶=̶ ̶n̶e̶w̶ ̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶I̶m̶p̶l̶(̶)̶;̶
GeradorPDF geradorPDF = GeradorPDF.cria();
O import deve ser limpo:
####### cotuba.application.Cotuba
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶p̶d̶f̶.̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶I̶m̶p̶l̶;̶
- Use também o método
cria
da interfaceGeradorEPUB
:
####### cotuba.application.Cotuba
G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶ ̶g̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶ ̶=̶ ̶n̶e̶w̶ ̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶I̶m̶p̶l̶(̶)̶;̶
GeradorEPUB geradorEPUB = GeradorEPUB.cria();
Não esqueça de limpar o import:
####### cotuba.application.Cotuba
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶e̶p̶u̶b̶.̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶I̶m̶p̶l̶;̶
Teste a geração de PDF e EPUB. Deve funcionar!
Pronto! Agora Cotuba
, uma a classe de alto nível , depende apenas de abstrações! A criação dos objetos ficou escondida em cada Factory.
Em projetos Java, é comum colocar o sufixo Impl
na classe que implementa uma interface quando há uma única implementação. Por exemplo, para a interface EnviadorSMS
, teríamos a classe EnviadorSMSImpl
como implementação.
Os desenvolvedores .NET têm um costume diferente: prefixar as interfaces com I
. Então, teríamos IEnviadorSMS
como interface e EnviadorSMS
como implementação.
Não soa estranho ter classes como EnviadorSMSImpl
ou interfaces como IEnviadorSMS
?
Uma alternativa é colocar no nome da implementação algo que revela a tecnologia usada.
No caso da interface EnviadorSMS
, como usamos a API do Twilio, a implementação poderia ser chamada de EnviadorSMSComTwilio
. Se modificarmos a implementação para usar a API do Nexmo, poderíamos chamá-la de EnviadorSMSComNexmo
.
De maneira análoga, teríamos EnviadorEmailComSendGrid
ao invés de EnviadorEmailImpl
e NotaFiscalDAOComHibernate
ao invés de NotaFiscalDAOImpl
.
Remove o sufixo Impl
das implementações.
Renomeie as implementações, considerando que:
- a renderização de Markdown usa a biblioteca CommonMark Java
- a geração de PDF usa a biblioteca iText
- a geração de EPUB usa a biblioteca Epublib
- Renomeie as implementações das dependências de baixo nível de
Cotuba
:
- de
RenderizadorMDParaHTMLImpl
paraRenderizadorMDParaHTMLComCommonMark
- de
GeradorPDFImpl
paraGeradorPDFComIText
- de
GeradorEPUBImpl
paraGeradorEPUBComEpublib
As interfaces RenderizadorMDParaHTML
, GeradorPDF
e GeradorEPUB
, que servem como abstrações para as dependências de baixo nível da classe Cotuba
, estão próximas às suas implementações:
- a interface
RenderizadorMDParaHTML
está no pacotecotuba.md
- a interface
GeradorPDF
está no pacotecotuba.pdf
- a interface
GeradorEPUB
está no pacotecotuba.epub
Parece bem organizado mas há um problema de design. Para revelar esse problema, vamos pensar nas dependências um nível acima das classes: as dependências entre pacotes.
A classe Cotuba
é a cliente das interfaces RenderizadorMDParaHTML
, GeradorPDF
e GeradorEPUB
. Afinal de contas, são abstrações criadas exatamente para inverter as dependências de Cotuba
.
Veja os imports:
####### cotuba.application.Cotuba
package cotuba.application;
// demais imports omitidos...
import cotuba.epub.GeradorEPUB;
import cotuba.md.RenderizadorMDParaHTML;
import cotuba.pdf.GeradorPDF;
public class Cotuba {
// código omitido...
}
O pacote de Cotuba
é cotuba.application
, que acaba dependendo de abstrações dos pacotes cotuba.md
, cotuba.pdf
e cotuba.epub
.
Um pacote com código de alto nível está dependente de pacotes com código de baixo nível. Isso fere o DIP!
Agora, se movermos as interfaces usadas por Cotuba
para o pacote cotuba.application
, as abstrações estarão mais próximas de quem as usa.
A dependência entre os pacotes também passa a ser invertida: o pacote de alto nível não depende mais dos pacotes de baixo nível, mas o contrário!
Interfaces de Serviço, em geral, são do cliente (quem as usa).
Uncle Bob, no livro Agile Principles, Patterns, and Practices in C# (MARTIN, 2006)
Mova as abstrações para o mesmo pacote da classe Cotuba
.
- Mova as interfaces
RenderizadorMDParaHTML
,GeradorPDF
eGeradorEPUB
para o pacotecotuba.application
.
Como dessas interfaces será o mesmo da classe Cotuba
, certifique-se que os imports desnecessários foram removidos:
####### cotuba.application.Cotuba
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶e̶p̶u̶b̶.̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶;̶
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶m̶d̶.̶R̶e̶n̶d̶e̶r̶i̶z̶a̶d̶o̶r̶M̶D̶P̶a̶r̶a̶H̶T̶M̶L̶;̶
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶p̶d̶f̶.̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶;̶
- (opcional) Discussão: será mesmo que invertemos as dependências dos pacotes?
Qual detalhe ainda faz com que, pensando em pacotes, haja uma quebra do DIP?
Observação: resolveremos esse problema em um capítulo posterior.
Repare no método executa
da classe Cotuba
:
public class Cotuba {
public void executa(String formato, Path diretorioDosMD, Path arquivoDeSaida) {
// código omitido...
}
}
Esse método recebe três parâmetros atualmente. A chance desse número aumentar é razoável. E uma lista longa de parâmetros é algo terrível de entender, principalmente na chamada do método.
Poderíamos criar uma classe para agrupar esses parâmetros, simplificando a assinatura do método executa
.
O livro Refactoring (FOWLER et al., 1999) cataloga a refatoração Introduce Parameter Object (Introduzir Objeto Parâmetro).
Além de simplificar a assinatura de um método, outra motivação dessa refatoração é agrupar parâmetros que aparecem juntos, repetidamente, em diversos métodos da mesma classe ou de classes diferentes.
Uma das vantagens é ter um lugar para colocar lógica que manipula esses parâmetros.
Mas veja só... Já temos uma classe que agrupa formato
, diretorioDosMD
e arquivoDeSaida
: a classe LeitorOpcoesCLI
!
Use a classe LeitorOpcoesCLI
como parâmetro do método executa
da classe Cotuba
.
- Altere o método
executa
da classeCotuba
para receber umLeitorOpcoesCLI
como parâmetro:
####### cotuba.application.Cotuba
public class Cotuba {
public void executa(LeitorOpcoesCLI parametros) { // modificado
// restante do código ...
Não esqueça do import:
####### cotuba.application.Cotuba
import cotuba.cli.LeitorOpcoesCLI;
O código deve apresentar erros de compilação no uso dos parâmetros antigos. Vamos corrigi-los no próximo passo.
- Dentro do método
executa
deCotuba
, crie novas variáveis com os mesmos nomes dos parâmetros que foram removidos,formato
,diretorioDosMD
earquivoDeSaida
:
####### cotuba.application.Cotuba
public void executa(LeitorOpcoesCLI parametros) {
String formato = parametros.getFormato(); // inserido
Path diretorioDosMD = parametros.getDiretorioDosMD(); // inserido
Path arquivoDeSaida = parametros.getArquivoDeSaida(); // inserido
// restante do código ...
Agora o código da classe Cotuba
deve compilar com sucesso!
- Na classe
Main
, altere a chamada do métodoexecuta
da classeCotuba
, passando o objetoopcoesCLI
. Remova as variáveis desnecessárias, mantendo as que ainda são usadas.
####### cotuba.cli.Main
LeitorOpcoesCLI opcoesCLI = new LeitorOpcoesCLI(args);
P̶a̶t̶h̶ ̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶ ̶=̶ ̶o̶p̶c̶o̶e̶s̶C̶L̶I̶.̶g̶e̶t̶D̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶(̶)̶;̶
S̶t̶r̶i̶n̶g̶ ̶f̶o̶r̶m̶a̶t̶o̶ ̶=̶ ̶o̶p̶c̶o̶e̶s̶C̶L̶I̶.̶g̶e̶t̶F̶o̶r̶m̶a̶t̶o̶(̶)̶;̶
Path arquivoDeSaida = opcoesCLI.getArquivoDeSaida();
boolean modoVerboso = opcoesCLI.isModoVerboso();
try {
Cotuba cotuba = new Cotuba();
c̶o̶t̶u̶b̶a̶.̶e̶x̶e̶c̶u̶t̶a̶(̶f̶o̶r̶m̶a̶t̶o̶,̶ ̶d̶i̶r̶e̶t̶o̶r̶i̶o̶D̶o̶s̶M̶D̶,̶ ̶a̶r̶q̶u̶i̶v̶o̶D̶e̶S̶a̶i̶d̶a̶)̶;̶
cotuba.executa(opcoesCLI); // modificado
- Teste a geração do PDF e do EPUB. Deve funcionar!
A classe Cotuba
, do pacote cotuba.application
, é uma classe relacionada à aplicação, de alto nível, que tem a ver com problema que estamos resolvendo.
Já a classe LeitorOpcoesCLI
, do pacote cotuba.cli
, é um detalhe de implementação, de baixo nível, que tem a ver com o mecanismo de entrega de UI que escolhemos: a linha de comando.
Ao fazermos Cotuba
, em seu método executa
, receber um objeto da classe LeitorOpcoesCLI
como parâmetro, fizemos um código de alto nível depender de um de baixo nível. Por isso, violamos o DIP.
Como fazer para aderir ao DIP novamente?
Criar um abstração, invertendo a dependência!
Cria uma abstração chama ParametrosCotuba
a partir de LeitorOpcoesCLI
e a use como parâmetro no método executa
da classe Cotuba
.
A nova abstração deve ficar no mesmo pacote de Cotuba
.
- Extraia uma interface
ParametrosCotuba
da classe que contém getters para os atributosformato
,diretorioDosMD
earquivoDeSaida
.
Certifique-se que a nova interface está declarada no pacote cotuba.application
.
Dica: use o menu Refactor > Extract Interface.... Deixem marcadas as opções Use the extracted interface type where possible e Generate '@Override' annotations. Desmarque as demais opções. Não esqueça de selecionar os getters a serem declarados na interface e clique em OK.
O resultado esperado está a seguir:
####### cotuba.application.ParametrosCotuba
package cotuba.application;
import java.nio.file.Path;
public interface ParametrosCotuba {
Path getDiretorioDosMD();
String getFormato();
Path getArquivoDeSaida();
}
####### cotuba.cli.LeitorOpcoesCLI
import cotuba.application.ParametrosCotuba; // inserido
public class LeitorOpcoesCLI implements ParametrosCotuba { // modificado
// restante do código...
}
####### cotuba.application.Cotuba
import java.nio.file.Path;
import java.util.List;
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶c̶l̶i̶.̶L̶e̶i̶t̶o̶r̶O̶p̶c̶o̶e̶s̶C̶L̶I̶;̶
import cotuba.domain.Capitulo;
import cotuba.domain.Ebook;
public class Cotuba {
public void executa(ParametrosCotuba parametros) { // modificado
// restante do código...
Observe que a classe Cotuba
passa a depender apenas:
- de bibliotecas padrão do Java: API de Collections e NIO
- das classes de domínio:
Ebook
eCapitulo
- de abstrações:
ParametrosCotuba
,RenderizadorMDParaHTML
,GeradorPDF
eGeradorEPUB
- Teste a geração dos PDFs e EPUBs. Deve funcionar!