# REFACTORING ## Code smells - Código duplicado ### Caso - 1 Neste exemplo temos dois métodos que apesar de serem diferirem, fazem o mesmo papel, ambos recebem "fields" e "labels" e verificam, se o field estiver vazio o label pode ser mostrado para o usuário. Eu fiz esta separação, pois um método recebe como parâmetro um "TextField" e outro um "PasswordField", mas pensando melhor, como a classe "PasswordField" é subclasse de "TextField" acredito que poderiamos utilizar apenas um método para ambos. ```java public static boolean textFieldsIsEmpty(List<TextField> listTf, List<Label> lLabel) { boolean test = false; for (int i = 0; i < listTf.size(); i++) { if (listTf.get(i).getText().isEmpty() || listTf.get(i).getText().replaceAll(" ", "").equals("")) { lLabel.get(i).setVisible(true); test = true; } } return test; } public static boolean passwordIsEmpty(PasswordField ps, Label error) { if (ps.getText().isEmpty() || ps.getText().replaceAll(" ", "").equals("")) { error.setVisible(true); return true; } return false; } ``` Para fazer este "refactoring" eu fiz a **remoção do código duplicado** e a **mudança de assinatura do método**, além da **renomeação de variáveis**. . ```java public static boolean verifyFieldsAreEmpty(List<TextField> listFields, List<Label> listLabels) { boolean isEmpty = false; for (int i = 0; i < listFields.size(); i++) { if (listFields.get(i).getText().isBlank()) { listLabels.get(i).setVisible(true); isEmpty = true; } } return isEmpty; } ``` ### Exemplo de funcionamento antes e depois. <img src="https://i.imgur.com/Ftjwt5c.png" alt="drawing" width="700"/> <img align="right" src="https://i.imgur.com/lAhYrPP.png" alt="drawing" width="700"/> ### Caso - 2 A seguir, temos um método duplicado em classes diferentes. ```java public class LoginViewController { /** **/ private void restartLabels() { this.lbEmailError.setVisible(false); this.lbPasswordError.setVisible(false); this.lbPasswordError.setVisible(false); } } public class CreateAccountController { /** **/ private void restartLabels() { this.lbNameError.setVisible(false); this.lbLastNameError.setVisible(false); this.lbEmailError.setVisible(false); this.lbPasswordError.setVisible(false); } } ``` Para resolver isto eu utilizei da técnica de **movimentação de métodos** e **alteração da assinatura do método**, neste caso, eu retirei estes métodos das suas respectivas classes e criei um método estático em uma classe utilitária para subustituí-los e poder reutiliza-lo em ambas as classes. ```java public class Utils { /** **/ public static void restartLabels(List<Label> list) { for (Label label : list) { label.setVisible(false); } } } ``` ## Code smells - Métodos longos ### Caso - 1 O método a seguir tem o objetivo de encontrar um número escolhido pelo usuário, com base em algumas perguntas. O mesmo está desnecessáriamente longo, logo é candidato a refatoração. ```java public static void findNumberGame(int beggin, int end){ if(beggin >= end - 1){ System.out.println("Número não encontrado"); return; } int middleNumber = beggin + ((end - beggin) / 2); Boolean isGreater = false; Scanner in = new Scanner(System.in); System.out.println("S para maior, N para menor ou qualquer coisa se achar o número"); System.out.print("O número é maior que: " + middleNumber + "? "); String response = in.nextLine(); if(response.equals("S")) isGreater = true; else if(response.equals("N")) isGreater = false; else isGreater = null; if(isGreater == null) { System.out.println("O número que vocẽ escolheu é: " + middleNumber); return; } else if(isGreater) findNumberGame(middleNumber, end); else findNumberGame(beggin, middleNumber); } ``` Utilizando a técnica de **extração de métodos**, extraímos partes deste método e criamos mais dois. "makeQuestion" e "printQuestion". Agora temos um código mais "clean". ```java public static void findNumberGame(int beggin, int end){ if(beggin >= end - 1){ System.out.println("Número não encontrado"); return; } int middleNumber = beggin + ((end - beggin) / 2); Boolean isGreater = makeQuestion(middleNumber); if(isGreater == null) { System.out.println("O número que vocẽ escolheu é: " + middleNumber); return; } else if(isGreater)findNumberGame(middleNumber, end); else findNumberGame(beggin, middleNumber); } public static Boolean makeQuestion(int number){ Scanner in = new Scanner(System.in); printQuestion(number); String response = in.nextLine(); if(response.equals("S")) return true; else if(response.equals("N")) return false; else return null; } public static void printQuestion(int number){ System.out.println("S para maior, N para menor ou qualquer coisa se achar o número"); System.out.print("O número é maior que: " + number + "? "); } ``` ### Caso - 2 No caso abaixo temos uma classe que tem como sua função receber um número e transformá-lo em um número romano. Exemplo: 3899 é MMMDCCCXCIX, 639 é DCXXXIX. O método que faz esta transformação está muito grande e como consequência, bastante complexo. Por isso iremos refatorar. ```java public class RomanNumeralsEncoder { private final HashMap<Integer, String> romanValues = fillValues(); private Integer decimalNumber; public RomanNumeralsEncoder(int number){ this.decimalNumber = number; } public String toRoman(int n) { String roman = ""; int holder = 0; int decimalNumberAux = n; HashMap<Integer, String> romanValues = fillValues(); for (Integer keys : romanValues.keySet()) { double result = (double) decimalNumberAux / keys; int resultInt = (int) result; if(result - resultInt > 0.899){ int auxKey = 0; if( keys == 5 || keys == 50 || keys == 500){ auxKey = keys - (keys - (keys / 5)); }else{ auxKey = keys - (keys - (keys / 10)); } roman += romanValues.get(keys).repeat(resultInt) + romanValues.get(auxKey) + romanValues.get(keys); decimalNumberAux = decimalNumberAux - ((keys * resultInt) + (keys - auxKey)); }else if(resultInt >= 1 && resultInt <= 3){ roman += romanValues.get(keys).repeat(resultInt); decimalNumberAux -= (keys * resultInt); }else if(resultInt > 3){ roman += romanValues.get(keys) + romanValues.get(holder); decimalNumberAux -= (holder - keys); } holder = keys; } return roman; } private HashMap<Integer, String> fillValues(){ HashMap<Integer, String> romanValuesAux = new LinkedHashMap<>(); romanValuesAux.put(1000,"M"); romanValuesAux.put(500,"D"); romanValuesAux.put(100,"C"); romanValuesAux.put(50,"L"); romanValuesAux.put(10,"X"); romanValuesAux.put(5,"V"); romanValuesAux.put(1,"I"); return romanValuesAux; } } ``` Utilizando a **extração de métodos** o código refatorado ficou assim: ```java public class RomanNumeralsEncoder { private final HashMap<Integer, String> romanValues = fillValues(); private Integer decimalNumber; public RomanNumeralsEncoder(int number){ this.decimalNumber = number; } public String toRoman() { String roman = ""; int holder = 0; Integer decimalNumberAux = this.decimalNumber; for (Integer keys : this.romanValues.keySet()) { double result = (double) decimalNumberAux / keys; int resultInt = (int) result; String [] values = handleResult(result, resultInt, roman, decimalNumberAux, keys, holder); roman = values[0]; decimalNumberAux = Integer.parseInt(values[1]); holder = values.length > 2 ? Integer.parseInt(values[2]) : holder; } return roman; } private int setAuxKeyValue(int keys){ int auxKey = 0; if (keys == 5 || keys == 50 || keys == 500) { auxKey = keys - (keys - (keys / 5)); } else { auxKey = keys - (keys - (keys / 10)); } return auxKey; } private String[] handleResult(double result, int resultInt, String roman, int decimalNumberAux, int keys, int holder){ String aux[]; if(result - resultInt > 0.899) { aux = handleGreaterThan089(keys, roman, resultInt, decimalNumberAux); }else{ aux = handleOtherValues(keys, resultInt, roman, decimalNumberAux, holder); } return aux; } private String[] handleGreaterThan089(int keys, String roman, int resultInt, Integer decimalNumberAux){ int auxKey = setAuxKeyValue(keys); roman += this.romanValues.get(keys).repeat(resultInt) + this.romanValues.get(auxKey) + this.romanValues.get(keys); decimalNumberAux = decimalNumberAux - ((keys * resultInt) + (keys - auxKey)); return new String[]{roman, Integer.toString(decimalNumberAux)}; } private String[] handleOtherValues(int keys, int resultInt, String roman, int decimalNumberAux, int holder){ if(resultInt >= 1 && resultInt <= 3){ roman += this.romanValues.get(keys).repeat(resultInt); decimalNumberAux -= (keys * resultInt); }else if(resultInt > 3){ roman += this.romanValues.get(keys) + this.romanValues.get(holder); decimalNumberAux -= (holder - keys); } holder = keys; return new String[]{roman, Integer.toString(decimalNumberAux), Integer.toString(holder)}; } private HashMap<Integer, String> fillValues(){ HashMap<Integer, String> romanValuesAux = new LinkedHashMap<>(); romanValuesAux.put(1000,"M"); romanValuesAux.put(500,"D"); romanValuesAux.put(100,"C"); romanValuesAux.put(50,"L"); romanValuesAux.put(10,"X"); romanValuesAux.put(5,"V"); romanValuesAux.put(1,"I"); return romanValuesAux; } } ``` ## Code smells Classes grandes ### Caso - 1 A seguir temos um código em kotlin que é responsável por gerar uma senha com base no seu nome. A classe responsável por isso está grande, e pode ser diminuida se fizermos a utilização de algumas ténicas de rafatoração. Exemplo de funcionamento: <img src="https://i.imgur.com/rFD3ujT.jpg" alt="drawing" height="500" width="300"/> <br> </br> Código reponsável por gerar a senha: ```kotlin /* Other methods, class etc */ fun generatePassword(name : String, onPasswordChange : (String) -> Unit){ val names = cleanSpaces(name).replace("(\\s)+","\\s").split(" ") onPasswordChange(contWords(names)) } private fun cleanSpaces(name : String) : String{ return name.trim() } private fun contWords(names : List<String>) : String{ var auxPassword = "" for(name in names){ auxPassword += contLetters(name) } return auxPassword } private fun contLetters(name : String) : String { return name.length.toString() } ``` Para fazer a refatoração destes métodos, eu irei utilizar a técnica de **métodos em linha**, ou seja, os métodos "cleanSpaces" e "contLetters" serão colocados em linha, pois são métodos curtos e não existe motivo aparente para criarmos um método específico para isso. Assim diminuindo o tamanho da nossa classe. ```kotlin fun generatePassword(name : String, onPasswordChange : (String) -> Unit){ val names = name.trim().split(" ") onPasswordChange(contWords(names)) } private fun contWords(names : List<String>) : String{ var auxPassword = "" for(name in names){ auxPassword += name.length.toString(); } return auxPassword } ``` ## Code smells - Featury Envy ### Caso - 1 Abaixo temos uma classe "Animal" que possui dois atributos: name e maxSpeed. Além disso temos uma classe "TimeToFinish" e seu método "calcTime" que recebe um "Animal" como parâmetro, e uma distância. Esse método é responsável por calcular o tempo que um "Animal" precisa para percorrer a distância espeficicada. ```java public class Animal { private String name; private Double maxSpeed; public Animal(String name, Double maxSpeed){ this.name = name; this.maxSpeed = maxSpeed; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getMaxSpeed() { return maxSpeed; } public void setMaxSpeed(Double maxSpeed) { this.maxSpeed = maxSpeed; } } public class TimeToFinish { public String calcTime(Animal animal, double kilometers){ double animalMaxSpeed = animal.getMaxSpeed(); double time = kilometers / animalMaxSpeed; return String.format("The %s can run %.2f kilometers in %.2f hours", animal.getName(), kilometers, time); } } ``` Vejamos que o método calcTime "inveja" muitos métodos da classe "Animal", logo podemos usar a **extração de métodos**, e colocarmos este método calcTime na própria classe "Animal". ```java public class Animal { private String name; private Double maxSpeed; public Animal(String name, Double maxSpeed){ this.name = name; this.maxSpeed = maxSpeed; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Double getMaxSpeed() { return maxSpeed; } public void setMaxSpeed(Double maxSpeed) { this.maxSpeed = maxSpeed; } public String calcTime(double kilometers){ double animalMaxSpeed = this.getMaxSpeed(); double time = kilometers / animalMaxSpeed; return String.format("The %s can run %.2f kilometers in %.2f hours", this.getName(), kilometers, time); } } ``` ## Code smells - Métodos com muitos parâmetros ### Caso - 1 A seguir vemos a classe "ClientServiceImpl" que implementa "ClientService", porém o método sobrescrito, chama atenção pelo número de parâmetros, ter muitos parâmetros em um método geralmente é ruim, ainda mais neste caso onde podemos facilmente encapsula-los em um classe "Client". ```java public class ClientServiceImpl implements ClientService{ private ClientDAO dao; @Override public int createClient(String name, String email, String password, int age, String phone, String street, String number) { return dao.insertClient(name, email, password, age, phone, street, number); } } ``` Neste caso faremos uma **mudança de assinatura dos métodos**, tanto o método "createClient", quanto o método "insertClient" da classe "ClientDao" terão que ser mudados. ```java public class ClientServiceImpl implements ClientService{ private ClientDAO dao; @Override public int createClient(Client client) { return dao.insertClient(client); } } ``` ## Code smells - Variáveis globais ### Caso - 1 Vemos no programa abaixo, pelo método "incrementaImpar" a utilização de uma variável global. Isso raramente é usado, pois isto viola o conceito de transparência referêncial, tendo em vista que precisamos de algo externo ao método para entendermos sua saída. ```java public class SomaImpar { private static int valor; public static void main(String[] args) { valor = 25; int[] vetor = {1,2,3,4,5,6}; incrementaImpar(vetor); printVetor(vetor); } public static void incrementaImpar(int [] vetor){ for(int i = 0; i < vetor.length; i++){ if(vetor[i] % 2 != 0) vetor[i] += valor; } } public static void printVetor(int []vetor){ for(int i = 0; i < vetor.length; i++){ System.out.print(vetor[i]+" "); } } } ``` Aqui excluiremos a variável global, e ao invés disso utilizaremos um novo parâmetro no método "incrementaImpar". ```java public class SomaImpar { public static void main(String[] args) { int[] vetor = {1,2,3,4,5,6}; incrementaImpar(vetor, 25); printVetor(vetor); } public static void incrementaImpar(int [] vetor, int valor){ for(int i = 0; i < vetor.length; i++){ if(vetor[i] % 2 != 0) vetor[i] += valor; } } public static void printVetor(int []vetor){ for(int i = 0; i < vetor.length; i++){ System.out.print(vetor[i]+" "); } } } ``` ### Caso - 2 O projeto abaixo contém a classe Product, e cada produto possui nome e valor, porém queremos adicionar um desconto a todos os produtos, e fazemos isto por meio de uma variável global. ```java public class Discont { private static Double valueOfDiscont = 0.15; public static void main(String[] args) { List<Product> listOfProducts = new ArrayList<>(); listOfProducts.add(new Product("TV",1002.25)); listOfProducts.add(new Product("Mouse",256.0)); listOfProducts.add(new Product("Notebook",4578.56)); applyDiscont(listOfProducts); } public static void applyDiscont(List<Product> listOfProducts){ for(Product product : listOfProducts){ product.setPrice(product.getPrice() - product.getPrice() * valueOfDiscont); System.out.println(product.toString()); } } } ``` Para diferenciarmos um pouco, ao invés de fazermos a mesma coisa, desta vez vamos utilizar **programação funcional** para refatorar esta classe. ```java public class Discont { public static void main(String[] args) { List<Product> listOfProducts = new ArrayList<>(); listOfProducts.add(new Product("TV",1002.25)); listOfProducts.add(new Product("Mouse",256.0)); listOfProducts.add(new Product("Notebook",4578.56)); double discont = 0.15; listOfProducts.stream().forEach((product) -> product.setPrice(product.getPrice() - product.getPrice() * discont)); listOfProducts.forEach((product -> System.out.println(product))); } } ``` ## Code smells - Obsessão por tipos primitivos ### Caso - 1 Na parte sobre métodos com muitos parâmetros, vimos que poderiamos encapsular estes parâmetros, passando o próprio client no método. Agora vamos ver como esta implementada esta classe "Client". ```java public class Client { private String name; private String email; private String password; private int age; private String phone; private String street; private String number; public Client(String name, String email, String password, int age, String phone, String street, String number) { this.name = name; this.email = email; this.password = password; this.age = age; this.phone = phone; this.street = street; this.number = number; } // getters and setters, toString() etc } ``` Esta classe "Client", tem o que chamamos de obsessão por tipos primitivos, ou seja, tem muitos atributos de tipos primitivos sem necessidade. Para fazer este "Refactoring" vamos criar três novas classes: "Address", "PhoneNumber" e "Account" ```java public class Client { private String name; private int age; private PhoneNumber phone; private Account account; private Address address; public Client(String name, int age, PhoneNumber phone, Account account, Address address) { this.name = name; this.age = age; this.phone = phone; this.account = account; this.address = address; } // getters setters toString } public class Account { private String email; private String password; public Account(String email, String password) { this.email = email; this.password = password; } // getters setters toString } public class Address { private String street; private String homeNumber; public Address(String street, String homeNumber) { this.street = street; this.homeNumber = homeNumber; } // getters setters toString } public class PhoneNumber { private String phoneNumber; public PhoneNumber(String phoneNumber) { this.phoneNumber = phoneNumber; } // getters setters toString() } ``` ## Code smells - Objetos mutáveis ### Caso - 1 Objetos mútaveis, podem ser um grande problema nas nossas aplicações, pois este objeto pode ter seu valor alterado indesejavelmente, logo sempre que pudermos deveremos optar pelo uso de objetos imutáveis. A classe "Dog" abaixo, não respeita este conceito. ```java public class Dog { private String name; private String breed; private int age; public Dog(String name, String breed, int age) { this.name = name; this.breed = breed; this.age = age; } // getters and setters } ``` Neste caso, os atributos name e breed estão ok, pois são Strings e no java Strings são imutáveis, contudo o atributo age não é, por isso iremos refatora-lo. Agora ele sera "final", ou seja, o valor de age não poderá ser mudado. ```java public class Dog { private String name; private String breed; private final int age; public Dog(String name, String breed, int age) { this.name = name; this.breed = breed; this.age = age; } // getters and setters } ``` Obs: Caso a classe "Dog", não fosse possuir nenhuma subclasse, poderiamos também deixar a classe "final". ## Code smells - Comentários ### Caso - 1 A seguir temos um código onde eu fiz alguns comentários inúteis. ```java //Buttons @FXML Button btEquals; @FXML Button btAc; @FXML Button btDelete; //TextField @FXML TextField tfResult; ``` O "refactoring" deste é bem simples, apenas fiz a **remção dos comentários**. ```java @FXML Button btEquals; @FXML Button btAc; @FXML Button btDelete; @FXML TextField tfResult; ``` ### Caso - 2 Neste caso temos um método comentado, este método é responsável por verificar se o número esta no padrão correto, porém este comentário não serve para nada, já que fica explícito na assinatura do método, o que ele faz. ```java // verifica se o número esta dentro do padrão public static Boolean veirfyIsValid(String phoneNumber){ String pattern = "\\(([0-9]{3})\\)\\s([0-9]{3})-([0-9]{4})"; return phoneNumber.matches(pattern); } ``` Para fazer o "refactoring", eu **retirei este comentário ruído e adicionei um comentário útil**. ```java // pattern = (111) 255-9867 public static Boolean veirfyIsValid(String phoneNumber){ String pattern = "\\(([0-9]{3})\\)\\s([0-9]{3})-([0-9]{4})"; return phoneNumber.matches(pattern); } ``` ## Code smells - Métodos acidentalmente complexos ### Caso - 1 No caso abaixo, temos um método cujo o objetivo é transformar os segundos recebidos como parâmetro, no formato de dias horas minutos e segundos. Exemplo: se a entrada fosse 618919 segundos a saída seria: "7 days, 3 hours, 55 minutes and 19 seconds". Porém esse método está extremamente complexo, por isso iremos fazer a refatoração do mesmo. ```java public static String format(int seconds){ String userMessage = ""; if(seconds <= 0) return "now"; else { HashMap<Integer, String> timeInstances = new LinkedHashMap<>(); timeInstances.put(31536000,"years"); timeInstances.put(86400,"days"); timeInstances.put(3600,"hours"); timeInstances.put(60,"minutes"); int cont = timeInstances.size() - 1; for(Integer keys : timeInstances.keySet()) { double convertedSeconds = seconds / keys; if (convertedSeconds >= 1) { String timeUnit = timeInstances.get(keys); int auxValue = (Math.round((int) convertedSeconds)); String formatedTimeUnit = auxValue != 1 ? timeUnit : timeUnit.substring(0, timeUnit.length() - 1); userMessage += auxValue + " " + formatedTimeUnit + ", "; seconds = seconds % keys; } } if(userMessage != ""){ if(userMessage.charAt(userMessage.length() - 1) == ' ') userMessage = userMessage.substring(0, userMessage.length() - 2) + " "; } if(seconds != 0){ seconds = Math.round(seconds); String timeUnit = seconds > 1 ? "seconds" : "second"; if(userMessage.equals("")) userMessage += seconds + " " + timeUnit; else userMessage += "and " + seconds + " " + timeUnit; }else if(userMessage.length() > 10){ int indexOfLastComma = userMessage.lastIndexOf(','); userMessage = userMessage.substring(0, indexOfLastComma) + " and" + userMessage.substring(indexOfLastComma + 1, userMessage.length()); } } return userMessage.trim(); } ``` Aqui faremos uma **extração de método**, além disso iremos **simplificar a lógica do método.** ```java public static String format(int seconds){ if(seconds == 0) return "now"; String [] formatedTimes = fillFormatedValues(seconds); return Arrays.stream(formatedTimes) .filter(time -> time != "") .collect(Collectors.joining(", ")) .replaceAll(", (?!.+,)", " and "); } public static String[] fillFormatedValues(int seconds){ String[] formatedTimes = new String[]{ formatTime("year", (seconds / 31536000)), formatTime("day", (seconds / 86400) % 365), formatTime("hour", (seconds / 3600) % 24), formatTime("minute", (seconds / 60) % 60), formatTime("second", (seconds % 3600) % 60) }; return formatedTimes; } public static String formatTime(String s, int time){ return time==0 ? "" : Integer.toString(time)+ " " + s + (time==1?"" : "s"); } ``` ### Caso - 2 Na situação abaixo, assim como na de cima, temos um método difícil de entender, apesar desta situação ser bem melhor do que a anterior, ainda sim, podemos fazer o refactoring neste caso também. Este método é responsável por inverter as palavras de uma frase recebida como parâmetro, deste que a mesma tenha 5 ou mais letras. Exemplo: "Hello World", "olleH dlroW". ```java public String spinWords(String sentence) { String[] splittedWords = sentence.split(" "); StringBuilder builder = new StringBuilder(); for (String word : splittedWords) { if (word.length() > 4) { StringBuilder tempBuilder = new StringBuilder(); tempBuilder.append(word).reverse(); builder.append(tempBuilder).append(" "); } else { builder.append(word).append(" "); } } builder.deleteCharAt(builder.length() - 1); return builder.toString(); } ``` Utilizando a **extração de métodos** e **mudando um pouco a lógica** usada temos : ```java public String spinWords(String sequence) { List<String> words = List.of(sequence.split(" ")); List<String> reversedWords = words.stream().map(SpinWords::reverse).collect(Collectors.toList()); return String.join(" ",reversedWords); } public static String reverse(String word){ if(word.length() >= 5) return new StringBuilder(word).reverse().toString(); return word; } ``` ## Débito técnico - Nomes fora do padrão Não é novidade para ninguém que, fazer um código que um computador entenda é muito mais fácil do que fazer um código que outro programador entenda. Tendo isto em vista, nós programadores seguimos algumas convenções de nomes de classes, pacotes e variáveis. A seguir mostrarei o refactoring de nomes de classes fora do padrão. ### Caso - 1 Na imagem abaixo, podemos ver que todos os arquivos .fxml estão seguindo um padrão de camelCase, porém um deles está no padrão de snake_case. Isto contudo não é uma boa prática, logo este arquivo é um canditado a refatoração. <img src="https://i.imgur.com/Ygxba4D.png" alt="drawing" width="700"/> <br></br> Temos duas opções aqui, a padrão que seria refatorar o arquivo para "tableContentView.fxml", e a outra trocar todos os outros para o formato snake_case, como em um projeto de grande porte a segunda opção seria inviável, faremos a primeira mesmo. ![](https://i.imgur.com/xTFr9V5.png) ### Caso - 2 Seguindo o mesmo projeto anterior, o nome das classes controller também estão em desordem, tendo em vista que algumas estão com o padrão: "nome + View + Controller", e outras estão com "nome + Controller". Exemplos: CreateAccountController e MessageViewController. Contudo vamos refatorar e iremos padronizar todos os nomes de classes que são "controladoras". ![](https://i.imgur.com/aqz1ENe.png) ## Débito Técnico - Classes não testadas ### Caso - 1 O código a seguir já foi visto anteriormente, porém mesmo após o refactoring do mesmo ainda precisamos testa-lo. ```java public class RomanNumeralsEncoder { private final HashMap<Integer, String> romanValues = fillValues(); private Integer decimalNumber; public RomanNumeralsEncoder(int number){ this.decimalNumber = number; } public String toRoman() { String roman = ""; int holder = 0; Integer decimalNumberAux = this.decimalNumber; for (Integer keys : this.romanValues.keySet()) { double result = (double) decimalNumberAux / keys; int resultInt = (int) result; String [] values = handleResult(result, resultInt, roman, decimalNumberAux, keys, holder); roman = values[0]; decimalNumberAux = Integer.parseInt(values[1]); holder = values.length > 2 ? Integer.parseInt(values[2]) : holder; } return roman; } private int setAuxKeyValue(int keys){ int auxKey = 0; if (keys == 5 || keys == 50 || keys == 500) { auxKey = keys - (keys - (keys / 5)); } else { auxKey = keys - (keys - (keys / 10)); } return auxKey; } private String[] handleResult(double result, int resultInt, String roman, int decimalNumberAux, int keys, int holder){ String aux[]; if(result - resultInt > 0.899) { aux = handleGreaterThan089(keys, roman, resultInt, decimalNumberAux); }else{ aux = handleOtherValues(keys, resultInt, roman, decimalNumberAux, holder); } return aux; } private String[] handleGreaterThan089(int keys, String roman, int resultInt, Integer decimalNumberAux){ int auxKey = setAuxKeyValue(keys); roman += this.romanValues.get(keys).repeat(resultInt) + this.romanValues.get(auxKey) + this.romanValues.get(keys); decimalNumberAux = decimalNumberAux - ((keys * resultInt) + (keys - auxKey)); return new String[]{roman, Integer.toString(decimalNumberAux)}; } private String[] handleOtherValues(int keys, int resultInt, String roman, int decimalNumberAux, int holder){ if(resultInt >= 1 && resultInt <= 3){ roman += this.romanValues.get(keys).repeat(resultInt); decimalNumberAux -= (keys * resultInt); }else if(resultInt > 3){ roman += this.romanValues.get(keys) + this.romanValues.get(holder); decimalNumberAux -= (holder - keys); } holder = keys; return new String[]{roman, Integer.toString(decimalNumberAux), Integer.toString(holder)}; } private HashMap<Integer, String> fillValues(){ HashMap<Integer, String> romanValuesAux = new LinkedHashMap<>(); romanValuesAux.put(1000,"M"); romanValuesAux.put(500,"D"); romanValuesAux.put(100,"C"); romanValuesAux.put(50,"L"); romanValuesAux.put(10,"X"); romanValuesAux.put(5,"V"); romanValuesAux.put(1,"I"); return romanValuesAux; } } ``` Para fazermos isto, eu criei uma classe de teste, e fiz alguns testes para previnir futuros "bugs". ```java public class RomanNumeralsEncoderTest { public static RomanNumeralsEncoder encoder; @BeforeClass public static void setUp() { encoder = new RomanNumeralsEncoder(); } @Test public void when9MustBeEqualsToIX(){ encoder.setDecimalNumber(9); String romanNumeral = encoder.toRoman(); assertEquals("IX",romanNumeral); } @Test public void when40MustBeEqualsToXL(){ encoder.setDecimalNumber(40); String romanNumeral = encoder.toRoman(); assertEquals("XL",romanNumeral); } @Test public void when3865MustBeEqualsToMMMDCCCLXV(){ encoder.setDecimalNumber(3865); String romanNumeral = encoder.toRoman(); assertEquals("MMMDCCCLXV",romanNumeral); } } ``` ### Caso - 2 Outra classe que já vimos antes. ```java public static String format(int seconds){ if(seconds == 0) return "now"; String [] formatedTimes = fillFormatedValues(seconds); return Arrays.stream(formatedTimes).filter(time -> time != "").collect(Collectors.joining(", ")).replaceAll(", (?!.+,)", " and "); } public static String[] fillFormatedValues(int seconds){ String[] formatedTimes = new String[]{ formatTime("year", (seconds / 31536000)), formatTime("day", (seconds / 86400) % 365), formatTime("hour", (seconds / 3600) % 24), formatTime("minute", (seconds / 60) % 60), formatTime("second", (seconds % 3600) % 60) }; return formatedTimes; } public static String formatTime(String s, int time){ return time==0 ? "" : Integer.toString(time)+ " " + s + (time==1?"" : "s"); } ``` Mas agora iremos testa-la. ```java public class TimeFormmaterTest { @Test public void when0SholdBeNow(){ Assert.assertEquals("now", TimeFormmater.format(0)); } @Test public void when1ShouldBe1second(){ Assert.assertEquals("1 second", TimeFormmater.format(1)); } @Test public void when2ShouldBe2seconds(){ Assert.assertEquals("2 seconds", TimeFormmater.format(2)); } @Test public void when152ShouldBe2minutesand32seconds(){ Assert.assertEquals("2 minutes and 32 seconds", TimeFormmater.format(152)); } } ```