0

Refatorando => Desafios avançado 1/3 de C#. Melhorando a qualidade e legibilidade do código. Aplicando Clean Code e Refactoring.

#.NET Core
CARLOS SILVA
CARLOS SILVA

Olá caro leitor,


Todo código é passível a ser refatorado e melhorado.


Nos desafios de código dos bootcamps, recebemos um breve escopo do desafio, com as regras de negócio e exemplos de input e output. E para ajudar mais ainda, recebemos um modelo de algoritmo com partes a serem preenchidas com a nossa solução. Isso é bom, principalmente para os Devs iniciantes, e até mesmo que já é mais avançados, por que dá uma ideia inicial da solução.


Manter um código limpo, modular, fácil de entender, modificar e testar, é uma habilidade que devemos praticar constantemente. No começo, vamos cometer muitos erros e isso é normal, o segredo está em não desistir. Dois livros que eu recomendo a leitura, são os livros: clean code e refactoring, eles fornecem dicas imprescindíveis para tornar uma base de código limpa, profissional e elegante.


A primeira vista, parece que o código ficou mais complicado, maior e era melhor manter o anterior, porém caso um dia precisássemos ampliar o nosso programa, ou até mesmo modificá-lo, os benefícios ficariam perceptíveis.


Decidi aplicar algumas das técnicas contidas nesses dois livros, e refatorar as estruturas dos algoritmos dos desafios, à medida que entendo melhor o que estou fazendo, a lógica de negócio e o algoritmo que estou escrevendo, aprendo a agrupar melhor os elementos de software. Algumas das técnicas que utilizei foram:

  • Extract Class (refactoring)
  • Extract Function (refactoring)
  • Introduce Parameter Object (refactoring)
  • Decompose Conditional (refactoring)
  • Replace Constructor with Factory Function (refactoring) = Factory (Pattern)
  • DIP (SOLID)
  • Independency Injection
  • Unit Test


Antes de ir ao código original, vamos olhar a estrutura do código sem refatoração.


class Program{
        public void ExecutarDesafio() {
            string[] valores = Console.ReadLine().Split(' ');
            double x = double.Parse(valores[0]);
            double y = double.Parse(valores[1]);

            if (x == 0 && y == 0)
                Console.WriteLine("Origem");
            else if (x == 0 && (y > 0 || y < 0))
                Console.WriteLine("Eixo Y");
            else if ((x > 0 || x < 0) && y == 0)
                Console.WriteLine("Eixo X");
            else if (x > 0 && y > 0)
                Console.WriteLine("Q1");
            else if (x < 0.0 && y > 0.0)
                Console.WriteLine("Q2");
            else if (x < 0.0 && y < 0.0)
                Console.WriteLine("Q3");
            else
                Console.WriteLine("Q4");
        }
    }


Nessa própria estrutura, já poderíamos aplicar um Extract Function e Decompose Conditional, veja como ficaria mais simples entender as condicionais ifs passando os nomes das funções que indicam o seu significado. Utilizei a convenção Is para o nome das funções que retornam um bool.


            if (IsPointOrigen())
                Console.WriteLine("Origem");
            else if (IsPointAxisY())
                Console.WriteLine("Eixo Y");
            else if (IsPointAxisX())
                Console.WriteLine("Eixo X");
            else if (IsPointQuadrant1())
                Console.WriteLine("Q1");
            else if (IsPointQuadrant2())
                Console.WriteLine("Q2");
            else if (IsPointQuadrant3())
                Console.WriteLine("Q3");
            else
                Console.WriteLine("Q4");
            
            private static bool IsPointOrigen() {
                if (_coordinates.X == 0 && _coordinates.Y == 0)
                  return true;
                return false;
            }
            ...

Essa técnica já ajudaria a deixar o código mais legível.


Extract Class


O código original, estava todo contido no métedo Main da classe Program, então analisando o enunciando, pude visualizar dois objetos de domínio "Plano cartesiano e Coordenadas" onde apliquei Extract Class, o que gerou duas classes: Coordinates e CartesianPlane .


interface ICoordinates


Particularmente eu sempre que possível eu procuro aplicar interfaces para minhas classes, e foi isso que eu com para a classe Coordinates.


public interface ICoordinates {
   double X { get; set; }
   double Y { get; set; }
}


class Coordinates


Na classe Coordinates, foi criado duas propriedades que representa-se as coordenadas do X e Y no plano cartesiano e encapsulassem em propriedades as variáveis internas x e y do código original. Inicialmente ficou bem simples, mas ela pode se melhorada, colocando restrições e validações, porém para esse exemplo não vi necessidade.


Estrutura da classe:

public class Coordinates : ICoordinates 
{
  public double X { get; set; }
  public double Y { get; set; }

  public Coordinates(double x, double y)
  {
    X = x;
    Y = y;
  }
}


Criei um construtor que força a passagem dos parâmetros obrigatórios X e Y.


CartesianPlane


Na classe CartesianPlane, foi onde eu transferi toda a regra de negócio e condicionais do código anterior. Dava para melhorar, criando classes e propriedades como por exemplo: Axes, Quadrant, Origin e etc, mas por enquanto decidi deixar nesse estado.


Estrutura completa da classe.

public class CartesianPlane {
        
        private string result;
        private readonly ICoordinates _coordinates;

        public CartesianPlane(ICoordinates coordinates) {
            _coordinates = coordinates;
        }

        public string GetCoordinatePoint() {

            if (IsPointOrigen())
                result = "Origem";

            if (IsPointAxisX())
                result = "Eixo X";

            if (IsPointAxisY())
                result = "Eixo Y";

            if (IsPointQuadrant1())
                result = "Q1";

            if (IsPointQuadrant2())
                result = "Q2";

            if (IsPointQuadrant3())
                result = "Q3";

            if (IsPointQuadrant4())
                result = "Q4";

            return result;
        }

        public bool IsPointOrigen() {
            if (_coordinates.X == 0 && _coordinates.Y == 0)
                return true;
            return false;
        }

        public bool IsPointAxisY() {
            if (_coordinates.X == 0 && _coordinates.Y != 0)
                return true;
            return false;
        }

        public bool IsPointAxisX() {
            if (_coordinates.X != 0 && _coordinates.Y == 0)
                return true;
            return false;
        }

        public bool IsPointQuadrant1() {
            if (_coordinates.X > 0 && _coordinates.Y > 0)
                return true;
            return false;
        }

        public bool IsPointQuadrant2() {
            if (_coordinates.X < 0 && _coordinates.Y > 0)
                return true;
            return false;
        }
        public bool IsPointQuadrant3() {
            if (_coordinates.X < 0 && _coordinates.Y < 0)
                return true;
            return false;
        }
        public bool IsPointQuadrant4() {
            if (_coordinates.X > 0 && _coordinates.Y < 0)
                return true;
            return false;
        }
    }


Agora vamos compreender algumas caracteristicas da classe.


Construí apenas duas propriedades, que são elas:


Propriedades

private string result;
private ICoordinates _coordinates;


A propriedade result, serviu para guardar os resultados dos testes condicionais, e _coordinates serviu para armazenar os valores de referencia das coordenadas X e Y.


Note que _coordinates é do tipo ICoordinates, isso é extremamente útil para realizarmos a Injeção de dependência via construtur.


Construtor

public CartesianPlane(ICoordinates coordinates) {
  _coordinates = coordinates;
}


No construtor da classe, criei um parâmetro do tipo ICoordinates que irá conter as referências das coordendas X e Y passadas pelo cliente, e atribuir à propriedade interna _coordinates via Injeção de dependência.


Metódos


O metódo GetCoordinatePoint() possui "quase" a mesma lógica que o código original, aqui cabe uma ressalva: dependendo do tipo de projeto, e da quantidade/necessidade de mais testes "ifs", é necessário aplicar mais algumas técnicas de Refactoring e principalmente o parttern Strategy para nos livrar dessa quantidade de ifs.



        public string GetCoordinatePoint() {

            if (IsPointOrigen())
                result = "Origem";

            if (IsPointAxisX())
                result = "Eixo X";

            if (IsPointAxisY())
                result = "Eixo Y";

            if (IsPointQuadrant1())
                result = "Q1";

            if (IsPointQuadrant2())
                result = "Q2";

            if (IsPointQuadrant3())
                result = "Q3";

            if (IsPointQuadrant4())
                result = "Q4";

            return result;
        }
 

O metódo IsPointOrigen() avalia se as duas coordenadas são iguais a zero, caso positivo a coordena é o ponto de origem.


        public bool IsPointOrigen() {
            if (_coordinates.X == 0 && _coordinates.Y == 0)
                return true;
            return false;
        }


O metódo IsPointAxisY() segue a mesma lógica, executando testes nas coordenas para identificar a qual quadrante e ou se as coordenadas estão na linha dos eixos.


        public bool IsPointAxisY() {
            if (_coordinates.X == 0 && _coordinates.Y != 0)
                return true;
            return false;
        }


Lógicas condicionais complexas.

else if (x == 0 && (y > 0 || y < 0))


Veja essa lógica condicional acima, meio confusa não é? Vamos deixar mais simples com uma simples trocar de operando.

else if (x == 0 && (y != 0))


Ficou mais simples, e reduziu uma condicional. Leia a primeira versão ou y é > "maior" que zero, || "ou" y < "menor" que zero. Não seria mais fácil dizer que y != "é diferente" de zero?


Não use Else sem ter real certeza de qual valor irá ser retornado.


Veja o teste que retorna o quadrante 4:

            ...
            else if (x < 0.0 && y > 0.0)
                Console.WriteLine("Q2");
            else if (x < 0.0 && y < 0.0)
                Console.WriteLine("Q3");
            else
                Console.WriteLine("Q4");
            ...


É um código que se acontece algo de errado no meio do caminho, ou se nem todas as condicionais foram tratadas acima, corre o risco dele atribuir valores errados.


A solução é ser explicito no tipo de condicional, conforme descrito no metódo IsPointQuadrant4() abaixo

        public bool IsPointQuadrant4() {
            if (_coordinates.X > 0 && _coordinates.Y < 0)
                return true;
            return false;
        }


Factory


Eu uso em quase todos os projetos o padrão Factory para abstrair a construção dos meus objetos. Nesse projeto eu criei duas Factorys "CoordinatesFactory e CartesianPlaneFactory".


Vamos dar uma olhada na estrutura das duas classes:

CartesianPlaneFactory

    public class CartesianPlaneFactory {
        public static CartesianPlane Create(ICoordinates coordinates) {
            return new CartesianPlane(coordinates);
        }
    }

CoordinatesFactory

    public class CoordinatesFactory {
        public static ICoordinates Create(double x, double y) {
            return new Coordinates(x, y);
        }
    }


E por último e não menos importante, é a chamada desses metódos no cliente "Program"


class Program {

  static void Main(string[] args) { 
    
    string[] valores = Console.ReadLine().Split(' ');
    double x = double.Parse(valores[0], CultureInfo.InvariantCulture);
    double y = double.Parse(valores[1], CultureInfo.InvariantCulture);
        
    var coordinates = CoordinatesFactory.Create(x, y);
    var result =     
    CartesianPlaneFactory.Create(coordinates).GetCoordinatePoint();
    
    Console.WriteLine(result);
    
  }
}


Agora, vamos dar início a construção dos testes unitários, pois um software que não se testa, é mais vulnerável a falhas. Para isso vamos utilizar o xUnit que é mais simples de utilização, mas para isso vamos deixar para o próximo artigo, para não ficar muito extenso.


Veja como está ficando a cobertura de teste:

Pode ser uma imagem de sentando


Então é isso pessoal, vou ficando por aqui.


Um abraço,


Atenciosamente,

Carlos Antonio.

https://carlosantoniocison.editorx.io/portifolio

0
7

Comentários (0)

Administrador de Empresas | Desenvolvedor C# Júnior | Um apaixonado por tecnologia, meio ambiente e solução de problemas.

Brasil