Refatoração na prática

04:01 Vinicius Knob 0 Comments


"Pensar é o trabalho mais pesado que existe. 
Talvez seja por isso que tão poucos o exercitem."
(Henry Ford)

Ser pragmático tem seu valor. Sempre me importei com o código, gosto de ser assim, pois conheço o real valor dessa ação, ou melhor, dessa conduta. Ser pragmático envolve muitos conceitos e acima de tudo, prática. Nesse post quero demonstrar uma prática muito conhecida de quem é pragmático que é a refatoração de código.

Refatorar código não é tão simples assim, exige conhecimento da lógica de negócio e pode ter resultados catastróficos se for feito por alguém que não possua uma visão macro do projeto e possa visualizar todas as dependências possíveis (lembrando que mesmo com os mecanismos de busca das IDEs muita informação está em locais inacessíveis para estas).



O código a seguir envolve um caso onde um controller é responsável por receber dados para o envio de um e-mail. Esse controller deve validar os dados de entrada, montar o e-mail e enviá-lo. Vou mostrar o código “cru” e então começarei a refatorá-lo. Passo a passo pretendo descrever conceitos que estão sendo atendidos e outros que possivelmente estão sendo violados. Será utilizada a linguagem Java para isso.

Case 1
import java.util.Date;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class Case1 {

    public void abstractController() throws Exception {
        
        try {
            // dados recebidos de uma fonte qualquer
            String de = "eu@email.com";
            String para = "qualquer@email.com";
            String assunto = "Refactoring na Prática";
            String corpo = "Corpo da Mensagem";
            
            // validacao de email
            Pattern p = Pattern.compile(".+@.+\\.[a-z]+");  
            
            Matcher m1 = p.matcher(de);  
            if (!m1.matches())  
                throw new IllegalArgumentException("Email address is invalid: \"" + de + "\"");
            
            Matcher m2 = p.matcher(para);  
            if (!m2.matches())  
                throw new IllegalArgumentException("Email address is invalid: \"" + para + "\"");
            
            Properties props = new Properties();
            props.put("mail.host", "127.0.0.1");
            Session session = Session.getInstance(props);
            MimeMessage msg = new MimeMessage(session);
            try {
                msg.setFrom(new InternetAddress(de));
                InternetAddress[] to = {new InternetAddress(para)};
                msg.setRecipients(Message.RecipientType.TO, to);
                msg.setSentDate(new Date());
                msg.setSubject(assunto);
                msg.setText(corpo);
                Transport.send(msg);
            } catch (MessagingException e) {
                // omitido
            }
        } catch (Exception e) {
            // omitido
        }
        
    }

}
A classe Case1 é o primeiro caso que quero mostrar. Em seu método abstractController ela executa a lógica de receber os atributos que identificam um e-mail, validar alguns deles, montar o e-mail utilizando a biblioteca javax.mail, então por fim, enviar o e-mail. Um método com muitas responsabilidades.

Isso funciona?

Sim (se você tiver o SMTP configurado em sua máquina como um servidor local). Acredito que nesse ponto se você não for alguém pragmático esse código é algo normal, mas para mim é um monstro. Quando estudei software a primeira coisa que aprendi lá em Algoritmos é que devo pegar um problema e fragmentá-lo, então resolver os problemas menores sendo que ao final terei resolvido o problema todo. O código acima para mim é um problema e que necessita de fragmentação.

