Refatoração de Código: Substituir Código de Tipo Por Classe
(2951) (2)
CategoriasAndroid, Design, Protótipo
AutorVinÃcius Thiengo
VÃdeo aulas186
Tempo15 horas
ExercÃciosSim
CertificadoSim
CategoriaDesenvolvimento Web
Autor(es)Robert C. Martin
EditoraAlta Books
Edição1ª
Ano2023
Páginas416
Tudo bem?
Neste artigo daremos continuidade com a série Refatoração de Código com o objetivo de obtermos maior performance em nossos projetos de software.
Desta vez abordaremos o método de refatoração Substituir Código de Tipo por Classe.
Antes de prosseguir lhe informo que esse método de refatoração e todos os outros já apresentados nessa série são úteis para qualquer tipo de linguagem e não somente Java (Android).
Em maioria são métodos para trabalharem padrões do paradigma orientado a objetos, mas alguns também são utilizáveis no paradigma procedural.
Já lhe adianto que apesar do exemplo ter um pouco mais de código do que alguns métodos de refatoração apresentados aqui, o método Substituir Código de Tipo por Classe está entre os mais fáceis de entender e utilizar.
Antes de prosseguir, não esqueça de se inscrever na 📫 lista de e-mails do Blog para receber em primeira mão todos os conteúdos exclusivos sobre desenvolvimento e codificação limpa.
A seguir os tópicos que estaremos abordando em artigo:
Motivação
Seu projeto muito provavelmente vai trabalhar com famílias de constantes, algo comum, porém ele pode ter um grande problema que você não perceba.
Qual?
Os tipos definidos para essas famílias de constantes.
Ok, mas o que você quer dizer com o termo "família de constantes"?
Família de constantes é um termo utilizado para identificar conjuntos de constantes que são utilizadas para um mesmo contexto em nosso sistema.
Por exemplo: CONTA_CORRENTE e CONTA_POUPANCA são constantes de mesmo contexto, "Conta em banco", e que poderiam ser utilizadas como valores possíveis de uma variável tipoConta.
Se imagine com uma família de constantes do tipo String.
Essas vão ser utilizadas para comparação e atribuição.
Veja o exemplo abaixo de um código cliente de uma classe PermissaoSistema que trabalha com família de constantes:
public void testePadraoParaRequisicaoPermissao(){
PermissaoSistema permissao = new PermissaoSistema();
assertEquals(
permissao.REQUISITADO,
permissao.getEstado() );
assertEquals(
"REQUISITADO",
permissao.getEstado() );
}
Ok, não há problemas, até porque tudo foi "digitado corretamente".
E se o teste fosse da seguinte maneira:
public void testePadraoParaRequisicaoPermissao() {
PermissaoSistema permissao = new PermissaoSistema();
assertEquals(
permissao.REQUISITADO,
permissao.getEstado() );
assertEquals(
"REQIUSITADO",
permissao.getEstado() );
}
Ou seja, REQIUSITADO ao invés de REQUISITADO, erro!
Mesmo quando você estivesse testando com intenção de ser verdadeiro.
Depois de algum tempo, alguns bons minutos, você perceberia que não é erro na sua lógica e sim na entrada que forneceu.
Chamamos esse problema de "trabalho com tipos inseguros de dados”.
Tipos inseguros de dados?
Sim, mais precisamente o trabalho com tipos primitivos (byte, short, int, long, float, double, char, boolean) e o tipo String.
Ok, você está me dizendo que não posso mais utilizar esses tipos de dados? Preciso deles para guardar os valores.
Não.
Na verdade quando você tem uma lógica de negócio que envolve um atributo que pode ter "n" valores que são valores definidos em uma família de constantes no sistema, nesse contexto, utilizar os tipos citados acima ocasiona no problema de trabalho com tipos inseguros de dados.
O exemplo de problema acima é o que podemos dizer ser "uma mãe" (fácil), pois ele foi realizado em código próprio para testes.
Imagine essa digitação errada, digo, uma String inválida quando comparada a todas as constantes possíveis de sua lógica de negócio.
O que vai acontecer é que seu sistema vai rodar sem problemas, porém com um saída inconsistente.
Isso é também um tipo de erro de lógica.
Pode fazer você perder horas (ou dias) debugando código até descobrir que foi o valor de uma String passada como argumento de um método que estava errado!
Final das contas: muito tempo e dinheiro gastos.
Ai você pergunta: como defender o código desse problema?
Um camarada chamado Joshua Block inventou um padrão conhecido pelo nome Type-Safe Enum que nada mais nada menos introduziu a utilização de classes para serem trabalhadas em famílias de constantes, criando então o conceito de tipos seguros de dados.
Então foi Joshua que inventou o método de refatoração Substituir Código de Tipo por Classe?
Não, ele somente introduziu na programação o conceito de tipos seguros de dados, utilizando classes.
Veja bem, você falou "classes" como tipos seguros, porém a pouco também informou que String é um tipo inseguro. Isso é inconsistência, não?
Não.
String por trabalhar de forma muito similar a um tipo primitivo ela se torna "ponto fora da curva" e faz parte de tipos inseguros de dados.
Com tipos de dados seguros, classes, não temos mais a preocupação de termos, por exemplo, erro devido a digitação descuidada (nesse caso o código nem mesmo compila) e ainda podemos trabalhar com valores constantes.
Código de exemplo
Abaixo o código exemplo que vamos utilizar na refatoração desse artigo.
O código é referente a permissão de acesso a um sistema:
public class PermissaoSistema {
public static final String REQUISITADO = "REQUERIDO";
public static final String REIVINDICADO = "REIVINDICADO";
public static final String NEGADO = "NEGADO";
public static final String CONCEDIDO = "CONCEDIDO";
private String estado;
private boolean concedido;
public PermissaoSistema(){
estado = REQUISITADO;
concedido = false;
}
public void reivindicado(){
if( estado.equals( REQUISITADO ) ){
estado = REIVINDICADO;
}
}
public void negado(){
if( estado.equals( REIVINDICADO ) ){
estado = NEGADO;
}
}
public void concedido(){
if( estado.equals( REIVINDICADO ) ){
estado = CONCEDIDO;
concedido = true;
}
}
public boolean ehConcedido(){
return concedido;
}
public String getEstado(){
return estado;
}
}
A partir daqui podemos prosseguir com o método de refatoração proposto.
Mecânica
Nosso primeiro passo é identificar um atributo que seja de um tipo inseguro de dados e que seja utilizado para atribuição ou comparação de uma família de constantes que também são de tipos inseguros de dados.
Em nosso caso temos a variável de instância estado com essas características.
Depois de identificado o atributo devemos encapsulá-lo.
Segue código de PermissaoSistema atualizado:
public class PermissaoSistema {
...
public PermissaoSistema(){
setEstado( REQUISITADO );
concedido = false;
}
public void reivindicado(){
if( getEstado().equals( REQUISITADO ) ){
setEstado( REIVINDICADO );
}
}
public void negado(){
if( getEstado().equals( REIVINDICADO ) ){
setEstado( NEGADO );
}
}
public void concedido(){
if( getEstado().equals( REIVINDICADO ) ){
setEstado( CONCEDIDO );
concedido = true;
}
}
public void setEstado( String estado ){
this.estado = estado;
}
...
}
Veja que foi necessário adicionarmos o método setEstado() para encapsular a atribuição a estado.
Nosso segundo passo é criar uma classe que será futuramente utilizada como o tipo de dado seguro em um atributo que será nosso novo atributo de comparação ou atribuição a uma família de constantes em PermissaoSistema.
Essa classe deve ter um nome auto-comentado.
Ou seja, deve ser um nome que faz sentido com os valores utilizados nas constantes.
Em nosso caso será EstadoPermissao:
public class EstadoPermissao {
/* TODO */
}
O terceiro passo será representar nossa família, de constantes de tipo inseguro de dados, em nossa nova classe.
O tipo dessas representações será o de nossa nova classe, EstadoPermissao.
Com isso estaremos criando uma nova família de constantes:
public class EstadoPermissao {
public static final EstadoPermissao REQUISITADO = new EstadoPermissao();
public static final EstadoPermissao REIVINDICADO = new EstadoPermissao();
public static final EstadoPermissao NEGADO = new EstadoPermissao();
public static final EstadoPermissao CONCEDIDO = new EstadoPermissao();
}
Veja que as constantes mantemos sendo public static final, pois isso é uma convenção para constantes em Java.
Note que ainda temos a opção de restringir ainda mais a utilização de nossa classe EstadoPermissao.
Restringir mais?
Sim, podemos bloquear a possibilidade de instanciação da classe EstadoPermissao, além de podermos também bloquear o trabalho com herança partindo dessa classe.
A alteração a seguir ainda faz parte do terceiro passo e é opcional.
Em nosso exemplo vou optar por utilizá-la, pois não há porque termos herança e instância da classe EstadoPermissao:
final public class EstadoPermissao {
public static final EstadoPermissao REQUISITADO = new EstadoPermissao();
public static final EstadoPermissao REIVINDICADO = new EstadoPermissao();
public static final EstadoPermissao NEGADO = new EstadoPermissao();
public static final EstadoPermissao CONCEDIDO = new EstadoPermissao();
private EstadoPermissao(){}
}
final para bloquear herança e private no construtor para bloquear instanciação.
Nosso próximo passo é na classe que tem o atributo de tipo inseguro de dados, PermissaoSistema.
Nessa classe vamos criar um atributo de tipo seguro de dados, no caso nossa nova classe, EstadoPermissao.
Devemos também criar um método de escrita (set) para esse novo atributo:
public class PermissaoSistema {
...
private EstadoPermissao permissao;
...
public void setPermissao( EstadoPermissao permissao ){
this.permissao = permissao;
}
}
No quinto passo devemos colocar o código de atribuição de valor ao novo atributo (utilizando o método setPermissao()) em todos os lugares onde temos o código de atribuição de valor ao atributo de tipo inseguro de dados, a variável de instância estado.
Devemos utilizar as constantes corretas na atribuição para o novo atributo:
public class PermissaoSistema {
...
public PermissaoSistema(){
setEstado( REQUISITADO );
setPermissao( EstadoPermissao.REQUISITADO );
concedido = false;
}
public void reivindicado(){
if( getEstado().equals( REQUISITADO ) ){
setEstado( REIVINDICADO );
setPermissao( EstadoPermissao.REIVINDICADO );
}
}
public void negado(){
if( getEstado().equals( REIVINDICADO ) ){
setEstado( NEGADO );
setPermissao( EstadoPermissao.NEGADO );
}
}
public void concedido(){
if( getEstado().equals( REIVINDICADO ) ){
setEstado( CONCEDIDO );
setPermissao( EstadoPermissao.CONCEDIDO );
concedido = true;
}
}
...
}
Veja a utilização do método setPermissao() logo após o método setEstado(), explicitando a atribuição de valor ao novo atributo, permissao.
Em nosso sexto passo devemos modificar o método de leitura do atributo de tipo inseguro de dados. Sim, é esse mesmo, o getEstado().
Devemos obter o retorno desse método, String, vindo do novo atributo de tipo seguro de dados, permissao.
Como? Se os atributos em questão são de tipos distintos.
Nesse caso devemos também atualizar a classe EstadoPermissao para que retorne das constantes de EstadoPermissao um valor do tipo String.
Vamos começar atualizando o construtor de EstadoPermissao.
Onde nele será atribuído um valor de entrada a uma nova variável de instância do tipo String:
final public class EstadoPermissao {
public static final EstadoPermissao REQUISITADO = new EstadoPermissao( "REQUISITADO" );
public static final EstadoPermissao REIVINDICADO = new EstadoPermissao( "REIVINDICADO" );
public static final EstadoPermissao NEGADO = new EstadoPermissao( "NEGADO" );
public static final EstadoPermissao CONCEDIDO = new EstadoPermissao( "CONCEDIDO" );
private final String tipo;
private EstadoPermissao( String tipo ){
this.tipo = tipo;
}
public String toString(){
return tipo;
}
}
Também atualizamos a inicialização das constantes para refletir nossa alteração no construtor.
Adicionamos uma sobrescrita a toString() para que possamos obter esse retorno em String no método getEstado() em PermissaoSistema.
Agora atualizamos o método getEstado():
public class PermissaoSistema {
...
public String getEstado(){
return permissao.toString();
}
...
}
Nosso próximo passo é apagar o atributo de tipo inseguro de dados em que está na classe PermissaoSistema.
No caso, a variável de instância estado, além de todas as linhas de código de chamada ao método privado de atribuição de valor a ela, além também do próprio método de atribuição, setEstado().
Segue o trecho de código que foi atualizado (o código foi apagado desse trecho):
public class PermissaoSistema {
...
public PermissaoSistema(){
setPermissao( EstadoPermissao.REQUISITADO );
concedido = false;
}
public void reivindicado(){
if( getEstado().equals( REQUISITADO ) ){
setPermissao( EstadoPermissao.REIVINDICADO );
}
}
public void negado(){
if( getEstado().equals( REIVINDICADO ) ){
setPermissao( EstadoPermissao.NEGADO );
}
}
public void concedido(){
if( getEstado().equals( REIVINDICADO ) ){
setPermissao( EstadoPermissao.CONCEDIDO );
concedido = true;
}
}
...
}
Nosso próximo e último passo é identificar todas as referências a nossas constantes de tipo inseguro de dados e modificar essas pelas constantes de tipo seguro de dados.
Note que nesse passo também devemos modificar o método getEstado() para retornar um valor referente ao tipo seguro de dados, ou seja, o tipo EstadoPermissao.
Segue código de PermissaoSistema atualizado:
public class PermissaoSistema {
...
public void reivindicado(){
if( getEstado().equals( EstadoPermissao.REQUISITADO ) ){
setPermissao( EstadoPermissao.REIVINDICADO );
}
}
public void negado(){
if( getEstado().equals( EstadoPermissao.REIVINDICADO ) ){
setPermissao( EstadoPermissao.NEGADO );
}
}
public void concedido(){
if( getEstado().equals( EstadoPermissao.REIVINDICADO ) ){
setPermissao( EstadoPermissao.CONCEDIDO );
concedido = true;
}
}
...
public EstadoPermissao getEstado(){
return permissao;
}
...
}
Note que como não trabalhamos com mais instâncias de EstadoPermissao do que as já definidas nas constantes dessa classe, podemos seguramente continuar utilizando o método equals() nas condicionais.
Podemos remover todas as constantes de tipo inseguro de dados de nossa classe PermissaoSistema e também podemos atualizar códigos clientes de PermissaoSistema.
Segue um código cliente:
public void testePadraoParaRequisicaoPermissao() {
PermissaoSistema permissao = new PermissaoSistema();
assertEquals(
permissao.REQUISITADO,
permissao.getEstado() );
assertEquals(
"REQUISITADO",
permissao.getEstado() );
}
Atualizaríamos para:
public void testePadraoParaRequisicaoPermissao() {
PermissaoSistema permissao = new PermissaoSistema();
assertEquals(
EstadoPermissao.REQUISITADO,
permissao.getEstado() );
}
A classe PermissaoSistema agora tem a seguinte interface:
public class PermissaoSistema {
private EstadoPermissao permissao;
private boolean concedido;
public PermissaoSistema(){
setPermissao( EstadoPermissao.REQUISITADO );
concedido = false;
}
public void reivindicado(){
if( getEstado().equals( EstadoPermissao.REQUISITADO ) ){
setPermissao( EstadoPermissao.REIVINDICADO );
}
}
public void negado(){
if( getEstado().equals( EstadoPermissao.REIVINDICADO ) ){
setPermissao( EstadoPermissao.NEGADO );
}
}
public void concedido(){
if( getEstado().equals( EstadoPermissao.REIVINDICADO ) ){
setPermissao( EstadoPermissao.CONCEDIDO );
concedido = true;
}
}
public boolean ehConcedido(){
return concedido;
}
public EstadoPermissao getEstado(){
return permissao;
}
public void setPermissao( EstadoPermissao permissao ){
this.permissao = permissao;
}
}
Ai você pode se pergunta:
E aquele código de construtor e variável de instância tipo em nossa classe EstadoPermissao, posso removê-lo?
Na verdade não seria uma boa escolha, pois aquela alteração ainda lhe permite ter a representação String (que poderá ser enviada a um recurso de saída de dados) de cada constante.
Com isso finalizamos a aplicação do método de refatoração Substituir Código de Tipo por Classe.
Conclusão
Esse método de refatoração tem desvantagens, são elas:
- Maior utilização de memória;
- e Maior quantidade de código no projeto.
Porém, na maioria dos casos, vale o custo benefício.
Pois as perdas com essas desvantagens são insignificantes quando comparadas aos ganhos de leitura e segurança de código.
Leitura de código também?
Sim. Por exemplo, um código utilizando EstadoPermissao.REQUISITADO diz muito mais aos developers do projeto do que somente a String "REQUISITADO".
A linha EstadoPermissao.REQUISITADO já informa implicitamente ao developer que a classe EstadoPermissao tem todos os valores constantes possíveis, com isso ele pode tomar os caminhos corretos na evolução de código que utiliza essas constantes.
Então é isso.
Por fim, não deixe de se inscrever na 📩 lista de e-mails do Blog para receber os conteúdos de desenvolvimento e codificação limpa exclusivos, em primeira mão e também...
... na versão em PDF (versão liberada somente para os inscritos da lista de e-mails).
Abraço.
Outros artigos da série
A seguir deixo a lista de todos os artigos aula já liberados desta série do Blog sobre Refatoração de Código:
- Internalizar Singleton;
- Mover Embelezamento Para Decorator;
- Substituir Condicionais que Alteram Estado por State;
- Introduzir Objeto Nulo;
- Unificar Interfaces Com Adapter;
- Extrair Adapter;
- Mover Conhecimento de Criação Para Factory;
- Substituir Notificações Hard-Coded Por Observer;
- Extrair Parâmetro;
- Unificar Interfaces;
- Limitar Instanciação Com Singleton;
- Mover Acumulação Para Parâmetro Coletor;
- Compor Method;
- Formar Template Method;
- Substituir Lógica Condicional Por Strategy;
- Introduzir Criação Polimórfica com Factory Method;
- Encapsular Classes Com Factory;
- Encadear Construtores;
- Substituir Construtores Por Métodos de Criação.
Comentários Facebook