Aprofundando em "Code Smells" e refatoração de código

Aprofundando em "Code Smells" e refatoração de código

Tem algo que não “cheira bem” no seu código? Humm, isso pode ser um sintoma de um problema mais profundo, de um possível bug, complexidade ou vulnerabilidades na sua aplicação. Ela pode não estar rodando em perfeitas condições…

"Code smells" são sinais indicativos de que um pedaço de código pode estar mal projetado ou mal implementado. Eles podem indicar problemas como complexidade excessiva, acoplamento excessivo, falta de coesão, duplicação de código, falta de modularidade, entre outros. Esses problemas podem tornar o código difícil de ler, entender, manter e evoluir.

Os code smells não são necessariamente erros ou bugs no código, mas são mais como pistas de que o código pode precisar de melhorias. Identificar e corrigir code smells é uma parte importante do processo de refatoração, que é o processo de melhorar a estrutura interna do código sem alterar seu comportamento externo. A refatoração pode ajudar a melhorar a qualidade do código, tornando-o mais fácil de entender, manter e evoluir.

Este termo surgiu pela primeira vez por Kent Beck no final da década de 1990 e se popularizou no livro Refactoring: Improving the Design of Existing Code de Martin Fowler de 1999.

E como prevenir é melhor do que remediar, vou abordar neste artigo como reconhecer e lidar com os Code Smells com técnicas de Refactoring (“Alteração feita na estrutura interna do software para torná-lo mais fácil de ser entendido e menos custoso de ser modificado, sem alterar o seu comportamento observável", Martin Fowler).

Conhecendo alguns tipos de Code Smells

Bloaters (Código muito grande)

Bloaters são códigos, métodos, classes inchados, que tomaram dimensões desproporcionais. Muitas vezes isso acontece ao longo do tempo a medida que surgem novas funcionalidades ou novas regras de negócio na aplicação e o código consome responsabilidades diversas e de outras classes, inclusive.

Um código muito longo e com muitas responsabilidades dificulta o entendimento e a sua manutenção.

Extrair novos métodos deste código de forma que pequenos pedaços de ações mais especializados que serão chamados pelo método principal. E faça questão de dar nomes bem apropriados para esses métodos, variáveis, classes para que o código seja bastante claro nas suas intenções.

Antes:

printOwing(): void {
  printBanner();

  // Print details.
  console.log("name: " + name);
  console.log("amount: " + getOutstanding());
}

Depois:

printOwing(): void {
  printBanner();
  printDetails(getOutstanding());
}

printDetails(outstanding: number): void {
  console.log("name: " + name);
  console.log("amount: " + outstanding);
}

Object-Orientation Abusers (Violação a orientação a objetos)

A utilização de forma errada ou incompleta de princípios de Orientação a Objetos (abstração, herança, polimorfismo, encapsulamento) pode gerar problemas no código. Isso pode ser visto quando temos muitos ifs aninhados ou switchs muito complexos o que faz com que o código não fique nada claro.

É possível criar subclasses que implementem as condições apropriadas para serem utilizadas numa condição.

Antes:

class Bird {
  // ...
  getSpeed(): number {
    switch (type) {
      case EUROPEAN:
        return getBaseSpeed();
      case AFRICAN:
        return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
      case NORWEGIAN_BLUE:
        return (isNailed) ? 0 : getBaseSpeed(voltage);
    }
    throw new Error("Should be unreachable");
  }
}

Depois:

abstract class Bird {
  // ...
  abstract getSpeed(): number;
}

class European extends Bird {
  getSpeed(): number {
    return getBaseSpeed();
  }
}
class African extends Bird {
  getSpeed(): number {
    return getBaseSpeed() - getLoadFactor() * numberOfCoconuts;
  }
}
class NorwegianBlue extends Bird {
  getSpeed(): number {
    return (isNailed) ? 0 : getBaseSpeed(voltage);
  }
}

// Somewhere in client code
let speed = bird.getSpeed();

Change Preventers (Inibidores de modificação)

Você precisa fazer alguma alteração numa parte do código porém se torna obrigatório realizar também outras várias alterações não relacionadas. Isso torna a manutenção muito mais complicada e perigosa.

Por exemplo, uma classe que tem muitas responsabilidades diferentes e precisa ser alterada frequentemente para acomodar mudanças.

Neste exemplo, temos a classe Product com informações básicas do produto e as responsabilidades mais específicas como gerenciar estoque e preço foram separadas para outras classes.

class Product {
  private id: number;
  private name: string;
  private price: number;
  private stock: number;

  constructor(id: number, name: string, price: number, stock: number) {
    this.id = id;
    this.name = name;
    this.price = price;
    this.stock = stock;
  }

  getId() {
    return this.id;
  }

  getName() {
    return this.name;
  }

  getPrice() {
    return this.price;
  }

  getStock() {
    return this.stock;
  }
}

class ProductStockManager {
  private product: Product;

  constructor(product: Product) {
    this.product = product;
  }

  updateStock(newStock: number) {
    this.product.stock = newStock;
  }

  isInStock() {
    return this.product.stock > 0;
  }
}

class ProductPriceManager {
  private product: Product;

  constructor(product: Product) {
    this.product = product;
  }

  updatePrice(newPrice: number) {
    this.product.price = newPrice;
  }

  isDiscounted() {
    // Check if product has a discount
    return false;
  }
}

class ProductPage {
  private product: Product;

  constructor(product: Product) {
    this.product = product;
  }

  displayProductInformation() {
    console.log(`Product ID: ${this.product.getId()}`);
    console.log(`Product Name: ${this.product.getName()}`);
    console.log(`Product Price: ${this.product.getPrice()}`);
    console.log(`Product Stock: ${this.product.getStock()}`);
  }

  addToCart() {
    // add product to cart
  }
}

Dispensables (Código dispensável)

É aquele trecho de código que, como o próprio nome diz, é completamente dispensável e não altera em absoluto o comportamento da aplicação.

Ele pode ser um comentário, código duplicado, código não utilizado, classes subutilizadas etc.

Neste exemplo, a classe Rectangle contém apenas dois métodos simples que retornam a largura e altura do retângulo. Essa classe não faz muito e pode ser considerada uma classe preguiçosa.

Antes:

class Rectangle {
  private width: number;
  private height: number;

  constructor(width: number, height: number) {
    this.width = width;
    this.height = height;
  }

  getWidth() {
    return this.width;
  }

  getHeight() {
    return this.height;
  }
}

class MathHelper {
  static calculateArea(rectangle: Rectangle) {
    return rectangle.getWidth() * rectangle.getHeight();
  }

  static calculatePerimeter(rectangle: Rectangle) {
    return 2 * (rectangle.getWidth() + rectangle.getHeight());
  }
}

Depois:

interface Rectangle {
  width: number;
  height: number;
}

class MathHelper {
  static calculateArea(rectangle: Rectangle) {
    return rectangle.width * rectangle.height;
  }

  static calculatePerimeter(rectangle: Rectangle) {
    return 2 * (rectangle.width + rectangle.height);
  }
}

Neste exemplo, a classe Rectangle foi removida e substituída por um objeto literal que contém as mesmas propriedades width e height. Essa solução mantém as funcionalidades necessárias para o cálculo da área e perímetro do retângulo, mas elimina a necessidade de uma classe separada.

Couplers (Acopladores)

É o alto acoplamento entre as classes da aplicação, ou seja, uma grande dependência que existe de uma classe para outra para realizar suas próprias funções. Por exemplo, um método de uma classe acessa mais os dados de outra classe do que seus próprios ou uma classe que usa métodos ou propriedades internas de outra.

Neste exemplo abaixo, a classe Customer está fortemente acoplada à classe EmailSender. Isso porque a classe Customer chama diretamente o método sendEmail() da classe EmailSender para enviar um e-mail para o cliente.

Antes:

class Customer {
  private name: string;
  private email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  getName() {
    return this.name;
  }

  getEmail() {
    return this.email;
  }

  sendEmail(message: string) {
    const emailSender = new EmailSender();
    emailSender.sendEmail(this.email, message);
  }
}

class EmailSender {
  sendEmail(email: string, message: string) {
    // send e-mail
  }
}

Depois:

Uma possível solução para esse code smell seria deixar a funcionalidade de envio de e-mails para uma classe que de fato é responsável por isso, a classe EmailSender, e deixar a classe Customer sem a dependência dela. Um use case seria responsável por receber a injeção de dependências e realizar as tarefas.

class Customer {
  private name: string;
  private email: string;

  constructor(name: string, email: string) {
    this.name = name;
    this.email = email;
  }

  getName() {
    return this.name;
  }

  getEmail() {
    return this.email;
  }
}

class SendEmailToCustomer {
  private customer: Customer;
  private emailSender: EmailSender;

  constructor(customer: Customer, emailSender: EmailSender) {
    this.customer = customer;
    this.emailSender = emailSender;
  }

  sendWelcomeEmail() {
    const message = `Welcome, ${this.customer.getName()}! We hope you enjoy our products.`;
    this.emailSender.sendEmail(this.customer.getEmail(), message);
  }

  sendOrderConfirmationEmail(orderId: string) {
    const message = `Thank you for your order, ${this.customer.getName()}! Your order (${orderId}) is being processed.`;
    this.emailSender.sendEmail(this.customer.getEmail(), message);
  }
}

class EmailSender {
  sendEmail(email: string, message: string) {
    // send e-mail
  }
}

Para conhecer outros tipos de Code Smells, recomendo a leitura do artigo Code smells que podemos evitar no nosso dia-a-dia de Thiago S. Adriano.

Também adiciono como referência, um famoso livro (se não o mais famoso) sobre refatoração de código: Refactoring: Improving the Design of Existing Code

Os Code Smells, apesar de não serem bugs, são grandes indícios de que sua aplicação pode quebrar a qualquer minuto e precisa de um cuidado. Para evitar situações como essas, sempre que estivermos revisitando algum código, podemos praticar o Refactoring e melhorar sua qualidade ;)