Case 2

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class Case2 {
    
    /**
     * Refactoring:
     * - Criacao da classe EmailAddress
     */
    public void abstractController() {
        
        EmailAddress de = new EmailAddress("eu@email.com");
        EmailAddress para = new EmailAddress("qualquer@email.com");
        
        String assunto = "Refactoring na Prática";
        String corpo = "Corpo da Mensagem";
        
        Properties props = new Properties();
        props.put("mail.host", "127.0.0.1");
        Session session = Session.getInstance(props);
        MimeMessage msg = new MimeMessage(session);
        try {
            msg.setFrom(de.toInternetAddress());
            InternetAddress[] to = {para.toInternetAddress()};
            msg.setRecipients(Message.RecipientType.TO, to);
            msg.setSentDate(new Date());
            msg.setSubject(assunto);
            msg.setText(corpo);
            Transport.send(msg);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}
Nesse ponto do código, uma classe extra foi criada com a intenção de centralizar o e-mail bem como sua validação. A classe EmailAddress tem a responsabilidade de fazer apenas isso, representar um e-mail. Vale lembrar que em um refactoring não se pode alterar o fluxo que a aplicação já possui, pois isso seria tarefa em uma correção ou manutenção. A classe EmailAddress segue o padrão Tiny Type, que são pequenos tipos de objeto que encapsulam pouca lógica.

EmailAddress

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;

/**
 * Tiny Type Pattern
 *
 * @author Vinicius Martins Knob
 */
public final class EmailAddress {

    private final InternetAddress _emailAddress;

    /**
     * Cria um EmailAddress a partir de um email
     * @param emailAddress
     * @throws AddressException
     */
    public EmailAddress(String emailAddress) throws AddressException {
        validar(emailAddress);
        _emailAddress = new InternetAddress(emailAddress);
    }

    private void validar(String emailAddress) {
        Pattern p = Pattern.compile(".+@.+\\.[a-z]+");  
        Matcher m = p.matcher(emailAddress);  
        if (!m.matches())  
            throw new IllegalArgumentException("Email address is invalid: \"" + emailAddress + "\"");
    }
    
    @Override
    public String toString() {
        return _emailAddress.getAddress();
    }
    
    public InternetAddress toInternetAddress(){
        return _emailAddress;
    }
    
    /**
     * Retorna uma lista imutavel de EmailAddress a partir de um array de emails.
     * @param emails
     * @return
     * @throws AddressException
     */
    public static List<EmailAddress> asList(String... emails) throws AddressException {
        if (emails.length == 0)
            return Collections.emptyList();
        
        List<EmailAddress> foo = new ArrayList<EmailAddress>();
        for (String bar : emails) {
            foo.add(new EmailAddress(bar.trim()));
        }
        return Collections.unmodifiableList(foo);
    }
}
O que ganho com isso?

Em um projeto grande, ter aquele trecho de código responsável por validar o e-mail espalhado por toda parte seria inadmissível. Uma alternativa encontrada muitas vezes por mim em outros projetos é uma classe com métodos estáticos onde exista um método validarEmail(), não que seja errado, mas quando criei a classe EmailAddress estive pensando em conceitos muito discutidos pela comunidade de software como coesão, responsabilidade única, legibilidade e baixo acoplamento. EmailAddress é uma classe que quando você vê tem plena noção de seus limites e responsabilidades, pois ela é simples e clara.

Case 3
import java.util.Date;
import java.util.Properties;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

public class Case3 {

    /**
     * Refactoring:
     * - Criacao da classe Email
     */
    public void abstractController() {

        EmailAddress de = new EmailAddress("eu@email.com");
        EmailAddress para = new EmailAddress("qualquer@email.com");

        Email email = Email.create()
                .from(de)
                .to(para)
                .subject("Refactoring na Prática")
                .body("Corpo da Mensagem")
            .build();

        Properties props = new Properties();
        props.put("mail.host", "127.0.0.1");
        Session session = Session.getInstance(props);
        MimeMessage msg = new MimeMessage(session);
        try {
            msg.setFrom(de.toInternetAddress());
            InternetAddress[] to = {para.toInternetAddress()};
            msg.setRecipients(Message.RecipientType.TO, to);
            msg.setSentDate(new Date());
            msg.setSubject(email.getSubject());
            msg.setText(email.getBody());
            Transport.send(msg);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
}
No caso 3 externalizei uma segunda parte da lógica, a montagem do e-mail. A montagem consiste na coleta dos dados e suas respectivas validações. Para isso criei a classe Email. Ela segue o padrão Builder.

Email

import java.text.MessageFormat;
import java.util.List;

/**
 * 
 * Esta classe segue o Builder Pattern
 *
 * @author Vinicius Martins Knob
 */
public class Email {
    
    private EmailAddress _from;
    private List<EmailAddress> _to;
    private String _subject;
    private String _body;
    
    private Email() {}
    
    public EmailAddress getFrom() {
        return _from;
    }
    public List<EmailAddress> getTo() {
        return _to;
    }
    public String getSubject() {
        return _subject;
    }
    public String getBody() {
        return _body;
    }
    
    @Override
    public String toString() {
        String pattern = "[from: {0}, to: {1}, subject: {2}, body: {3}]";
        Object[] data = {_from, _to.toString(), _subject, _body};
        return MessageFormat.format(pattern, data);
    }
    
    @Override
    public boolean equals(Object obj) {
        return this.toString().equals(obj.toString());
    }

    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result * (_from==null?0:_from.hashCode());
        result = 31 * result * (_to==null?0:_to.hashCode());
        result = 31 * result * (_subject==null?0:_subject.hashCode());
        result = 31 * result * (_body==null?0:_body.hashCode());
        return result;
    }
    
    /**
     * 
     * Builder Method - Builder Pattern
     * 
     */
    public static EmailBuilder create() {
         return new EmailBuilder();
    }
    
    /**
     * 
     * Builder Class - Builder Pattern
     * <br>Inner Class
     *
     */
    public static class EmailBuilder{
        private final Email _email = new Email();
        
        private EmailBuilder(){}
        
        public EmailBuilder from(EmailAddress from){
            _email._from = from;
            return this;
        }
        public EmailBuilder to(List<EmailAddress> to){
            _email._to = to;
            return this;
        }
        public EmailBuilder subject(String subject){
            _email._subject = subject;
            return this;
        }
        public EmailBuilder body(String body){
            _email._body = body;
            return this;
        }
        public Email build(){
            return _email;
        }
        
    }
    
}
A classe Email contempla o padrão Builder, padrão aplicado em virtude da quantidade de parâmetros utilizados, e a tendência dessa classe, dependendo do sistema, é aumentar a quantidade desses parâmetros. Cada método poderia conter a validação respectiva ao dado inserido, centralizando toda a lógica apenas em uma classe, para todo sistema. A classe Email faz a coleta e validação de dados de um e-mail, e só isso. Os atributos atendidos pela classe EmailAddress são também atendidos aqui: coesão, responsabilidade única, legibilidade e baixo acoplamento.

Case 4

import java.util.Arrays;

public class Case4 {
    
    /**
     * Refactoring: 
     * - Criacao da classe Sender
     */
    public void abstractController() {
        try {
            EmailAddress de = new EmailAddress("eu@email.com");
            EmailAddress para = new EmailAddress("qualquer@email.com");

            Email email = Email.create()
                    .from(de)
                    .to(Arrays.asList(para))
                    .subject("Refactoring na Prática")
                    .body("Corpo da mensagem")
                .build();

            Sender.send(email);
        } catch (Exception e) {
            // log
        }

    }
}

No caso 4, uma terceira lógica foi movida do controller para uma classe específica, a classe Sender. Essa classe passa a ser responsável pelo envio do e-mail, e somente isso. Ela encapsula boa parte da API de envio de e-mail do java (javax.mail) e possui apenas um método estático, send(email). O parâmetro de send é um objeto do tipo Email, a classe criada no caso 3. O contrutor de Sender lança uma UnsupportedOperationException, pois não existe motivos para instanciar essa classe, ela foi projetada para apenas enviar um e-mail, não armazenando dado algum. O seu método private getArrayAddress efetua uma conversão da lista de EmailAddress para um array de InternetAddress, exigência esta imposta pela API javax.mail.

Sender

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Properties;

import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.naming.NamingException;

/**
 * <p>
 * Classe responsavel por enviar um email.
 * </p>
 * <p>
 * Classe nao-instanciavel, seu unico proposito eh enviar um email,
 * funcionalidade essa fornecidade pelo metodo {@link #send(Email)}.
 * </p>
 * <p>
 * <b>OBS:</b> Classe nao projetada para heranca.
 * </p>
 * 
 * @author Vinicius Martins Knob
 * 
 */
public final class Sender {
    
    private Sender() {
        throw new UnsupportedOperationException("classe nao-instanciavel");
    }
    
    public static void send(Email email) throws NamingException {
        Properties p = new Properties();
        p.put("mail.host", "127.0.0.1");
        Session session = Session.getInstance(p);
        MimeMessage msg = new MimeMessage(session);
        try {
            msg.setFrom(email.getFrom().toInternetAddress());
            msg.setRecipients(Message.RecipientType.TO, getArrayAddress(email.getTo()));
            msg.setSentDate(new Date());
            msg.setSubject(email.getSubject());
            msg.setText(email.getBody());
            Transport.send(msg);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }
    
    private static InternetAddress[] getArrayAddress(List<EmailAddress> emails){
        List<InternetAddress> internetAddresses = new ArrayList<InternetAddress>();
        for (EmailAddress address : emails) {
            internetAddresses.add(address.toInternetAddress());         
        }
        return internetAddresses.toArray(new InternetAddress[internetAddresses.size()]);
    }
}

Conclusão

Acredito que com essa refatoração o controller ficou muitas vezes mais legível, fácil de efetuar uma alteração já que temos as partes do processo bem definidas. As classes geradas por esta refatoração são totalmente reutilizáveis, flexíveis e coesas. Nem sempre é possível efetuar uma refatoração de código, pois esta ação exige conhecimento amplo. Um ambiente preparado para testes seria ideal para manter uma rotina de refatoração diminuindo significamente partes repetidas de um sistema que utiliza uma linguagem orientada a objetos. Conceitos aplicados aqui, e muitos deles posso ter passado por cima, deveria sempre ser aplicados, desde o inicio.

0 comentários: