Vamos voltar ao exemplo do EmissorNotaFiscal
que vimos no capítulo sobre DIP:
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).
Considerando que NotaFiscalDAO
, EnviadorEmail
e EnviadorSMS
são interfaces que abstraem as dependências de baixo nível da classe EmissorNotaFiscal
, será que chegamos a um bom design?
Repare que, ao final do método gera
de EmissorNotaFiscal
, são feitas várias ações:
- persistir a nota gerada no BD através de
NotaFiscalDAO
e sua implementaçãoNotaFiscalDAOComHibernate
- enviar uma notificação por email através de
EnviadorEmail
e sua implementaçãoEnviadorEmailComSendGrid
- enviar um notificação por SMS através de
EnviadorSMS
e sua implementaçãoEnviadorSMSComTwilio
Podemos buscar abstrações melhores.
Email e SMS são tipos de notificação, não é mesmo?
Poderíamos criar uma abstração para Notificacao
, que seria implementada tanto por EnviadorEmailComSendGrid
como por EnviadorSMSComTwilio
:
public interface Notificacao {
void notifica(NotaFiscal nota);
}
Mas podemos ir além: todas essas ações, de persistência ou de notificação, são um conjunto de ações que devem ser feitas após a emissão da nota fiscal.
Que tal uma abstração chamada AcaoPosEmissao
? Tanto NotaFiscalDAOComHibernate
quantoEnviadorEmailComSendGrid
e EnviadorSMSComTwilio
seriam implementações.
public interface AcaoPosEmissao {
void faz(NotaFiscal nota);
}
Na classe EmissorNotaFiscal
, teríamos uma lista de AcaoPosEmissao
que percorreríamos no final da geração da nota, invocando cada implementação:
public class EmissorNotaFiscal {
private RegrasDeTributacao tributacao;
private LegislacaoFiscal legislacao;
private List<AcaoPosEmissao> acoes;
//...
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
for (AcaoPosEmissao acao: acoes) {
acao.faz(nota);
}
return nota;
}
}
Note que podemos encaixar qualquer tipo de ação na abstração AcaoPosEmissao
.
Ao surgirem novas necessidades, como envio para o SAP ERP ou para um novo sistema da prefeitura, bastaria fornecermos mais uma implementação.
Com uma abstração correta, o código se torna extremamente flexível, tolerante a mudanças. É o que exatamente o que almejamos em um design OO.
Flexível
Que se adapta bem a diferentes atividades ou funções; acomodatício, adaptável, moldável.
Perceba que não há tanta diferença entre as interfaces Notificacao
e AcaoPosEmissao
em termos de código. Basicamente, o que mudam são os nomes dos métodos e das próprias interfaces.
A principal diferença é em termos semânticos: o significado da abstração muda, tornando-se mais moldável.
A abstração AcaoPosEmissao
serve tanto para persistência, como para notificações, como para qualquer outro tipo de operação a ser feita depois da emissão da nota fiscal.
A ideia de ter uma abstração que representa uma operação do sistema, sem detalhes sobre o que é feito, é descrita no Command Pattern do livro Design Patterns (GAMMA et al., 1994).
Esconder os detalhes de várias implementações diferentes, mas relacionadas, por trás de uma mesma abstração é o que Craig Larman chama de Variações Protegidas no artigo Protected Variation (LARMAN, 2001):
Identifique pontos previstos de variação e crie uma interface estável em torno deles.
Precisamos, constantemente, analisar o nosso código em busca de pontos de articulação onde nosso design varia e, por meio de abstrações, torná-los flexíveis.
Encontrar os pontos onde sua modelagem precisa ser flexível e onde não precisa é um desafio.
Maurício Aniche, no livro "OO e SOLID para Ninjas" (2015)
Uncle Bob parte de uma frase de Bertrand Meyer, pioneiro de OO, no livro Object-Oriented Software Construction (MEYER, 1988), para cunhar o seguinte princípio:
Open/Closed Principle (OCP)
Entidades de software (classes, módulos, funções, etc) devem ser abertas para extensão mas fechadas para modificação.
De acordo com o OCP, classes devem ser abertas e fechadas ao mesmo tempo. Como assim?
Esse princípio trata de esclarecer que, em um bom design OO, deve ser possível criar novos comportamentos sem modificar o código já existente.
É o que fizemos ao criar abstração AcaoPosEmissao
no exemplo anterior: podemos adicionar novas ações sem mudar a classe EmissorNotaFiscal
, que dispara a execução dessas ações.
Devemos escrever módulos que podem ser estendidos sem que sejam modificados. Mudar o que os módulos fazem sem mudar o seu código fonte.
Uncle Bob, no artigo Design Principles and Design Patterns (MARTIN, 2000)
No artigo Design Principles and Design Patterns (MARTIN, 2000), Uncle Bob compara os princípios DIP e OCP, mostrando que são complementares:
Se o OCP declara o objetivo de uma arquitetura OO, o DIP declara o seu mecanismo fundamental.
O DIP fomenta a classificação de dependências em alto/baixo nível e a inversão das dependências em direção às regras de negócio.
Isso conduz a abstrações que levam a um código que pode ser estendido sem modificá-lo: o intuito do OCP.
Repare nas abstrações definidas pelas interfaces GeradorPDF
e GeradorEPUB
:
public interface GeradorPDF {
void gera(Ebook ebook);
// factory omitida...
}
public interface GeradorPDF {
void gera(Ebook ebook);
// factory omitida...
}
Ambas as interfaces definem um método gera
que recebe como parâmetro um Ebook
e não tem nenhum retorno.
As abstrações são idênticas! Há algo de errado nisso, não?
Tanto PDF como EPUB são formatos de ebooks.
Poderíamos criar uma abstração mais relacionada ao problema que estamos resolvendo: um gerador de books.
Esse gerador não define em qual formato o ebook deverá ser gerado. Esse detalhe é tarefa para as implementações.
A geração de ebooks seria mais flexível: para novos formatos, basta uma nova implementação. É o espírito do OCP!
Crie uma interface GeradorEbook
e faça com que GeradorPDFComIText
e GeradorEPUBComEpublib
a implemente.
- Crie uma nova interface
GeradorEbook
no pacotecotuba.application
, conforme a seguir:
####### cotuba.application.GeradorEbook
package cotuba.application;
import cotuba.domain.Ebook;
public interface GeradorEbook {
void gera(Ebook ebook);
}
- Faça a classe
GeradorPDFComIText
implementar a nova interface, removendo a anterior:
####### cotuba.pdf.GeradorPDFComIText
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶a̶p̶p̶l̶i̶c̶a̶t̶i̶o̶n̶.̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶;̶
import cotuba.application.GeradorEbook; // inserido
//outros imports...
p̶u̶b̶l̶i̶c̶ ̶c̶l̶a̶s̶s̶ ̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶C̶o̶m̶I̶T̶e̶x̶t̶ ̶i̶m̶p̶l̶e̶m̶e̶n̶t̶s̶ ̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶ ̶{̶
public class GeradorPDFComIText implements GeradorEbook { // modificado
// código omitido ...
}
- Faça o mesmo na classe
GeradorEPUBComEpublib
:
####### cotuba.epub.GeradorEPUBComEpublib
i̶m̶p̶o̶r̶t̶ ̶c̶o̶t̶u̶b̶a̶.̶a̶p̶p̶l̶i̶c̶a̶t̶i̶o̶n̶.̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶;̶
import cotuba.application.GeradorEbook; // inserido
//outros imports...
p̶u̶b̶l̶i̶c̶ ̶c̶l̶a̶s̶s̶ ̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶C̶o̶m̶E̶p̶u̶b̶l̶i̶b̶ ̶i̶m̶p̶l̶e̶m̶e̶n̶t̶s̶ ̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶ ̶{̶
public class GeradorEPUBComEpublib implements GeradorEbook { // modificado
// código omitido ...
}
Ignore, por enquanto, os erros de compilação nas interfaces GeradorPDF
e GeradorEPUB
. Vamos corrigi-los!
Remova as interfaces GeradorPDF
e GeradorEPUB
e corrija o restante do código.
- As interfaces
GeradorPDF
eGeradorEPUB
devem apresentar erros de compilação nos respectivos métodoscria
.
Essas interfaces não tem mais utilidade. Remova-as sem dó!
c̶o̶t̶u̶b̶a̶.̶a̶p̶p̶l̶i̶c̶a̶t̶i̶o̶n̶.̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶
c̶o̶t̶u̶b̶a̶.̶a̶p̶p̶l̶i̶c̶a̶t̶i̶o̶n̶.̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶
Agora o erro de acontecer na classe Cotuba
, na criação das instâncias dos geradores.
- Use a interface
GeradorEbook
, instanciando os objetos apropriados, na classeCotuba
:
####### cotuba.application.Cotuba
GeradorEbook gerador; // inserido
if ("pdf".equals(formato)) {
G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶ ̶g̶e̶r̶a̶d̶o̶r̶P̶D̶F̶ ̶=̶ ̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶.̶c̶r̶i̶a̶(̶)̶;̶
g̶e̶r̶a̶d̶o̶r̶P̶D̶F̶.̶g̶e̶r̶a̶(̶e̶b̶o̶o̶k̶)̶;̶
gerador = new GeradorPDFComIText(); // inserido
} else if ("epub".equals(formato)) {
G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶ ̶g̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶ ̶=̶ ̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶.̶c̶r̶i̶a̶(̶)̶;̶
g̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶.̶g̶e̶r̶a̶(̶e̶b̶o̶o̶k̶)̶;̶
gerador = new GeradorEPUBComEpublib(); // inserido
} else {
throw new RuntimeException("Formato do ebook inválido: " + formato);
}
gerador.gera(ebook); // inserido
- Teste a geração de PDF e EPUB. Deve funcionar!
Ao definirmos a abstração de um gerador sem um formato específico, criamos uma maneira mais flexível de gerar ebooks.
Para um novo formato de ebook, basta uma nova implementação de GeradorEbook
.
A definição de uma abstração comum a várias implementações diferentes de um mesmo comportamento é descrita no Strategy Pattern do livro Design Patterns (GAMMA et al., 1994).
Nos termos do livro, o Strategy define uma família de algoritmos intercambiáveis, encapsulados em um contrato comum.
No exemplo do começo do capítulo, da emissão de notas fiscais, a persistência no BD e as notificações por email e SMS foram "escondidas" atrás da interface AcaoPosEmissao
.
Já no exemplo do Cotuba, maneiras diferentes de gerar um ebook foram "escondidas" atrás da interface GeradorEbook
.
Usamos o mesmo mecanismo nos dois casos: interfaces e suas implementações.
Então, por que o primeiro caso é um exemplo do Command Pattern e o segundo, do Strategy Pattern?
A principal diferença entre o Command e o Strategy não é o código resultante, mas o objetivo:
- no Command, queremos abstrair uma operação genérica, sem definir o que é feito
- no Strategy, abstraímos um procedimento específico que faz, de jeitos diferentes, coisas parecidas
Não há mais necessidade de revelarmos a tecnologia usada na implementação no nome da classe.
Temos dois geradores de ebook. O que os diferencia é o formato gerado.
Adeque os nomes das implementações de GeradorEbook
, simplificando-os.
- Renomeie as classes que implementam a interface
GeradorEbook
da seguinte maneira:
- de
GeradorEPUBComEpublib
paraGeradorEPUB
- de
GeradorPDFComIText
paraGeradorPDF
Ao removermos as antigas interfaces GeradorPDF
e GeradorEPUB
, que agora são nomes de classes, acabamos inserindo dependências indesejadas na classe Cotuba
.
Perceba pelos seguintes imports:
import cotuba.epub.GeradorEPUB;
import cotuba.pdf.GeradorPDF;
Fizemos com que uma classe de alto nível, Cotuba
, dependesse das classes de baixo nível GeradorPDF
e GeradorEPUB
.
E, pensando em pacotes, fizemos com que cotuba.application
, de alto nível, dependesse de cotuba.pdf
e cotuba.epub
, de baixo nível.
Regras de negócio dependendo de mecanismos de entrega... Violamos o DIP!
Como resolver?
Criando as instâncias das implementações fora do objeto que as usa. Ou seja, como uma Factory.
Essa nova Factory será inteligente, já que terá uma lógica mais elaborada que as que já havíamos definido: dado um formato de ebook, será retornada uma instância da implementação correta.
Defina uma Factory na interface GeradorEbook
que, dado um formato, cria a instância apropriada.
- Crie um novo método chamado
cria
na interfaceGeradorEbook
, que recebe umaString
no parâmetroformato
e retorna uma instância deGeradorEbook
:
####### cotuba.application.GeradorEbook
public interface GeradorEbook {
void gera(Ebook ebook);
public static GeradorEbook cria(String formato) { // inserido
}
}
- Mova a lógica da criação de instâncias de
GeradorEbook
da classeCotuba
para o novo métodocria
. Não deixe da incluir oreturn
:
####### cotuba.application.GeradorEbook
GeradorEbook gerador;
if ("pdf".equals(formato)) {
gerador = new GeradorPDF();
} else if ("epub".equals(formato)) {
gerador = new GeradorEPUB();
} else {
throw new RuntimeException("Formato do ebook inválido: " + formato);
}
return gerador; // ATENÇÃO!
- Use a Factory de
GeradorEbook
no métodoexecuta
da classeCotuba
:
####### cotuba.application.Cotuba
GeradorEbook gerador = GeradorEbook.cria(formato); // modificado
gerador.gera(ebook);
Não deixe de limpar os imports desnecessários.
####### 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̶.̶p̶d̶f̶.̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶;̶
- Teste a geração de PDF e EPUB. Deve funcionar!
Nesse momento, as dependências da classe Cotuba
estão algo como a seguinte imagem:
À medida que avançamos nas refatorações, fomos caminhando na direção do polimorfismo e nos distanciando de condicionais na classe Cotuba
.
Uma boa abstração aliada a polimorfismo nos permite criar geradores de ebook em diferentes formatos sem modificar a classe que usa esses geradores.
OCP na veia!
O livro Refactoring (FOWLER et al., 1999) cataloga a refatoração Replace Conditional with Polymorphism (Substituir Condicional por Polimorfismo).
Ao mover cada ramificação da condicional para uma implementação diferente de uma abstração comum, usando de polimorfismo, conseguimos simplificar o código da classe original.
Um bom design OO vai ter poucas condicionais como if-else ou switch-case.
Objetos têm um mecanismo fabuloso, mensagens polimórficas, que permitem expressar lógica condicional de maneira flexível mas clara.
Ao trocar condicionais explícitos por mensagens polimórficas, comumente você consegue:
- reduzir duplicação,
- tornar seu código mais claro e
- aumentar a flexibilidade
tudo ao mesmo tempo.
Kent Beck, no livro Refactoring (FOWLER et al., 1999)
Nos distanciamos de condicionais em direção ao polimorfismo na classe Cotuba
.
Mas onde foram parar esses condicionais? No método cria
da interface GeradorEbook
, a Factory de geradores de ebook.
Será que é possível livrar-nos totalmente de instruções if-else e switch-case?
Francesco Cirillo, desenvolvedor de software e criador da técnica Pomodoro de gerenciamento de tempo, iniciou em 2007 a campanha Anti-IF:
Muitos times querem ser ágeis, mas têm dificuldade em reduzir a complexidade de código. Vamos começar com algo concreto: saber como usar objetos de uma maneira que permita que os desenvolvedores eliminem os IFs ruins, aqueles que frequentemente comprometem a flexibilidade e a habilidade de evoluir do software.
Francesco Cirillo, no site Anti-IF Campaign (CIRILLO)
Como podemos eliminar os if-else ao máximo do código do Cotuba?
Uma das maneira é ter um Map<String, GeradorEbook>
que, dado um determinado formato, contém a implementação de GeradorEbook
correspondente.
Definiríamos esse Map
na interface GeradorEbook
:
import java.util.HashMap;
import java.util.Map;
// outros imports...
public interface GeradorEbook {
Map<String, GeradorEbook> GERADORES = new HashMap<>();
// código omitido...
}
Apesar de parecer um atributo, GERADORES
é uma constante, implicitamente public static final
.
Mas como inicializar os valores nesse Map
? Não podemos definir blocos static
em interfaces, muito menos construtores.
Podemos usar a sintaxe double brace initialization, que define um bloco de inicialização em uma subclasse anônima! É usado em algumas bibliotecas como a jMock.
Veja:
public interface GeradorEbook {
Map<String, GeradorEbook> GERADORES = new HashMap<String, GeradorEbook>() {{
put("pdf", new GeradorPDF());
put("epub", new GeradorEPUB());
}};
// código omitido...
}
Perceba que não é possível usar o diamond operator ao usar inicialização com double braces porque é necessário definir os tipos contidos no HashMap
.
Então, usaríamos o Map
para obter os geradores a partir do formato, sem a necessidade de nenhuma condicional:
public interface GeradorEbook {
// código omitido...
public static GeradorEbook cria(String formato) {
GeradorEbook gerador = GERADORES.get(formato);
if (gerador == null) {
throw new RuntimeException("Formato do ebook inválido: " + formato);
}
return gerador;
}
Guilherme Silveira explora uma implementação parecida, usando mapas, e uma outra usando a Reflection API do Java no post Como não aprender orientação a objetos: o excesso de ifs (SILVEIRA, 2011).
Até agora, os formatos PDF e EPUB estão representados no código como String
.
Poderíamos defini-los numa enum. Como é um conceito do domínio, podemos colocá-la no pacote cotuba.domain
:
package cotuba.domain;
public enum FormatoEbook {
PDF, EPUB;
}
Enums são tipos bastante poderosos em Java: podem ter atributos, construtores (apenas privados) e métodos públicos ou privados.
Podemos usar esse poder das enums para evitar condicionais:
public enum FormatoEbook {
PDF(new GeradorPDF()),
EPUB(new GeradorEPUB());
private GeradorEbook gerador;
private FormatoEbook(GeradorEbook gerador) {
this.gerador = gerador;
}
public GeradorEbook getGerador() {
return gerador;
}
}
Defina uma enum FormatoEbook
no pacote cotuba.domain
com valores para PDF e EPUB.
Armazene um GeradorEbook
nessa enum.
Use a nova enum para obter instâncias dos geradores.
- Defina a enum
FormatoEbook
no pacotecotuba.domain
, cujos valores PDF e EPUB armazenam os respectivos geradores de ebook, conforme o código a seguir:
####### cotuba.domain.FormatoEbook
package cotuba.domain;
import cotuba.application.GeradorEbook;
import cotuba.epub.GeradorEPUB;
import cotuba.pdf.GeradorPDF;
public enum FormatoEbook {
PDF(new GeradorPDF()),
EPUB(new GeradorEPUB());
private GeradorEbook gerador;
private FormatoEbook(GeradorEbook gerador) {
this.gerador = gerador;
}
public GeradorEbook getGerador() {
return gerador;
}
}
- Na classe
Ebook
, mude o tipo do atributoformato
paraFormatoEbook
e corrija os getters e os setters:
####### cotuba.domain.Ebook
public class Ebook {
p̶r̶i̶v̶a̶t̶e̶ ̶S̶t̶r̶i̶n̶g̶ ̶f̶o̶r̶m̶a̶t̶o̶;̶
private FormatoEbook formato; // modificado
private Path arquivoDeSaida;
private List<Capitulo> capitulos;
p̶u̶b̶l̶i̶c̶ ̶S̶t̶r̶i̶n̶g̶ ̶g̶e̶t̶F̶o̶r̶m̶a̶t̶o̶(̶)̶ ̶{̶
public FormatoEbook getFormato() { // modificado
return formato;
}
p̶u̶b̶l̶i̶c̶ ̶v̶o̶i̶d̶ ̶s̶e̶t̶F̶o̶r̶m̶a̶t̶o̶(̶S̶t̶r̶i̶n̶g̶ ̶f̶o̶r̶m̶a̶t̶o̶)̶ ̶{̶
public void setFormato(FormatoEbook formato) { // modificado
this.formato = formato;
}
// restante do código...
}
- Deve acontecer um erro de compilação na classe
Cotuba
. Use a nova enum como tipo da variávelformato
:
####### cotuba.application.Cotuba
import cotuba.domain.FormatoEbook; // inserido
public class Cotuba {
public void executa(ParametrosCotuba parametros) {
S̶t̶r̶i̶n̶g̶ ̶f̶o̶r̶m̶a̶t̶o̶ ̶=̶ ̶p̶a̶r̶a̶m̶e̶t̶r̶o̶s̶.̶g̶e̶t̶F̶o̶r̶m̶a̶t̶o̶(̶)̶;̶
FormatoEbook formato = parametros.getFormato();
// código omitido...
}
}
Devem acontecer novos erros de compilação. Vamos arrumá-los aos poucos.
- Na interface
ParametrosCotuba
, corrija o tipo de retorno do métodogetFormato
:
####### cotuba.application.ParametrosCotuba
import cotuba.domain.FormatoEbook; // inserido
public interface ParametrosCotuba {
S̶t̶r̶i̶n̶g̶ ̶g̶e̶t̶F̶o̶r̶m̶a̶t̶o̶(̶)̶;̶
FormatoEbook getFormato(); // modificado
// outros métodos...
}
- Em
LeitorOpcoesCLI
, mude o tipo do atributoformato
paraFormatoEbook
e corrija o getter e o construtor.
####### cotuba.cli.LeitorOpcoesCLI
import cotuba.domain.FormatoEbook; // inserido
public class LeitorOpcoesCLI implements ParametrosCotuba {
p̶r̶i̶v̶a̶t̶e̶ ̶S̶t̶r̶i̶n̶g̶ ̶f̶o̶r̶m̶a̶t̶o̶;̶
private FormatoEbook formato; // modificado
// outro parâmetros...
public LeitorOpcoesCLI(String[] args) {
// código omitido...
f̶o̶r̶m̶a̶t̶o̶ ̶=̶ ̶n̶o̶m̶e̶D̶o̶F̶o̶r̶m̶a̶t̶o̶D̶o̶E̶b̶o̶o̶k̶.̶t̶o̶L̶o̶w̶e̶r̶C̶a̶s̶e̶(̶)̶;̶
formato = FormatoEbook.valueOf(nomeDoFormatoDoEbook.toUpperCase()); // modificado
// código omitido...
f̶o̶r̶m̶a̶t̶o̶ ̶=̶ ̶"̶p̶d̶f̶"̶;̶
formato = FormatoEbook.PDF; // modificado
// código omitido...
a̶r̶q̶u̶i̶v̶o̶D̶e̶S̶a̶i̶d̶a̶ ̶=̶ ̶P̶a̶t̶h̶s̶.̶g̶e̶t̶(̶"̶b̶o̶o̶k̶.̶"̶ ̶+̶ ̶f̶o̶r̶m̶a̶t̶o̶.̶t̶o̶L̶o̶w̶e̶r̶C̶a̶s̶e̶(̶)̶)̶;̶
arquivoDeSaida = Paths.get("book." + formato.name().toLowerCase()); // modificado
// restante do código...
}
@Override
p̶u̶b̶l̶i̶c̶ ̶S̶t̶r̶i̶n̶g̶ ̶g̶e̶t̶F̶o̶r̶m̶a̶t̶o̶(̶)̶ ̶{̶
public FormatoEbook getFormato() { // modificado
return formato;
}
// outros getters...
}
- Na interface
GeradorEbook
, corrija o métodocria
, para que receba umFormatoEbook
. Faça com que a instância do gerador seja obtida da própria enum:
####### cotuba.application.GeradorEbook
public interface GeradorEbook {
// código omitido...
p̶u̶b̶l̶i̶c̶ ̶s̶t̶a̶t̶i̶c̶ ̶G̶e̶r̶a̶d̶o̶r̶E̶b̶o̶o̶k̶ ̶c̶r̶i̶a̶(̶S̶t̶r̶i̶n̶g̶ ̶f̶o̶r̶m̶a̶t̶o̶)̶ ̶{̶
public static GeradorEbook cria(FormatoEbook formato) { // modificado
G̶e̶r̶a̶d̶o̶r̶E̶b̶o̶o̶k̶ ̶g̶e̶r̶a̶d̶o̶r̶;̶
i̶f̶ ̶(̶"̶p̶d̶f̶"̶.̶e̶q̶u̶a̶l̶s̶(̶f̶o̶r̶m̶a̶t̶o̶)̶)̶ ̶{̶
g̶e̶r̶a̶d̶o̶r̶ ̶=̶ ̶n̶e̶w̶ ̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶(̶)̶;̶
}̶ ̶e̶l̶s̶e̶ ̶i̶f̶ ̶(̶"̶e̶p̶u̶b̶"̶.̶e̶q̶u̶a̶l̶s̶(̶f̶o̶r̶m̶a̶t̶o̶)̶)̶ ̶{̶
g̶e̶r̶a̶d̶o̶r̶ ̶=̶ ̶n̶e̶w̶ ̶G̶e̶r̶a̶d̶o̶r̶E̶P̶U̶B̶(̶)̶;̶
}̶ ̶e̶l̶s̶e̶ ̶{̶
t̶h̶r̶o̶w̶ ̶n̶e̶w̶ ̶R̶u̶n̶t̶i̶m̶e̶E̶x̶c̶e̶p̶t̶i̶o̶n̶(̶"̶F̶o̶r̶m̶a̶t̶o̶ ̶d̶o̶ ̶e̶b̶o̶o̶k̶ ̶i̶n̶v̶á̶l̶i̶d̶o̶:̶ ̶"̶ ̶+̶ ̶f̶o̶r̶m̶a̶t̶o̶)̶;̶
}̶
GeradorEbook gerador = formato.getGerador(); // modificado
return gerador;
}
}
Arrume os imports:
####### cotuba.application.GeradorEbook
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̶.̶p̶d̶f̶.̶G̶e̶r̶a̶d̶o̶r̶P̶D̶F̶;̶
import cotuba.domain.FormatoEbook; // inserido
O código deve compilar sem erros.
- Teste a geração de PDF e EPUB. Deve funcionar!
Fizemos várias alterações na direção de boas abstrações e de um código sem if-else.
Agora, para definir um novo formato de ebook, basta criarmos mais uma implementação de GeradorEbook
e mais um valor na enum FormatoEbook
.
Não precisamos alterar o código da classe Cotuba
. Nem de outras classes!