Code Smell
O que é um Code Smell?
Code smell é um termo usado em desenvolvimento de software para descrever sintomas no código fonte que podem indicar problemas mais profundos. Embora um code smell não seja um erro ou bug em si, ele sugere que o código pode ser de baixa qualidade ou que há potenciais problemas de manutenção e evolução no futuro. Code smells são pistas de que o design ou a implementação de uma parte do software precisa ser revisada ou refatorada para melhorar sua clareza, eficiência ou manutenção.
Mesmo sem afetar o funcionamento imediato, podem causar problemas de desempenho, leitura e futuros bugs.
Branas Rodrigo (2023)
Vamos explorar os code smells mais comuns em TypeScript, oferecendo insights práticos e exemplos ao longo do caminho. Vale lembrar que as soluções propostas aqui são apenas melhorias.
Vamos começar! 🚀
Inconsistent Naming | Repetitive Naming
Inconsistência nos nomes ou repetição desnecessária do nome da classe, variáveis ou funções, causando redundância e dificultando a manutenção do código
// má prática:
class Book {
// repetição desnecessária de 'Book' nos parâmetros e variáveis
constructor(public bookName: string, public bookAuthor: string) {
this.bookName = bookName;
this.bookAuthor = bookAuthor;
}
getBookName(): string { // repetição desnecessária de 'Book'
return this.bookName;
}
getBookAuthor(): string { // repetição...
return this.bookAuthor;
}
setBookName(bookName: string): void { // repetição...
this.bookName = bookName;
}
setBookAuthor(bookAuthor: string): void { // repetição...
this.bookAuthor = bookAuthor;
}
}
// exemplo de uso
const book = new Book('name', 'Jhon');
book.getBookName(); // já sabemos que é um book// solução
class Book {
constructor(public name: string, public author: string) {
this.name = name;
this.author = author;
}
getName(): string {
return this.name;
}
getAuthor(): string {
return this.author;
}
setName(name: string): void {
this.name = name;
}
setAuthor(author: string): void {
this.author = author;
}
}
// exemplo de uso
const book = new Book('name', 'Jhon');
book.getName();Long Method | God Function
ocorre quando um método realiza muitas tarefas ou tem muitas responsabilidades, dificultando a leitura, manutenção e testes do código.
Viola o Princípio de Responsabilidade Única(SRP) no SOLID.
// Má prática:
class OrderProcessor {
processOrder(order: any): void {
// Validação de dados do pedido
if (!order.customer || !order.items || order.items.length === 0) {
throw new Error('Invalid order data');
}
// Validação do cliente
if (!order.customer.email || !order.customer.name) {
throw new Error('Customer data is invalid');
}
// Validação de estoque
for (const item of order.items) {
const stock = 100; // Mock de estoque
if (stock < item.quantity) {
throw new Error(`Insufficient stock for item ${item.name}`);
}
}
// Cálculo do valor total do pedido
let total = 0;
for (const item of order.items) {
total += item.price * item.quantity;
}
// Aplicando desconto
if (order.discount) {
total *= 1 - order.discount / 100;
}
// Processamento de pagamento
const paymentSuccess = true; // Mock de processamento de pagamento
if (!paymentSuccess) {
throw new Error('Payment failed');
}
// Atualização de estoque
for (const item of order.items) {
// ...
}
// Criação de fatura
...
// Envio de e-mail de confirmação
...
// Log do pedido
...
// msg enviada
...
}
}// solução
class OrderProcessor {
processOrder(order: Order): void {
this.validateOrder(order);
const total = this.calculateTotal(order);
this.processPayment(order.paymentMethod, total);
this.updateInventory(order.items);
this.createInvoice(order, total);
this.sendEmail(order.customer.email, total);
this.logOrder(order);
}
private validateOrder(order: Order): void {
this.validateCustomer(order.customer);
this.validateItems(order.items);
}
private validateCustomer(customer: Customer): void { ... }
private validateItems(items: OrderItem[]): void { ... }
private checkStock(item: OrderItem): number { ... }
private calculateTotal(order: Order): number { ... }
private processPayment(paymentMethod: string, total: number): void { ... }
private paymentSuccess(paymentMethod: string,total: number): boolean { ... }
private updateInventory(items: OrderItem[]): void { ... }
private updateStock(item: OrderItem): void { ... }
private createInvoice(order: Order, total: number): Invoice { ... }
private sendEmail(email: string, total: number): void { ... }
private logOrder(order: Order): void { ... }
}Large Class
Ocorre quando uma classe se torna muito grande e possui muitas responsabilidades. Isso torna o código difícil de entender, manter e testar. Classes grandes geralmente contêm métodos e propriedades que não estão diretamente relacionadas entre si, tornando a classe complexa e difícil de trabalhar.
Viola o princípio de Responsabilidade Única (Single Responsibility Principle) no SOLID
A classe anterior OrderProcessor resolveu um problema do Long Method e agora vamos melhora-la ainda mais criando várias classes cada uma com sua responsabilidade:
// solução
class OrderValidator { ... }
class PaymentProcessor { ... }
class InventoryManager { ... }
class InvoiceCreator { ... }
class EmailSender { ... }
class Logger { ... }
class OrderProcessor {
private validator: OrderValidator;
private paymentProcessor: PaymentProcessor;
private inventoryManager: InventoryManager;
private invoiceCreator: InvoiceCreator;
private emailSender: EmailSender;
private logger: Logger;
constructor() {
this.validator = new OrderValidator();
this.paymentProcessor = new PaymentProcessor();
this.inventoryManager = new InventoryManager();
this.invoiceCreator = new InvoiceCreator();
this.emailSender = new EmailSender();
this.logger = new Logger();
}
processOrder(order: any): void {
this.validator.validateOrder(order);
const total = this.invoiceCreator.calculateTotal(order);
this.paymentProcessor.processPayment(order.paymentMethod, total);
this.inventoryManager.updateInventory(order.items);
const invoice = this.invoiceCreator.createInvoice(order, total);
this.emailSender.sendEmail(order.customer.email, total);
this.logger.logOrder(order);
console.log('Order processed successfully');
}
}Redundant Code | Duplicate Code | Redundant Variables
Ocorre quando o mesmo código é repetido em diferentes locais ou quando a variável é repetida dentro e fora do construtor sem nenhum tratamento ou regra de negócio . Isso aumenta a complexidade do sistema e torna a manutenção mais difícil, já que qualquer mudança em um desses trechos de código precisará ser refletida em todos os outros lugares onde a duplicação ocorre. Isso pode resultar em erros e maior esforço de manutenção.ocorre quando o mesmo código é repetido em diferentes locais. Isso aumenta a complexidade do sistema e torna a manutenção mais difícil, já que qualquer mudança em um desses trechos de código precisará ser refletida em todos os outros lugares onde a duplicação ocorre. Isso pode resultar em erros e maior esforço de manutenção.
// problema 01
class Project {
name: string;
description: string;
startDate: Date;
endDate: Date;
budget: number;
clientName: string;
clientEmail: string;
manager: string;
status: string;
teamMembers: string[];
constructor(
name: string,
description: string,
startDate: Date,
endDate: Date,
budget: number,
clientName: string,
clientEmail: string,
manager: string,
status: string,
teamMembers: string[]
) {
this.name = name; // variavel Redundante
this.description = description; // variavel Redundante
this.startDate = startDate; // variavel Redundante
this.endDate = endDate; // variavel Redundante
this.budget = budget; // variavel Redundante
this.clientName = clientName; // variavel Redundante
this.clientEmail = clientEmail; // variavel Redundante
this.manager= manager; // variavel Redundante
this.status = status; // variavel Redundante
this.teamMembers = teamMembers; // variavel Redundante
}
...
}// solucao 01
class Project {
constructor(
public name: string,
public description: string,
public startDate: Date,
public endDate: Date,
public budget: number,
public clientName: string,
public clientEmail: string,
public projectManager: string,
public status: string,
public teamMembers: string[]
) {}
...
}// problema 02
class Person {
constructor(public name: string, public birthDate: Date) {}
calculateAge() { // código repetido
let age = new Date().today.getFullYear() - birthDate.getFullYear();
...
return age;
}
}
class Animal {
constructor(public species: string, public birthDate: Date) {}
calculateAge() { // código repetido
let age = new Date().today.getFullYear() - birthDate.getFullYear();
...
return age;
}
}// solucao 02
class LifeSpan {
protected calculateAge(birthDate: Date): number {
let age = new Date().today.getFullYear() - birthDate.getFullYear();
...
return age;
}
}
class Person extends LifeSpan {
constructor(public name: string, public birthDate: Date) {}
get age() {
return this.calculateAge(this.birthDate);
}
}
class Animal extends LifeSpan {
constructor(public name: string, public species: string, public birthDate: Date) {}
get age() {
return this.calculateAge(this.birthDate);
}
}Long Parameter List
Ocorre quando uma função ou método tem muitos parâmetros. Isso torna o código difícil de entender, pois é necessário lembrar de todos os parâmetros ao usar ou modificar a função. Além disso, quando há muitas variáveis sendo passadas, pode ser um sinal de que a função está fazendo muitas coisas ou de que o design da aplicação não está bem estruturado.
class OrderProcessor {
processOrder(
orderId: string,
customerName: string,
customerEmail: string,
itemName: string,
itemQuantity: number,
itemPrice: number,
discount: number,
paymentMethod: string,
shippingAddress: string,
billingAddress: string,
stock: number,
... // mais parametros
): void {
...
}
}// solução
class Item {
constructor(
public name: string,
public quantity: number,
public price: number
) {}
}
class Address {
constructor(
public street: string,
public city: string,
public zipCode: string
) {}
}
class Customer {
constructor(
public name: string,
public email: string
) {}
}
class PaymentDetails {
constructor(
public method: string,
public discount: number
) {}
}
class Order {
constructor(
public orderId: string,
public customer: Customer,
public items: Item[],
public paymentDetails: PaymentDetails,
public shippingAddress: Address,
public billingAddress: Address,
public stock: number
) {}
}
class OrderProcessor {
processOrder(order: Order): void {
...
}
}Confusing Method Signatures
Similar ao Long Parameter List, ocorre quando os métodos de uma classe têm assinaturas que são difíceis de entender, seja por serem muito longas, terem parâmetros mal nomeados, ou uma ordem de parâmetros confusa. Isso torna o código difícil de usar e compreender, aumentando a chance de erros e dificultando a manutenção.
class ProductService {
// 1. muitos parâmetros sem clareza
addProduct(id: number, name: string, price: number, stock: number, category: string, discount: number, color: string, weight: number, size: string, brand: string): void { ... }
// 2. ordem dos parâmetros confusa
calculateDiscount(price: number, discount: number, quantity: number, category: string, customerType: string, promoCode: string): number {
// lógica para calcular o desconto
return price * (1 - discount) * quantity;
}
// 3. nomes de parâmetros genéricos
processOrder(a: number, b: string, c: boolean, d: any, e: string): void { ... }
// 4. parâmetros com diferentes tipos, sem contexto
updateProduct(id: string, price: number, availability: boolean, date: Date, description: string): void { ... }
// 5. método com muitos parâmetros, alguns não claros
handleTransaction(transactionId: number, userId: number, amount: number, transactionType: string, paymentMethod: string, status: string, date: Date): void { ... }
// 6. parâmetros do tipo "boolean" com nomes não informativos
createAccount(userId: string, active: boolean, verified: boolean, subscription: boolean): void { ... }
// 7. parâmetros com nomes confusos
setOrderStatus(orderTId: number, flg: boolean, ste: string, ttamp: Date): void { ... }
// 8. muitos parâmetros para um simples cálculo
calculateTotalCost(price: number, taxRate: number, shippingCost: number, promoCode: string, discountRate: number, isGift: boolean, membershipStatus: string, quantity: number): number {
return price * quantity + shippingCost + (price * taxRate) - discountRate;
}
// 9. parâmetros não relacionados juntos
generateReport(startDate: Date, endDate: Date, format: string, includeCharts: boolean, userId: string, reportType: string): void { ... }
// 10. uso de um parâmetro sem contexto claro
updateUserSettings(userId: string, settings: any, type: string, active: boolean, saveImmediately: boolean): void { ... }
}// solução
class ProductDetails { ... }
class Details { ... }
class Transaction { ... }
class AccountStatus { ... }
class Order { ... }
class ReportDetails { ... }
class Settings { ... }
class ProductService {
// 1. simplificando com objeto
addProduct(product: ProductDetails): void { ... }
// 2. melhorando a clareza dos parâmetros
calculateDiscount(price: number, discount: number, quantity: number): number {
// lógica para calcular o desconto
return price * (1 - discount) * quantity;
}
// 3. parâmetros mais descritivos
processOrder(orderId: number, customerName: string, isUrgent: boolean, orderDetails: any): void { ... }
// 4. melhorando a clareza dos parâmetros
updateProduct(productId: string, updatedDetails: Details): void { ... }
// 5. usando objetos para transações
handleTransaction(transaction: Transaction): void { ... }
// 6. nomeando parâmetros com mais clareza
createAccount(userId: string, accountStatus: AccountStatus): void { ... }
// 7. parâmetros com nomes mais claros
setOrderStatus(orderId: number, status: string, timestamp: Date): void { ... }
// 8. agrupando parâmetros de custo em objeto
calculateTotalCost(order: Order): number {
// lógica para calcular o custo total
return order.price * order.quantity + order.shippingCost + (order.price * order.taxRate) - order.discountRate;
}
// 9. agrupando parâmetros relacionados ao relatório
generateReport(reportDetails: ReportDetails): void { ... }
// 10. usando objeto para configurações do usuário
updateUserSettings(userId: string, settings: Settings): void { ... }
}Boolean Trap
Ocorre quando se usa uma variável booleana de forma inadequada, levando a um código confuso, redundante ou propenso a erros. Esse problema ocorre, principalmente, quando a variável booleana é usada em expressões ou condições complexas de maneira que causa ambiguidades ou torna o código difícil de entender.
A principal questão aqui é que, ao usar variáveis booleanas de maneira inadequada, pode-se criar armadilhas lógicas, onde o comportamento do código não é claro ou é inesperado.
class User {
constructor(private isActive: boolean, private isSuspended: boolean) {}
checkUserStatus(): boolean {
// armadilha booleana: a negação dupla não faz sentido
return !(this.isActive && !this.isSuspended);
}
}
// exemplo de uso
const user = new User(true, false);
console.log(user.checkUserStatus());// solução
class User {
constructor(private isActive: boolean, private isSuspended: boolean) {}
public isUserActive(): boolean {
// Solução: A lógica é clara e direta
return this.isActive && !this.isSuspended;
}
}
// exemplo de uso
const user = new User(true, false);
console.log(user.isUserActive());Boolean Variable Naming | Boolean Method Naming
Ocorre quando uma variável booleana ou um método booleano não segue a convenção de nomenclatura esperada, o que pode causar confusão no código. Para tornar o código mais legível, é recomendado que variáveis e métodos booleanos comecem com os prefixos is, has, can, ou outros prefixos semelhantes, para indicar que o valor ou a operação é do tipo booleano e para expressar claramente o que está sendo verificado ou afirmado.
class User {
// nome não apropriado para uma variável booleana
constructor(private flag: boolean) {}
checkStatusActive(): boolean {
return this.flag;
}
}
const user = new User(true);
console.log(user.checkStatusActive()); // Não é claro que "flag" é um boleano// solução
class User {
// nome de variável booleana com prefixo 'is'
constructor(private isActive: boolean) {}
isActive(): boolean {
return this.isActive;
}
}
const user = new User(true);
console.log(user.isActive()); // Agora é claro que "isActive" booleanoSide Effects | Uncontrolled Side Effects
Ocorre quando uma função, método ou classe altera variáveis externas, parâmetros passados por referência ou o estado global de maneira imprevisível e descontrolada. Isso pode levar a bugs difíceis de rastrear e viola o princípio de funções puras, onde o comportamento deve ser previsível e sem dependências externas.
let items: string[] = ['apple', 'banana', 'cherry']; // global default
function addItemToCart(item: string): void {
items.push(item); // modificando o estado global
console.log(`Item adicionado: ${item}`);
}
addItemToCart('orange');
console.log(items); // ['apple', 'banana', 'cherry', 'orange']// solução
let items: string[] = ['apple', 'banana', 'cherry']; // global default
function addItemToCart(item: string, items: string[]): string[] {
return [...items, item]; // Criando um novo array com o item adicionado
}
const newItems = addItemToCart('orange', items);
console.log(newItems); // ['apple', 'banana', 'cherry', 'orange']
console.log(items); // ['apple', 'banana', 'cherry'] (estado original)Default Parameters Misuse | Mutating Parameters
Similar ao Side Effects mas focado nos parâmetros. Ocorre quando valores padrão são fornecidos para parâmetros de funções, mas esses valores podem criar confusão ou comportamentos inesperados. O uso incorreto de parâmetros padrão pode resultar em valores inesperados, dificultando a leitura e manutenção do código. Isso é particularmente problemático quando os valores padrão são complexos, como objetos ou arrays, ou quando esses valores podem ser alterados acidentalmente durante a execução da função.
class User {
constructor(public name: string, age: number){ }
}
updateUser(user: User): void {
user.age += 1; // Modificando o parâmetro diretamente
console.log(`O usuário ${user.name} tem agora ${user.age} anos.`);
}
const user = new User("Alice", 25);
updateUser(user); // O usuário Alice tem agora 26 anos.// solução
updateUser(user: { name: string, age: number }): { name: string, age: number } {
const updatedUser = { ...user, age: user.age + 1 }; // Criando uma cópia e modificando a cópia
console.log(`O usuário ${updatedUser.name} tem agora ${updatedUser.age} anos.`);
return updatedUser;
}
const user = { name: "Alice", age: 25 };
const updatedUser = updateUser(user); // O usuário Alice tem agora 26 anos.
console.log(user); // O objeto original não foi alterado: { name: "Alice", age: 25 }
console.log(updatedUser); // { name: "Alice", age: 26 }Primitive Obsession
Ocorre quando tipos primitivos (como string, number, boolean) são usados excessivamente para representar conceitos mais complexos. Em vez de utilizar tipos primitivos, é melhor usar classes ou tipos específicos que encapsulam a lógica relacionada e fornecem maior clareza, flexibilidade e segurança para o código.
class User {
constructor(
public name: string,
public email: string, // Email é apenas uma string
public phoneNumber: string, // PhoneNumber é apenas uma string
public age: number
) {}
// validar email (simples e ineficiente)
validateEmail(): boolean {
return this.email.includes("@");
}
// validar número de telefone (simples e ineficiente)
validatePhoneNumber(): boolean {
return this.phoneNumber.length === 10;
}
}// solução
class Email {
constructor(private email: string) {
if (!this.validateEmail()) {
throw new Error("Invalid email address.");
}
}
private validateEmail(): boolean {
return this.email.includes("@");
}
public getValue(): string {
return this.email;
}
}
class PhoneNumber {
constructor(private phoneNumber: string) {
if (!this.validatePhoneNumber()) {
throw new Error("Invalid phone number.");
}
}
private validatePhoneNumber(): boolean {
return this.phoneNumber.length === 10;
}
public getValue(): string {
return this.phoneNumber;
}
}
class User {
constructor(
public name: string,
private email: Email, // email é um objeto
private phoneNumber: PhoneNumber, // phoneNumber é um objeto
public age: number
) {}
public getEmail(): string {
return this.email.getValue();
}
public getPhoneNumber(): string {
return this.phoneNumber.getValue();
}
}
// exemplo de uso
const email = new Email("john.doe@example.com");
const phoneNumber = new PhoneNumber("1234567890");
const user = new User("John Doe", email, phoneNumber, 30);Feature Envy
ocorre quando um método de uma classe está mais interessado nos dados de outra classe do que nos dados de sua própria classe. Esse comportamento sugere que a responsabilidade do método não está bem definida, e ele deveria estar na outra classe, que tem os dados necessários. Isso pode aumentar o acoplamento entre as classes e diminuir a coesão, o que torna o código mais difícil de entender e manter.
class Employee {
constructor(
public name: string,
public salary: number,
public hoursWorked: number
) {}
getSalary(): number { // retornar o salário base
return this.salary;
}
getHoursWorked(): number { // retorna horas trabalhadas
return this.hoursWorked;
}
}
class Payroll {
constructor(private employee: Employee) {}
/** cálculo do pagamento feito na classe Payroll,
acessando dados da classe Employee */
calculatePay(): number {
return this.employee.getSalary() * this.employee.getHoursWorked();
}
}
// exemplo de uso
const employee = new Employee('John Doe', 50, 160);
const payroll = new Payroll(employee);
console.log(`Employee's Pay: $${payroll.calculatePay()}`);// solução
class Employee {
constructor(
public name: string,
public salary: number,
public hoursWorked: number
) {}
getSalary(): number { // retorna o salário base
return this.salary;
}
getHoursWorked(): number { // retorna as horas trabalhadas
return this.hoursWorked;
}
calculatePay(): number { // calcula o pagamento, agora na classe Employee
return this.salary * this.hoursWorked;
}
}
class Payroll {
constructor(private employee: Employee) {}
processPayroll(): number {
return this.employee.calculatePay();
}
}
// exemplo de uso
const employee = new Employee('John Doe', 50, 160);
const payroll = new Payroll(employee);
console.log(`Employee's Pay: $${payroll.processPayroll()}`);Data Clumps
Ocorre quando um grupo de dados relacionados é frequentemente passado juntos entre diferentes métodos ou classes. Isso pode indicar que esses dados pertencem a um único objeto ou entidade, mas estão sendo tratados de maneira separada e não encapsulada.
class Order {
constructor(
public productName: string, // parametros repetidos
public quantity: number,
public price: number
) {}
calculateTotal(): number {
return this.quantity * this.price;
}
printInvoice(): void {
...
}
}
class OrderService {
// parametros repetidos
processOrder(productName: string, quantity: number, price: number): void {
// parametros repetidos
const order = new Order(productName, quantity, price);
order.printInvoice();
}
}
// exemplo de uso
const orderService = new OrderService();
orderService.processOrder("Laptop", 1, 1500);// solução
class Product { // classe Product encapsula nome, quantidade e preço
constructor(
public name: string,
public quantity: number,
public price: number
) {}
calculateTotal(): number {
return this.quantity * this.price;
}
}
class Order {
constructor(public product: Product) {}
printInvoice(): void {
...
}
}
class OrderService {
processOrder(product: Product): void {
const order = new Order(product);
order.printInvoice();
}
}
// exemplo de uso
const product = new Product("Laptop", 1, 1500);
const orderService = new OrderService();
orderService.processOrder(product);Lazy Class
Ocorre quando uma classe tem tão pouca responsabilidade ou funcionalidade que não justifica sua existência. Ou seja, ela não faz nada de útil ou sua funcionalidade poderia ser facilmente incorporada em outra classe. Esse code smell pode indicar que uma classe foi criada de forma excessiva, fragmentando o código em unidades muito pequenas e desnecessárias.
class Address {
constructor(
private street: string,
private zip: string,
...
) {}
// getters and setters
...
}// solução
class Address {
constructor(
private street = '',
private zip: string,
...
) {
this.setZip(this.zip); // adicionado validação do zip code
}
private validateZip(): boolean {
const zipPattern = /^\d{5}-\d{4}$/;
return zipPattern.test(this.zip);
}
getFullAddress(): string { // adiconado retorno do endereco completo
return `${this.street}, ${this.city}, ${this.state}, ${this.zip}`;
}
setZip(value: string): void {
this.zip = this.validateZip() ? this.zip : '';
}
// getters and setters
...
}Middle Man
Ocorre quando uma classe ou método atua como um intermediário desnecessário, simplesmente delegando responsabilidades para outra classe ou método sem adicionar valor significativo. Isso cria uma camada extra de complexidade e dificulta a manutenção do código.
class Order {
constructor(
public id: number,
public customerName: string,
public total: number
) {}
getOrderDetails(): string {
return `Order #${this.id} for ${this.customerName}, Total: $${this.total}`;
}
}
class OrderManager {
private order: Order;
constructor(order: Order) {
this.order = order;
}
// simplesmente delega chamadas para a classe Order
getOrderDetails(): string {
return this.order.getOrderDetails();
}
getTotal(): number {
return this.order.total;
}
}// solução
class Order {
constructor(
public id: number,
public customerName: string,
public total: number
) {}
getOrderDetails(): string {
return `Order #${this.id} for ${this.customerName}, Total: $${this.total}`;
}
getTotal(): number {
return this.total;
}
}
// exemplo de uso
const order = new Order(1, 'John Doe', 150.0);
console.log(order.getOrderDetails());
console.log(`Total: $${order.getTotal()}`);Divergent Change
Ocorre quando uma única classe ou módulo precisa ser alterado por vários motivos diferentes. Isso significa que a classe ou módulo está lidando com responsabilidades múltiplas, violando o Princípio da Responsabilidade Única (SRP) no SOLID. Esse tipo de code smell pode dificultar a manutenção e aumentar o risco de introdução de bugs.
class CustomerService {
getCustomerDetails(customerId: string): string {
...
return `Detalhes do cliente para ${customerId}`;
}
updateCustomerDetails(customerId: string, details: any): void {
...
}
// CustomerService não deve entender de processar ordem
processOrder(orderId: string): void {
...
}
// CustomerService não deve entender envio de msg
sendNotification(customerId: string, message: string): void {
...
}
}
// exemplo de uso
const service = new CustomerService();
service.getCustomerDetails("123");
service.updateCustomerDetails("123", { name: "João Silva" });
service.processOrder("456");
service.sendNotification("123", "Seu pedido foi enviado!");// solução
class CustomerService {
getCustomerDetails(customerId: string): string {
...
}
updateCustomerDetails(customerId: string, details: any): void {
...
}
}
class OrderService {
processOrder(orderId: string): void {
...
}
}
class NotificationService {
sendNotification(customerId: string, message: string): void {
...
}
}
// exemplo de uso
const customerService = new CustomerService();
customerService.getCustomerDetails("123");
customerService.updateCustomerDetails("123", { name: "João Silva" });
const orderService = new OrderService();
orderService.processOrder("456");
const notificationService = new NotificationService();
notificationService.sendNotification("123", "Seu pedido foi enviado!");Shotgun Surgery
Ocorre quando uma única mudança no sistema exige que você modifique várias classes ou módulos ao mesmo tempo. Isso indica que a lógica está espalhada, violando o princípio de alta coesão e dificultando a manutenção do código.
class Order {
calculateOrderValue(baseValue: number): number {
const tax = baseValue * 0.1; // Imposto fixo de 10% na classe Order
return baseValue + tax;
}
}
class Invoice {
generateInvoice(baseValue: number): string {
const tax = baseValue * 0.1; // Imposto fixo de 10% na classe Invoice
const total = baseValue + tax;
return `Valor da Fatura: ${total}`;
}
}
class Report {
generateReport(baseValue: number): string {
const tax = baseValue * 0.1; // Imposto fixo de 10% na Report
const total = baseValue + tax;
return `Relatório: Total com imposto: ${total}`;
}
}
// exemplo de uso
const order = new Order();
console.log(order.calculateOrderValue(100));
const invoice = new Invoice();
console.log(invoice.generateInvoice(100));
const report = new Report();
console.log(report.generateReport(100));// solução
class TaxCalculator {
private static readonly TAX_RATE = 0.1; // taxa de imposto fixa de 10%
static calculateTax(baseValue: number): number {
return baseValue * this.TAX_RATE;
}
}
class Order {
calculateOrderValue(baseValue: number): number {
const tax = TaxCalculator.calculateTax(baseValue);
return baseValue + tax;
}
}
class Invoice {
generateInvoice(baseValue: number): string {
const tax = TaxCalculator.calculateTax(baseValue);
const total = baseValue + tax;
return `Valor da Fatura: ${total}`;
}
}
class Report {
generateReport(baseValue: number): string {
const tax = TaxCalculator.calculateTax(baseValue);
const total = baseValue + tax;
return `Relatório: Total com imposto: ${total}`;
}
}
// exemplo de uso
const order = new Order();
console.log(order.calculateOrderValue(100));
const invoice = new Invoice();
console.log(invoice.generateInvoice(100));
const report = new Report();
console.log(report.generateReport(100));Parallel Inheritance Hierarchies
Ocorre quando duas ou mais hierarquias de classes crescem juntas, ou seja, para cada classe criada em uma hierarquia, é necessário criar uma classe correspondente em outra. Isso indica um design acoplado e rígido, o que dificulta a manutenção e a evolução do código.
// Hierarquia 1: Produtos
abstract class Product {
constructor(public name: string, public price: number) {}
}
class Electronics extends Product {}
class Clothing extends Product {}
class Groceries extends Product {}
// Hierarquia 2: Descontos
abstract class Discount {
abstract calculate(product: Product): number;
}
class ElectronicsDiscount extends Discount {
calculate(product: Electronics): number {
return product.price * 0.9; // 10%
}
}
class ClothingDiscount extends Discount {
calculate(product: Clothing): number {
return product.price * 0.8; // 20%
}
}
class GroceriesDiscount extends Discount {
calculate(product: Groceries): number {
return product.price; // 0%
}
}
// exemplo de uso
const electronics = new Electronics("Smartphone", 1000);
const electronicsDiscount = new ElectronicsDiscount();
console.log(electronicsDiscount.calculate(electronics)); // 900
const clothing = new Clothing("Camisa", 50);
const clothingDiscount = new ClothingDiscount();
console.log(clothingDiscount.calculate(clothing)); // 40// solução
// Classe base para Produtos
abstract class Product {
constructor(public name: string, public price: number) {}
abstract calculateDiscount(): number;
}
// Implementações específicas de Produtos
class Electronics extends Product {
calculateDiscount(): number {
return this.price * 0.9; // 10%
}
}
class Clothing extends Product {
calculateDiscount(): number {
return this.price * 0.8; // 20%
}
}
class Groceries extends Product {
calculateDiscount(): number {
return this.price; // 0%
}
}
// exemplo de uso
const electronics = new Electronics("Smartphone", 1000);
console.log(`${electronics.name} | ${electronics.calculateDiscount()}`);
const clothing = new Clothing("Camisa", 50);
console.log(`${clothing.name} | ${clothing.calculateDiscount()}`);
const groceries = new Groceries("Maçã", 5);
console.log(`${groceries.name} | ${groceries.calculateDiscount()}`);Temporary Field
Ocorre quando uma classe possui variáveis de instância que são configuradas apenas em situações específicas ou temporárias, mas que nem sempre são necessárias. Esses campos podem confundir quem lê o código, aumentar o uso de memória e tornar o código mais difícil de manter.
class Order {
constructor(
public id: number,
public customerName: string,
public total: number,
public deliveryInstructions?: string // usado apenas para delivery
) {}
processOrder(): void {
console.log(`Processando pedido #${this.id} para ${this.customerName}`);
if (this.deliveryInstructions) {
console.log(`Instruções de entrega: ${this.deliveryInstructions}`);
}
}
}
// exemplo de uso
const deliveryOrder = new Order(1, "João", 100, "Deixar na portaria");
deliveryOrder.processOrder();
const pickupOrder = new Order(2, "Maria", 50);
pickupOrder.processOrder();// solução
abstract class Order {
constructor(
public id: number,
public customerName: string,
public total: number
) {}
abstract processOrder(): void;
}
class DeliveryOrder extends Order {
constructor(
id: number,
customerName: string,
total: number,
public deliveryInstructions: string
) {
super(id, customerName, total);
}
processOrder(): void {
...
}
}
class PickupOrder extends Order {
processOrder(): void {
...
}
}
// exemplo de uso
const deliveryOrder = new DeliveryOrder(1,'João', 5, 'Deixar na portaria');
deliveryOrder.processOrder();
const pickupOrder = new PickupOrder(2, 'Maria', 50);
pickupOrder.processOrder();Refused Bequest
Ocorre quando uma classe herda de uma classe base, mas rejeita (ou não utiliza) partes da funcionalidade herdada. Isso é um sinal de que a herança pode não ser apropriada, indicando que a relação “é-um” entre a classe base e a classe derivada não é válida.
Violação do Princípio de Substituição de Liskov (LSP) no SOLID.
// classe base
class Animal {
eat(): void {
console.log("O animal está comendo.");
}
sleep(): void {
console.log("O animal está dormindo.");
}
fly(): void {
console.log("O animal está voando.");
}
}
// subclasse
class Dog extends Animal {
fly(): void {
throw new Error("Cachorros não podem voar."); // método rejeitado
}
}
// exemplo de uso
const dog = new Dog();
dog.eat(); // funciona corretamente
dog.fly(); // lança erro: "Cachorros não podem voar."// solução
// interfaces para habilidades
interface Eater {
eat(): void;
}
interface Sleeper {
sleep(): void;
}
interface Flyer {
fly(): void;
}
// implementações específicas
class BasicEater implements Eater {
eat(): void {
console.log("Está comendo.");
}
}
class BasicSleeper implements Sleeper {
sleep(): void {
console.log("Está dormindo.");
}
}
class BasicFlyer implements Flyer {
fly(): void {
console.log("Está voando.");
}
}
// classes específicas
class Dog implements Eater, Sleeper {
constructor(
private eater: Eater = new BasicEater(),
private sleeper: Sleeper = new BasicSleeper()
) {}
eat(): void {
this.eater.eat();
}
sleep(): void {
this.sleeper.sleep();
}
}
class Bird implements Eater, Sleeper, Flyer {
constructor(
private eater: Eater = new BasicEater(),
private sleeper: Sleeper = new BasicSleeper(),
private flyer: Flyer = new BasicFlyer()
) {}
eat(): void {
this.eater.eat();
}
sleep(): void {
this.sleeper.sleep();
}
fly(): void {
this.flyer.fly();
}
}
// exemplo de uso
const dog = new Dog();
dog.eat(); // "está comendo."
dog.sleep(); // "está dormindo."
const bird = new Bird();
bird.eat(); // "está comendo."
bird.sleep(); // "está dormindo."
bird.fly(); // "está voando."Inappropriate Intimacy
Ocorre quando duas classes têm uma relação muito estreita, acessando diretamente os detalhes internos uma da outra de maneira que quebra o encapsulamento. Esse tipo de acoplamento pode tornar o código difícil de manter e evoluir, além de criar dependências desnecessárias.
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
getBalance(): number {
return this.balance;
}
setBalance(newBalance: number): void {
this.balance = newBalance;
}
}
class Transaction {
processDeposit(account: BankAccount, amount: number): void {
// manipulação de informações onde apenas o BankAccount deveria saber
const currentBalance = account.getBalance();
account.setBalance(currentBalance + amount);
console.log(`Depósito de ${amount} realizado. Saldo: ${account.getBalance()}`);
}
processWithdrawal(account: BankAccount, amount: number): void {
// manipulação de informações onde apenas o BankAccount deveria saber
const currentBalance = account.getBalance();
if (currentBalance >= amount) {
account.setBalance(currentBalance - amount);
console.log(`Saque de ${amount} realizado. Saldo: ${account.getBalance()}`);
} else {
console.log("Saldo insuficiente.");
}
}
}
// exemplo de uso
const account = new BankAccount(1000);
const transaction = new Transaction();
transaction.processDeposit(account, 500); // Saldo atual: 1500
transaction.processWithdrawal(account, 200); // Saldo atual: 1300
transaction.processWithdrawal(account, 2000); // Saldo insuficiente.// solução
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
deposit(amount: number): void {
if (amount <= 0) {
console.log("O valor do depósito deve ser positivo.");
return;
}
this.balance += amount;
console.log(`Depósito de ${amount} realizado. Saldo: ${this.balance}`);
}
withdraw(amount: number): void {
if (amount <= 0) {
console.log("O valor do saque deve ser positivo.");
return;
}
if (this.balance >= amount) {
this.balance -= amount;
console.log(`Saque de ${amount} realizado. Saldo: ${this.balance}`);
} else {
console.log("Saldo insuficiente.");
}
}
getBalance(): number {
return this.balance;
}
}
class Transaction {
processDeposit(account: BankAccount, amount: number): void {
account.deposit(amount);
}
processWithdrawal(account: BankAccount, amount: number): void {
account.withdraw(amount);
}
}
// exemplo de uso
const account = new BankAccount(1000);
const transaction = new Transaction();
transaction.processDeposit(account, 500); // Saldo atual: 1500
transaction.processWithdrawal(account, 200); // Saldo atual: 1300
transaction.processWithdrawal(account, 2000); // Saldo insuficiente.Message Chains
Ocorre quando uma classe depende de várias chamadas de métodos encadeadas para acessar funcionalidades de outra classe. Esse tipo de acoplamento pode tornar o código frágil e difícil de manter, pois mudanças em uma classe intermediária podem quebrar toda a cadeia.
class Address {
constructor(public street: string, public city: string) {}
}
class Customer {
constructor(public name: string, public address: Address) {}
}
class Order {
constructor(public id: number, public customer: Customer) {}
printDeliveryAddress(): void {
console.log(
`${this.customer.address.street}, ${this.customer.address.city}`
);
}
}
// exemplo de uso
const address = new Address("Rua A, 123", "São Paulo");
const customer = new Customer("João", address);
const order = new Order(1, customer);
order.printDeliveryAddress(); // Saída: Rua A, 123, São Paulo// solução
class Address {
constructor(private street: string, private city: string) {}
getFullAddress(): string {
return `${this.street}, ${this.city}`;
}
}
class Customer {
constructor(private name: string, private address: Address) {}
getDeliveryAddress(): string {
return this.address.getFullAddress();
}
}
class Order {
constructor(private id: number, private customer: Customer) {}
printDeliveryAddress(): void {
console.log(`Endereço: ${this.customer.getDeliveryAddress()}`);
}
}
// exemplo de uso
const address = new Address("Rua A, 123", "São Paulo");
const customer = new Customer("João", address);
const order = new Order(1, customer);
order.printDeliveryAddress(); // saída: Endereço: Rua A, 123, São PauloSwitch Statements
Ocorre quando usamos declarações switch ou muitos if-else para realizar a lógica baseada em valores ou tipos. Esse padrão pode levar a dificuldades na manutenção, já que adicionar novos casos frequentemente requer modificar o código existente.
Violação do Open-Closed Principle (OCP) no SOLID.
class DiscountCalculator {
calculateDiscount(customerType: string, purchaseAmount: number): number {
switch (customerType) {
case "Regular":
return purchaseAmount * 0.05; // 5% de desconto
case "Premium":
return purchaseAmount * 0.1; // 10% de desconto
case "VIP":
return purchaseAmount * 0.2; // 20% de desconto
default:
return 0; // sem desconto
}
}
}
// exemplo de uso
const calculator = new DiscountCalculator();
calculator.calculateDiscount("Regular", 1000); // 50
calculator.calculateDiscount("VIP", 2000); // 400
calculator.calculateDiscount("Unknown", 500); // 0Problemas Identificados
- Fácil de Quebrar: Adicionar ou alterar tipos de clientes requer a modificação do método
calculateDiscount, potencialmente introduzindo erros. - Baixa Extensibilidade: O código não é extensível sem editar diretamente o método existente.
- Violação do Open-Closed Principle: O método não está fechado para modificações, já que novos casos exigem mudanças na lógica interna.
// solução
interface DiscountStrategy { // interface para tipos de desconto
calculate(purchaseAmount: number): number;
}
// estratégias de desconto específicas
class RegularCustomerDiscount implements DiscountStrategy {
calculate(purchaseAmount: number): number {
return purchaseAmount * 0.05; // 5% de desconto
}
}
class PremiumCustomerDiscount implements DiscountStrategy {
calculate(purchaseAmount: number): number {
return purchaseAmount * 0.1; // 10% de desconto
}
}
class VIPCustomerDiscount implements DiscountStrategy {
calculate(purchaseAmount: number): number {
return purchaseAmount * 0.2; // 20% de desconto
}
}
class NoDiscount implements DiscountStrategy {
calculate(purchaseAmount: number): number {
return 0; // Sem desconto
}
}
// classe principal
class DiscountCalculator {
private strategies: { [key: string]: DiscountStrategy } = {
Regular: new RegularCustomerDiscount(),
Premium: new PremiumCustomerDiscount(),
VIP: new VIPCustomerDiscount(),
};
calculateDiscount(customerType: string, purchaseAmount: number): number {
const strategy = this.strategies[customerType] || new NoDiscount();
return strategy.calculate(purchaseAmount);
}
}
// exemplo de uso
const calculator = new DiscountCalculator();
calculator.calculateDiscount("Regular", 1000); // 50
calculator.calculateDiscount("VIP", 2000); // 400
calculator.calculateDiscount("Unknown", 500); // 0A solução e os Benefícios da Refatoração
- Alta Extensibilidade: Adicionar um novo tipo de cliente requer apenas a criação de uma nova classe que implemente a interface
DiscountStrategy. - Baixo Acoplamento: A lógica de desconto está isolada em classes específicas, melhorando a modularidade.
- Adesão ao Open-Closed Principle: A classe
DiscountCalculatorestá fechada para modificações, mas aberta para extensões com novas estratégias.
Duplicate Code
Ocorre quando o mesmo código é repetido em várias partes do sistema. Isso pode ser problemático porque, se houver a necessidade de modificar esse código, será necessário atualizar todas as instâncias em que ele aparece, o que aumenta o risco de erros. Além disso, a duplicação torna o código mais difícil de manter e menos legível. A solução é abstrair a lógica repetida em uma função ou classe reutilizável.
class Order {
constructor(
public orderId: number,
public product: string,
public quantity: number,
public price: number
) {}
calculateTotal(): number {
return this.quantity * this.price;
}
printOrderSummary(): void {
const total = this.quantity * this.price; // código duplicado
console.log(`Pedido #${this.orderId}: ${this.product} - Total: $${total}`);
}
sendOrderConfirmation(): void {
const total = this.quantity * this.price; // código duplicado
console.log(`Confirmação de pedido #${this.orderId}: ${this.product} - Total: $${total}`);
}
}
// exemplo de uso
const order = new Order(101, 'Cadeira Gamer', 1, 500);
order.printOrderSummary();
order.sendOrderConfirmation();// solução
class Order {
constructor(
public orderId: number,
public product: string,
public quantity: number,
public price: number
) {}
private calculateTotal(): number {
return this.quantity * this.price;
}
printOrderSummary(): void {
const total = this.calculateTotal(); // reutilizando o código
console.log(`Pedido #${this.orderId}: ${this.product} - Total: $${total}`);
}
sendOrderConfirmation(): void {
const total = this.calculateTotal(); // reutilizando o código
console.log(`Confirmação de pedido #${this.orderId}: ${this.product} - Total: $${total}`);
}
}
// exemplo de uso
const order = new Order(101, 'Cadeira Gamer', 1, 500);
order.printOrderSummary();
order.sendOrderConfirmation();Speculative Generality
Ocorre quando o código é projetado para um cenário que ainda não existe, ou seja, é excessivamente generalizado e projetado com base em suposições de futuras extensões que talvez nunca ocorram. Isso pode resultar em complexidade desnecessária, já que o código é mais genérico do que o necessário no momento. O problema é que a tentativa de prever o futuro frequentemente leva a abstrações que não são usadas, tornando o código mais difícil de entender e manter.
class PaymentProcessor {
constructor(public paymentMethod: string) {}
processPayment(amount: number): void {
if (this.paymentMethod === 'CreditCard') {
this.processCreditCardPayment(amount);
return;
}
if (this.paymentMethod === 'PayPal') { // não foi chamado
this.processPayPalPayment(amount);
return;
}
if (this.paymentMethod === 'BankTransfer') { // não foi chamado
this.processBankTransferPayment(amount);
return;
}
console.log('Método de pagamento não suportado');
}
private processCreditCardPayment(amount: number): void { ... }
private processPayPalPayment(amount: number): void { ... }
private processBankTransferPayment(amount: number): void { ... }
}
// exemplo de uso
const paymentProcessor = new PaymentProcessor('CreditCard');
paymentProcessor.processPayment(100);// solução
class PaymentProcessor {
constructor(public paymentMethod: string) {}
processCreditCardPayment(amount: number): void { ... }
}
// exemplo de uso
const paymentProcessor = new PaymentProcessor();
paymentProcessor.processCreditCardPayment(100);Data Class
Ocorre quando uma classe é usada apenas para armazenar dados e não contém nenhum comportamento ou lógica significativa. Essas classes não têm métodos que realizem ações significativas com os dados, apenas armazenam valores. Isso pode ser problemático, pois uma classe sem comportamento não agrega valor real ao sistema e pode ser substituída por um simples objeto ou estrutura de dados.
class Product {
constructor(
public id: number,
public name: string,
public price: number
) {}
}
class Order {
private products: Product[] = [];
addProduct(product: Product): void {
this.products.push(product);
}
calculateTotal(): number {
return this.products.reduce((total, product) => total + product.price, 0);
}
}
// exemplo de uso
const product1 = new Product(1, 'Cadeira Gamer', 500);
const product2 = new Product(2, 'Mesa de Escritório', 300);
const order = new Order();
order.addProduct(product1);
order.addProduct(product2);
console.log(`Total do pedido: $${order.calculateTotal()}`);// solução
class Product {
constructor(
public id: number,
public name: string,
public price: number
) {}
applyDiscount(discount: number): void {
this.price -= this.price * discount / 100;
}
getProductInfo(): string {
return `${this.name} - $${this.price}`;
}
}
class Order {
private products: Product[] = [];
addProduct(product: Product): void {
this.products.push(product);
}
calculateTotal(): number {
return this.products.reduce((sum, product) => sum + product.price, 0);
}
}
// exemplo de uso
const product1 = new Product(1, 'Cadeira Gamer', 500);
const product2 = new Product(2, 'Mesa de Escritório', 300);
product1.applyDiscount(10); // aplica desconto de 10% no produto 1
const order = new Order();
order.addProduct(product1);
order.addProduct(product2);
console.log(`Total do pedido: $${order.calculateTotal()}`);
console.log(product1.getProductInfo()); // exibe as informações do produtoDead Code
Refere-se a trechos de código que nunca são executados, ou seja, são totalmente inúteis. Isso pode ocorrer quando funções, métodos, variáveis ou classes são criados, mas nunca são utilizados ou chamadas em nenhum lugar do programa. O código morto pode gerar confusão, aumentar o tamanho do código e dificultar a manutenção, pois pode fazer parecer que há mais funcionalidade do que realmente existe.
class ShoppingCart {
private items: string[] = [];
addItem(item: string): void {
this.items.push(item);
}
private printReceipt(): void { ... }
private unusedMethod(): void { ... } // metodo inutilizado
private checkInStock(): void { ... } // metodo inutilizado
checkout(): void {
this.printReceipt();
}
}
// exemplo de uso
const cart = new ShoppingCart();
cart.addItem('Camiseta');
cart.checkout();// solução
class ShoppingCart {
private items: string[] = [];
addItem(item: string): void {
this.items.push(item);
}
private printReceipt(): void { ... }
checkout(): void {
this.printReceipt();
}
}
// exemplo de uso
const cart = new ShoppingCart();
cart.addItem('Camiseta');
cart.checkout();God Object | Poor Separation of Concerns
Ocorre quando uma classe ou objeto assume responsabilidades demais, tornando-se excessivamente complexo e difícil de manter. Este “objeto deus” concentra muitas responsabilidades e, frequentemente, manipula dados de várias outras partes do sistema. Esse tipo de design viola os princípios de coesão e separação de responsabilidades, tornando o código mais difícil de entender e testar.
class System {
users: any[];
orders: any[];
inventory: any[];
constructor() {
this.users = [];
this.orders = [];
this.inventory = [];
}
addUser(user: any) {
this.users.push(user);
}
placeOrder(order: any) {
this.orders.push(order);
}
manageInventory(item: any) {
this.inventory.push(item);
}
}// solução
class UserService {
users: any[] = [];
addUser(user: any) {
this.users.push(user);
}
}
class OrderService {
orders: any[] = [];
placeOrder(order: any) {
this.orders.push(order);
}
}
class InventoryService {
inventory: any[] = [];
manageInventory(item: any) {
this.inventory.push(item);
}
}
class System {
userService: UserService;
orderService: OrderService;
inventoryService: InventoryService;
constructor() {
this.userService = new UserService();
this.orderService = new OrderService();
this.inventoryService = new InventoryService();
}
}Overengineering
Ocorre quando uma solução é mais complexa do que realmente necessita ser, adicionando funcionalidades, abstrações ou características que não são necessárias para o problema atual. Isso pode resultar em código difícil de entender, manter, testar, e geralmente é excessivo, considerando os requisitos reais do sistema.
O principal problema com o Overengineering é que ele pode resultar em um sistema excessivamente complexo, que é difícil de modificar e propenso a erros, enquanto que uma solução mais simples seria suficiente para atender às necessidades do negócio.
Sem exemplos de código.
Overloading Constructors
Ocorre quando uma classe tem múltiplos construtores para lidar com diferentes tipos de inicialização de objetos, o que pode levar a complexidade desnecessária. Em TypeScript (e outras linguagens como Java ou C#), um construtor pode ser “sobrecarregado” para aceitar diferentes combinações de parâmetros. No entanto, essa prática pode tornar o código mais difícil de entender e manter, pois pode ser difícil para os desenvolvedores entenderem qual construtor deve ser utilizado em determinado momento.
A sobrecarga de construtores pode ser problemática, pois cria várias versões de um método de inicialização, tornando o código confuso e mais propenso a erros. Além disso, pode violar o princípio de Responsabilidade Única no SOLID, já que cada construtor acaba lidando com várias formas de inicializar a classe.
Sem exemplos de código.
Circular Dependencies
Ocorrem quando dois ou mais módulos (ou classes) dependem diretamente ou indiretamente uns dos outros, criando um ciclo de dependências. Esse tipo de dependência pode tornar o sistema difícil de entender, testar e manter, além de introduzir problemas de performance e dificuldade na inicialização do código. Em muitas linguagens de programação, a presença de dependências circulares pode causar problemas como erros de importação ou loops infinitos.
class Author {
private name: string;
private books: Book[] = [];
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
addBook(book: Book): void {
this.books.push(book);
}
listBooks(): string[] {
return this.books.map((book) => book.getTitle());
}
}
class Book {
private title: string;
private author: Author;
constructor(title: string, author: Author) {
this.title = title;
this.author = author;
this.author.addBook(this); // Circular dependency ocorre aqui
}
getTitle(): string {
return this.title;
}
getAuthorName(): string {
return this.author.getName();
}
}
// exemplo de uso
const author = new Author('J.K. Rowling');
const book = new Book('Harry Potter', author);
console.log(book.getAuthorName()); // J.K. Rowling
console.log(author.listBooks()); // ['Harry Potter']// solução
class Author {
private name: string;
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
}
class Book {
private title: string;
constructor(title: string) {
this.title = title;
}
getTitle(): string {
return this.title;
}
getAuthorName(): string {
return this.author.getName();
}
}
class LibraryService {
private authorBooksMap: Map<Author, Book[]> = new Map();
addBook(author: Author, book: Book): void {
if (!this.authorBooksMap.has(author)) {
this.authorBooksMap.set(author, []);
}
this.authorBooksMap.get(author).push(book);
}
getBooksByAuthor(author: Author): string[] {
return this.authorBooksMap.get(author).map((book) => book.getTitle()) || [];
}
}
// exemplo de uso
const libraryService = new LibraryService();
const author = new Author('J.K. Rowling');
const book = new Book('Harry Potter', author);
libraryService.addBook(author, book);
console.log(book.getAuthorName()); // J.K. Rowling
console.log(libraryService.getBooksByAuthor(author)); // ['Harry Potter']Hard-Coded Values
São valores fixos inseridos diretamente no código, como strings, números ou caminhos. Isso dificulta a manutenção, reutilização e escalabilidade do código. Se esses valores precisarem ser alterados, você terá que modificar múltiplas partes do código.
class DiscountService {
calculateDiscount(price: number): number {
if (price > 1000) {
return price * 0.1; // valor fixo de 10% para descontos
} else if (price > 500) {
return price * 0.05; // valor fixo de 5% para descontos
} else {
return 0; // sem desconto
}
}
getCurrencySymbol(): string {
return '$'; // moeda hard-coded
}
}
// exemplo de uso
const discountService = new DiscountService();
console.log(discountService.calculateDiscount(1200)); // 120
console.log(discountService.getCurrencySymbol() + 1200); // $1200// solução
// arquivo discount-threshold.interface.ts
export interface IDiscountThreshold {
price: number;
discount: number;
}
// arquivo config.ts
export const discountThresholds: DiscountThreshold[] = [
{ price: 1000, discount: 0.1 }, // 10%
{ price: 500, discount: 0.05 }, // 5%
];
export const currencySymbol: string = '$';
// arquivo discount.service.ts
class DiscountService {
private readonly discountThresholds = config.discountThresholds;
private readonly currencySymbol = config.currencySymbol;
calculateDiscount(price: number): number {
for (const threshold of this.discountThresholds) {
if (price > threshold.price) {
return price * threshold.discount;
}
}
return 0;
}
getCurrencySymbol(): string {
return this.currencySymbol;
}
}Over-Commenting
Ocorre quando o código é acompanhado de explicações excessivas ou desnecessárias. Isso pode dificultar a leitura e a manutenção, especialmente quando os comentários se tornam desatualizados ou redundantes devido a alterações no código.
export class OrderService {
// este método calcula o total de um pedido
calculate(items: { price: number; quantity: number }[]): number {
let total = 0; // inicializa o total como zero
for (const item of items) {
total += item.price * item.quantity; // soma o preço total de cada item
}
return total; // retorna o total do pedido
}
// este método verifica se o pedido é elegível para frete grátis
verify(total: number): boolean {
if (total > 100) {
return true; // retorna verdadeiro se o total for maior que 100
} else {
return false; // retorna falso caso contrário
}
}
// este método formata o valor total como uma string com um símbolo de moeda
format(total: number): string {
return `$${total.toFixed(2)}`; // converte o número para string com duas casas decimais
}
}
// exemplo de uso
const orderService = new OrderService();
const items = [ { price: 20, quantity: 3 }, { price: 50, quantity: 1 } ];
const total = orderService.calculate(items); // calcula o total
orderService.format(total); // exibe o total formatado
orderService.verify(total); // verifica o frete grátis// solução
export class OrderService {
calculateTotal(items: { price: number; quantity: number }[]): number {
return items.reduce((total, item) => total + item.price * item.quantity, 0);
}
isEligibleForFreeShipping(total: number): boolean {
return total > 100;
}
formatTotal(total: number): string {
return `$${total.toFixed(2)}`;
}
}
// exemplo de uso
const orderService = new OrderService();
const items = [ { price: 20, quantity: 3 }, { price: 50, quantity: 1 } ];
const total = orderService.calculateTotal(items);
orderService.formatTotal(total);
orderService.isEligibleForFreeShipping(total);Too Many Interfaces
Ocorre quando o design do sistema inclui um número excessivo de interfaces que adicionam complexidade desnecessária sem proporcionar benefícios claros. Isso geralmente resulta em código difícil de entender, navegar e manter.
interface Item {
name: string;
price: number;
}
interface ItemWithDiscount extends Item {
discount: number;
}
interface ItemWithStock extends Item {
stock: number;
}
interface DiscountedStockItem extends ItemWithDiscount, ItemWithStock {
discountedPrice: number;
}
interface TaxableItem extends DiscountedStockItem {
taxRate: number;
finalPrice: number;
}
class InventoryManager {
calculateFinalPrice(item: TaxableItem): number { ... }
displayItemDetails(item: TaxableItem): void { ... }
}// solução
interface InventoryItem {
name: string;
price: number;
discount: number;
stock: number;
taxRate: number;
}
class InventoryManager {
calculateFinalPrice(item: InventoryItem): number { ... }
displayItemDetails(item: InventoryItem): void { ... }
}Tight Coupling | High Coupling
Ocorre quando as classes ou módulos estão fortemente dependentes uns dos outros, ou seja, quando uma classe depende fortemente de outra. Isso torna o código mais difícil de entender, testar e manter, pois mudanças em uma classe podem afetar várias outras. Isso também pode dificultar a reutilização de componentes.
interface User { ... }
class UserService {
private database: Database;
constructor() {
this.database = new Database(); // dependência direta de Database
}
createUser(name: string, email: string): void {
this.database.save(name, email); // chamada direta para o banco de dados
}
getUser(id: string): User {
return this.database.get(id); // chamada direta para o banco de dados
}
}
class Database {
save(name: string, email: string): void { ... }
get(id: string): User { ... }
}// solução
interface User { ... }
interface Database {
save(user: User): void;
get(id: string): User;
}
class UserService {
private database: Database;
constructor(database: Database) {
this.database = database; // Injeção de dependência
}
createUser(user: User): void {
this.database.save(user);
}
getUser(id: string): User {
return this.database.get(id);
}
}
class MongoDatabase implements Database {
save(user: User): void { ... }
get(id: string): User { ... }
}
class PostgreSQLDatabase implements Database {
save(user: User): void { ... }
get(id: string): User { ... }
}
// exemplo de uso
const mongoDb = new MongoDatabase();
const userService = new UserService(mongoDb);
userService.createUser("Alice", "alice@example.com");
const postgresqlDb = new PostgreSQLDatabase();
const userServicePostgres = new UserService(postgresqlDb);
userServicePostgres.createUser("Bob", "bob@example.com");Low Cohesion
Ocorre quando uma classe ou módulo tem muitas responsabilidades não relacionadas entre si. Em outras palavras, ela realiza tarefas que não têm uma relação clara entre si, o que torna a classe difícil de entender, testar e manter. Classes com baixa coesão tendem a ter métodos e variáveis que fazem coisas muito diferentes, o que dificulta a reutilização e a modificação de código.
Classes com alta coesão, por outro lado, devem ter um conjunto de responsabilidades que estão intimamente relacionadas entre si. Isso facilita a manutenção, a clareza e os testes, já que cada classe faz uma coisa bem definida.
class UtilityService {
// Métodos não relacionados entre si
logToConsole(message: string): void { ... }
saveToDatabase(data: any): void { ... }
sendEmail(email: string, subject: string, body: string): void { ... }
generateReport(data: any): void { ... }
}// solução
class Logger {
logToConsole(message: string): void { ... }
}
class DatabaseService {
saveToDatabase(data: any): void { ... }
}
class EmailService {
sendEmail(email: string, subject: string, body: string): void { ... }
}
class ReportService {
generateReport(data: any): void { ... }
}Magic Numbers
refere-se a valores numéricos literais usados diretamente no código sem uma explicação clara do que representam. Esses números “mágicos” podem ser confusos, difíceis de entender e difíceis de manter, pois não é claro por que um determinado número foi escolhido ou qual é seu significado.
A principal questão com o uso de Magic Numbers é que, ao ver um número diretamente no código, fica difícil para os desenvolvedores entenderem rapidamente o seu propósito sem procurar por um comentário ou documentação adicional. Se o valor precisar ser alterado, pode ser fácil cometer erros, já que o número pode ser usado em várias partes do código sem um contexto claro.
class ShippingCalculator {
calculateShippingCost(weight: number, distance: number): number {
// Número mágico: 5. O que significa esse valor? Por que 5?
return weight * distance * 5;
}
}// solução
class ShippingCalculator {
// Explicando o que o valor 5 representa
private static readonly BASE_SHIPPING_COST = 5;
calculateShippingCost(weight: number, distance: number): number {
return weight * distance * ShippingCalculator.BASE_SHIPPING_COST;
}
}Magic Strings
Muito semelhante ao Magic Numbers, mas no caso, se refere ao uso de strings literais no código sem contexto claro. Assim como os Magic Numbers, as Magic Strings são difíceis de entender, manter e modificar, pois não possuem um significado explícito que explique seu propósito no código.
class UserService {
getUserStatus(userId: string): string {
if (userId === "ad") {
return "ad";
}
if (userId === "gst") {
return "gst";
}
return "unknown";
}
}// solução
class UserService {
private static readonly ADMIN_STATUS = "ad";
private static readonly GUEST_STATUS = "gst";
private static readonly UNKNOWN_STATUS = "unknown";
getUserStatus(userId: string): string {
if (userId === UserService.ADMIN_STATUS) {
return UserService.ADMIN_STATUS;
}
if (userId === UserService.GUEST_STATUS) {
return UserService.GUEST_STATUS;
}
return UserService.UNKNOWN_STATUS;
}
}Failure to Use Constants
Está intimamente relacionado aos conceitos de Magic Numbers e Magic Strings, mas há uma leve diferença de ênfase. É um conceito mais amplo, que inclui a falha em definir qualquer tipo de valor fixo como uma constante, não apenas números ou strings.
Hidden Dependencies
Ocorre quando uma classe ou módulo depende de outro recurso ou classe de maneira implícita, ou seja, o recurso ou classe necessária não é explicitamente declarada como dependência. Isso dificulta a manutenção, os testes e a compreensão do código, pois fica difícil para um desenvolvedor perceber todas as dependências que uma classe ou módulo realmente possui.
Essa prática pode resultar em código difícil de testar, pois as dependências não estão claramente definidas ou injetadas. Além disso, isso pode levar a efeitos colaterais inesperados se a classe ou módulo dependente for alterado sem que sua dependência seja visível.
class OrderProcessor {
processOrder(orderId: string): void {
// A dependência de "InventoryService" está oculta dentro do método
const inventoryService = new InventoryService();
const isAvailable = inventoryService.checkAvailability(orderId);
if (isAvailable) {
...
}
...
}
}// solução
class InventoryService {
checkAvailability(orderId: string): boolean {
// Verificação de disponibilidade (exemplo simples)
return orderId === 'available';
}
}
class OrderProcessor {
private inventoryService: InventoryService;
// A dependência é passada explicitamente usando Injeção de Dependência
constructor(inventoryService: InventoryService) {
this.inventoryService = inventoryService;
}
processOrder(orderId: string): void {
const isAvailable = this.inventoryService.checkAvailability(orderId);
if (isAvailable) {
...
}
...
}
}Control Flag
É um padrão de controle de fluxo que usa uma variável booleana para controlar o fluxo de execução em um sistema, geralmente para alternar entre diferentes lógicas ou estados. Embora o uso de uma variável booleana para controle de fluxo não seja um problema em si, o abuso dessa prática pode tornar o código confuso, difícil de entender e difícil de manter.
Esse problema ocorre quando uma variável booleana é usada para controlar o comportamento de diversas lógicas em diferentes pontos do código, criando uma espécie de “bandeira” (flag) para determinar como o código deve se comportar. O uso de um Control Flag pode levar a fluxos de controle complicados, já que o código passa a depender de uma simples variável booleana para mudar sua lógica.
class OrderProcessor {
processOrder(orderId: string, isPriority: boolean): void {
let orderStatus = 'Pending';
if (isPriority) {
// lógica de prioridade
orderStatus = 'Priority Order';
...
} else {
// lógica normal
orderStatus = 'Normal Order';
...
}
console.log(`Order ${orderId} status: ${orderStatus}`);
}
}// solução
interface OrderProcessorStrategy {
processOrder(orderId: string): void;
}
class PriorityOrderProcessor implements OrderProcessorStrategy {
processOrder(orderId: string): void {
console.log(`Processing Priority Order: ${orderId}`);
...
}
}
class NormalOrderProcessor implements OrderProcessorStrategy {
processOrder(orderId: string): void {
console.log(`Processing Normal Order: ${orderId}`);
...
}
}
class OrderProcessor {
private orderProcessorStrategy: OrderProcessorStrategy;
constructor(orderProcessorStrategy: OrderProcessorStrategy) {
this.orderProcessorStrategy = orderProcessorStrategy;
}
processOrder(orderId: string): void {
this.orderProcessorStrategy.processOrder(orderId);
}
}
// exemplo de uso
const priorityOrderProcessor = new OrderProcessor(new PriorityOrderProcessor());
priorityOrderProcessor.processOrder('1'); // Processing Priority Order: 1
const normalOrderProcessor = new OrderProcessor(new NormalOrderProcessor());
normalOrderProcessor.processOrder('2'); // Processing Normal Order: 2Mutable Constants
Ocorre quando uma variável ou constante que deveria ser imutável é alterada durante a execução do programa. Isso vai contra o propósito de constantes, que são valores que, uma vez definidos, não devem ser modificados. Alterar um valor de uma constante pode levar a comportamentos imprevisíveis, dificuldade em rastrear o fluxo do código e aumentar a chance de erros, especialmente em sistemas grandes.
No TypeScript, o uso de const cria uma variável de referência imutável, mas não impede que objetos e arrays referenciados sejam modificados. Isso significa que, embora o valor de uma variável declarada com const não possa ser reatribuído, os dados internos (como propriedades de objetos ou elementos de arrays) podem ser modificados.
class DiscountCalculator {
private readonly discountRates: { [key: string]: number } = {
"standard": 0.1,
"premium": 0.2,
"gold": 0.3,
};
applyDiscount(customerType: string, amount: number): number {
if (this.discountRates[customerType]) {
// Modificando uma constante
this.discountRates[customerType] = 0.5; // Um "mutable constant"
return amount * (1 - this.discountRates[customerType]);
}
return amount;
}
}// solução
class DiscountCalculator {
private readonly discountRates: { [key: string]: number } = Object.freeze({
"standard": 0.1,
"premium": 0.2,
"gold": 0.3,
});
applyDiscount(customerType: string, amount: number): number {
const discountRate = this.discountRates[customerType];
if (discountRate) {
return amount * (1 - discountRate);
}
return amount;
}
}Leaky Abstraction
Ocorre quando uma abstração (como uma classe ou função que esconde detalhes internos) não esconde completamente a complexidade subjacente ou detalhes de implementação. Em vez disso, esses detalhes “vazam” para o código cliente, fazendo com que o cliente precise entender e lidar com esses detalhes. Isso pode levar a um código mais acoplado e difícil de manter, além de diminuir a flexibilidade da abstração.
O propósito de uma abstração é esconder os detalhes internos, permitindo que o código cliente interaja com uma interface simples e bem definida, sem precisar se preocupar com os detalhes internos de implementação. Quando a abstração “vaza”, o código cliente é forçado a lidar com aspectos internos que deveriam ser invisíveis para ele.
function formatDate(date: Date): string {
const day = date.getDate();
const month = date.getMonth() + 1; // Month starts at 0
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}O Vazamento:
- Dependência do formato: A função assume que queremos a data no formato DD/MM/AAAA. Se precisarmos mudar para AAAA-MM-DD, teremos que alterar a função.
- Conhecimento interno: O código que usa essa função precisa saber que o mês retornado pela função
getMonthcomeça em 0, o que é um detalhe de implementação do objetoDate.
// solução
interface DateFormatter {
format(date: Date): string;
}
class BrazilianDateFormatter implements DateFormatter {
format(date: Date): string {
return date.toLocaleDateString('pt-BR');
}
}
class USDateFormatter implements DateFormatter {
format(date: Date): string {
return date.toLocaleDateString('en-US');
}
}
// exemplo de uso
...
displayDate(formatter: DateFormatter, date: Date) {
const formattedDate = formatter.format(date);
console.log(formattedDate);
}
const today = new Date();
const brazilianFormatter = new BrazilianDateFormatter();
displayDate(brazilianFormatter, today);Por que isso é melhor?
- Flexibilidade: Podemos facilmente adicionar novos formatadores para outros locais sem alterar o código que usa a função.
- Leiturabilidade: O código fica mais claro, pois a formatação da data está encapsulada em classes específicas.
- Manutenibilidade: Se precisarmos mudar a forma como formatamos a data em um determinado local, basta alterar a classe correspondente.
Em resumo:
Ao criar uma interface FormatadorData, abstraímos a lógica de formatação da data, permitindo que o código que utiliza essa interface se preocupe apenas com o resultado final, e não com os detalhes de como a data é formatada.
Outros exemplos de vazamentos de abstração em contextos mais simples:
- Função que calcula a área de um círculo: Expor o valor de pi dentro da função, em vez de usar uma constante definida em outro lugar.
- Classe que representa um usuário: Expor o ID interno do usuário como uma propriedade pública, em vez de fornecer métodos para obter informações sobre o usuário.
Poltergeist
Ocorre quando uma classe tem um ou mais métodos que não fazem sentido dentro dela, ou seja, quando uma classe possui comportamento que não se encaixa no seu propósito. Isso pode ocorrer quando uma classe possui métodos ou funcionalidades que estão ali apenas para interagir com outras partes do sistema, mas não têm um papel próprio.
class Employee {
private name: string;
private salary: number;
constructor(name: string, salary: number) {
this.name = name;
this.salary = salary;
}
// não faz sentido aqui, deveria estar na classe PayrollService
generatePayroll() {
...
}
...
}
class PayrollService {
private employees: Employee[];
constructor(employees: Employee[]) {
this.employees = employees;
}
// responsabilidade da classe PayrollService é gerar a folha de pagamento, não de Employee
generatePayroll() {
for (let employee of this.employees) {
// Chama um método de Employee, o que é um poltergeist
employee.generatePayroll();
}
}
}
// exemplo de uso
const employee1 = new Employee('John Doe', 5000);
const employee2 = new Employee('Jane Smith', 6000);
const payrollService = new PayrollService([employee1, employee2]);
payrollService.generatePayroll(); // "John Doe receives a salary of $5000"// solução
class Employee {
private name: string;
private salary: number;
constructor(name: string, salary: number) {
this.name = name;
this.salary = salary;
}
...
}
class PayrollService {
private employees: Employee[];
constructor(employees: Employee[]) {
this.employees = employees;
}
// agora, o método generatePayroll está aqui, na classe apropriada
generatePayroll() {
for (let employee of this.employees) {
console.log(`${employee.getName()} receives a salary of $${employee.getSalary()}`);
}
}
}
// exemplo de uso
const employee1 = new Employee('John Doe', 5000);
const employee2 = new Employee('Jane Smith', 6000);
const payrollService = new PayrollService([employee1, employee2]);
payrollService.generatePayroll(); //"John Doe receives a salary of $5000"Static Cling
Ocorre quando uma classe ou método depende excessivamente de métodos ou dados estáticos, tornando o código difícil de testar, expandir ou modificar. Isso acontece porque os membros estáticos criam um forte acoplamento, removendo a flexibilidade de substituir ou mockar dependências em testes.
Sem exemplos de código.
Imperative Comments
Ocorre quando os comentários no código são usados para instruir como o código deve funcionar em vez de simplesmente explicar ou documentar. Isso é um problema porque os comentários podem se tornar desatualizados ou desnecessários, especialmente quando o código é alterado. Além disso, um código bem escrito deve ser claro por si só, com comentários sendo usados apenas para explicar o “porquê”, e não o “como”.
Sem exemplos de código.
Outdated Comments
Ocorre quando os comentários no código não refletem mais o comportamento ou o propósito do código. Isso pode causar confusão, levar a suposições erradas e dificultar a manutenção. Geralmente, isso acontece quando o código é alterado, mas os comentários não são atualizados.
Sem exemplos de código.
Unused Variables
Ocorre quando variáveis são declaradas no código, mas nunca são usadas. Isso pode ser um indicador de código mal projetado, confuso ou deixado após uma alteração incompleta. Remover variáveis não utilizadas melhora a legibilidade e evita confusões desnecessárias.
Sem exemplos de código.
Excessive Logging
Ocorre quando o código possui um número excessivo de logs, dificultando a identificação de mensagens importantes. Isso pode impactar a performance, gerar arquivos de log grandes e tornar a análise de problemas mais difícil.
Sem exemplos de código.
Overuse of Globals
Ocorre quando variáveis globais ou objetos globais são utilizados de forma excessiva no código. Isso pode levar a problemas de manutenção, testes e compreensão do sistema, além de aumentar o risco de erros causados por modificações inesperadas de variáveis globais em partes diferentes do código.
Sem exemplos de código.
Lack of Encapsulation
Ocorre quando uma classe ou objeto expõe seus dados internos diretamente, sem ocultá-los adequadamente. O encapsulamento é um dos princípios fundamentais da programação orientada a objetos e se refere ao ato de esconder o estado interno de um objeto, permitindo acesso e modificação apenas por meio de métodos ou propriedades controladas.
Get Gilvam Mourão’s stories in your inbox
Join Medium for free to get updates from this writer.
A falta de encapsulação pode causar diversos problemas, como:
- Dificuldade de manutenção: Se os dados internos podem ser modificados de qualquer lugar, isso pode levar a mudanças inesperadas e difícil rastreabilidade do que causou a alteração.
- Baixa modularidade: Classes que expõem seu estado interno diretamente dificultam a criação de componentes reutilizáveis e isolados.
- Dificuldade de validação: Quando não há controle sobre o acesso aos dados, não é possível garantir que eles sempre estejam em um estado válido.
Insufficient Testing
Ocorre quando o código não tem testes adequados ou cobertura de testes insuficiente para garantir que o comportamento do sistema seja validado corretamente. Isso pode levar a erros não detectados, comportamentos inesperados e uma maior dificuldade em refatorar o código, já que não há uma rede de segurança proporcionada pelos testes automatizados.
Inconsistent Indentation
Ocorre quando o código tem indentação inconsistente, o que torna o código difícil de ler, entender e manter. Isso pode acontecer por várias razões, como misturar espaços e tabulações, usar diferentes estilos de indentação (por exemplo, 2 espaços em alguns lugares e 4 espaços em outros), ou simplesmente não seguir um padrão claro para toda a base de código.
class User {
name: string;
age: number;
constructor(name: string,age: number) {
this.name = name;
age = age;
}
getName():string {
return this.name;
}
}// solução
class User {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
age = age;
}
getName(): string {
return this.name;
}
}Dependency Injection Overuse
Ocorre quando o uso de Dependency Injection (DI) é exagerado ou mal aplicado, levando a um código difícil de manter, entender e testar. Embora DI seja uma boa prática para gerenciar dependências, o excesso pode resultar em configurações complexas e classes que dependem de muitas dependências, algumas das quais podem nem ser usadas.
Excessive Object Creation
Ocorre quando objetos são criados repetidamente, muitas vezes de forma desnecessária, em vez de serem reutilizados ou otimizados. Esse code smell pode levar a um uso excessivo de memória e processamento, especialmente em sistemas de alta carga.
Excessive Overloading
Ocorre quando uma classe possui muitas versões de métodos sobrecarregados com a mesma assinatura base (várias funções com mesmo nome), mas parâmetros diferentes. Isso pode tornar o código difícil de entender, manter e testar, já que os desenvolvedores podem se confundir com qual método está sendo chamado em um determinado momento.
Reused Constants
Ocorre quando constantes são usadas de forma inconsistente ou inadequada em diferentes partes do código. Por exemplo, uma constante definida para um propósito específico acaba sendo usada em situações que não refletem sua intenção original. Isso pode levar a confusão, bugs e dificulta a manutenção do código.
const DISCOUNT_PERCENTAGE = 10; // usada para representar desconto de produtos
class Product {
constructor(public name: string, public price: number) {}
public applyDiscount(): number {
return this.price - (this.price * DISCOUNT_PERCENTAGE) / 100;
}
}
class TaxCalculator {
// usando a mesma constante para calcular impostos
public calculateTax(price: number): number {
return (price * DISCOUNT_PERCENTAGE) / 100; // uso inadequado
}
}// solução
const PRODUCT_DISCOUNT_PERCENTAGE = 10;
const TAX_PERCENTAGE = 15;
class Product {
constructor(public name: string, public price: number) {}
public applyDiscount(): number {
return this.price - (this.price * PRODUCT_DISCOUNT_PERCENTAGE) / 100;
}
}
class TaxCalculator {
public calculateTax(price: number): number {
return (price * TAX_PERCENTAGE) / 100;
}
}Non-Descriptive Names
Ocorre quando nomes de variáveis, funções, classes ou outros elementos do código são vagos, confusos ou não representam claramente sua finalidade. Isso torna o código difícil de entender, mantendo e propenso a erros.
class A {
private x: number;
private y: number;
constructor(a: number, b: number) {
this.x = a;
this.y = b;
}
public doThing(): number {
return this.x + this.y;
}
}
// exemplo de uso
const obj = new A(5, 10);
console.log(obj.doThing()); // o que "doThing" faz?// solução
class Point {
private xCoordinate: number;
private yCoordinate: number;
constructor(xCoordinate: number, yCoordinate: number) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
}
public calculateSum(): number {
return this.xCoordinate + this.yCoordinate;
}
}
// exemplo de uso
const point = new Point(5, 10);
console.log(`Sum of coordinates: ${point.calculateSum()}`);Excessive Getters/Setters
Ocorre quando há um uso excessivo de métodos de acesso (getters e setters) em uma classe. Isso pode levar a um design mais rígido, onde as operações em objetos se tornam mais complicadas e menos intuitivas, reduzindo a coesão e violando o princípio de encapsulamento.
class Product {
private _name: string;
private _price: number;
constructor(name: string, price: number) {
this._name = name;
this._price = price;
}
public getName(): string {
return this._name;
}
public setName(name: string): void {
this._name = name;
}
public getPrice(): number {
return this._price;
}
public setPrice(price: number): void {
this._price = price;
}
}
// exemplo de uso
const product = new Product("Laptop", 1500);
console.log(product.getName());
console.log(product.getPrice());
product.setPrice(1400);
console.log(product.getPrice());// solução
class Product {
constructor(private name: string, private price: number) {}
public applyDiscount(discount: number): void {
if (discount < 0 || discount > 1) {
throw new Error("Discount must be between 0 and 1.");
}
this.price = this.price * (1 - discount);
}
public getDescription(): string {
return `Product: ${this.name}, Price: $${this.price.toFixed(2)}`;
}
}
// exemplo de uso
const product = new Product("Laptop", 1500);
console.log(product.getDescription());
product.applyDiscount(0.1);
console.log(product.getDescription());Unnecessary Abstraction
Ocorre quando um sistema ou classe possui abstrações que não são necessárias, ou seja, quando um nível extra de complexidade é introduzido sem trazer valor, resultando em um design mais difícil de entender e manter. Isso geralmente acontece quando você cria classes, interfaces ou métodos para representar algo simples demais, ou quando as abstrações são excessivas para o que está sendo feito.
class Engine {
public start(): void {
console.log("Engine started");
}
public stop(): void {
console.log("Engine stopped");
}
}
class Car {
private engine: Engine;
constructor(engine: Engine) {
this.engine = engine;
}
public startCar(): void {
this.engine.start();
}
public stopCar(): void {
this.engine.stop();
}
}
// exemplo de uso
const engine = new Engine();
const car = new Car(engine);
car.startCar();
car.stopCar();// solução
class Car {
public start(): void {
console.log("Car started");
}
public stop(): void {
console.log("Car stopped");
}
}
// exemplo de uso
const car = new Car();
car.start();
car.stop();Deep Nesting
Ocorre quando o código possui muitos níveis de aninhamento (como loops dentro de loops, condicionais dentro de condicionais, etc.). Isso dificulta a leitura e compreensão do código, além de aumentar a complexidade e o risco de erros. O aninhamento profundo pode ser um sinal de que o código está tentando fazer muitas coisas ao mesmo tempo ou que a lógica pode ser simplificada.
...
processOrder(order: IOrder): string {
if (order.status === 'pending') {
if (order.paymentStatus === 'paid') {
if (order.items && order.items.length > 0) {
if (order.shippingAddress) {
return `Order ${order.id} is ready to ship to ${order.shippingAddress}`;
} else {
return 'Shipping address is missing';
}
} else {
return 'Order items are missing';
}
} else {
return 'Payment not completed';
}
} else {
return 'Order is not pending';
}
}
...
// exemplo de uso
const order: IOrder = {
id: 123,
status: 'pending',
paymentStatus: 'paid',
items: [{ id: 1, name: 'Laptop' }],
shippingAddress: '123 Main St'
};
processOrder(order);// solução
validateOrder(order: any): string | null {
if (order.status !== 'pending') {
return 'Order is not pending';
}
if (order.paymentStatus !== 'paid') {
return 'Payment not completed';
}
if (!order.items || order.items.length === 0) {
return 'Order items are missing';
}
if (!order.shippingAddress) {
return 'Shipping address is missing';
}
return null;
}
processOrder(order: any): string {
const validationError = validateOrder(order);
if (validationError) {
return validationError;
}
return `Order ${order.id} is ready to ship to ${order.shippingAddress}`;
}
// exemplo de uso
const order = {
id: 123,
status: 'pending',
paymentStatus: 'paid',
items: [{ id: 1, name: 'Laptop' }],
shippingAddress: '123 Main St'
};
console.log(processOrder(order));A solução oferecida apenas remove o Deep Nesting mas ainda continúa com outro problema de várias condicionais.
Excessive Conditionals | Too Many Conditionals | Excessive Branching
Ocorre quando um código possui uma grande quantidade de declarações condicionais (if, else if, switch, etc.). Isso torna o código difícil de ler, manter e testar. Frequentemente, esse problema pode ser resolvido ao substituir condicionais por abordagens mais elegantes, como o uso de polimorfismo, tabelas de busca ou mapeamento de funções.
calculateDiscount(
orderType: string,
customerType: string,
orderAmount: number
): number {
if (orderType === 'regular') {
if (customerType === 'premium') {
if (orderAmount > 1000) {
return orderAmount * 0.2;
} else {
return orderAmount * 0.1;
}
} else if (customerType === 'regular') {
if (orderAmount > 1000) {
return orderAmount * 0.15;
} else {
return orderAmount * 0.05;
}
}
} else if (orderType === 'special') {
if (customerType === 'premium') {
return orderAmount * 0.25;
} else if (customerType === 'regular') {
return orderAmount * 0.2;
}
}
return 0;
}// solução
enum OrderType {
REGULAR = "regular",
SPECIAL = "special",
}
enum CustomerType {
PREMIUM = "premium",
REGULAR = "regular",
}
// Lookup Table design pattern
const discountTable: Record<string, (orderAmount: number) => number> = {
"regular-premium-high": (amount: number) => amount * 0.2,
"regular-premium-low": (amount: number) => amount * 0.1,
"regular-regular-high": (amount: number) => amount * 0.15,
"regular-regular-low": (amount: number) => amount * 0.05,
"special-premium": (amount: number) => amount * 0.25,
"special-regular": (amount: number) => amount * 0.2,
};
getKey(
orderType: OrderType,
customerType: CustomerType,
orderAmount: number
): string {
const level = orderType === OrderType.REGULAR && orderAmount > 1000 ? "high" : "low";
return `${orderType}-${customerType}-${level}`;
}
calculateDiscount(
orderType: OrderType,
customerType: CustomerType,
orderAmount: number
): number {
const key = getKey(orderType, customerType, orderAmount);
return discountTable[key]?.(orderAmount) ?? 0;
}
// exemplos de uso:
calculateDiscount(OrderType.REGULAR, CustomerType.PREMIUM, 1500); // 300
calculateDiscount(OrderType.SPECIAL, CustomerType.REGULAR, 800); // 160
calculateDiscount(OrderType.REGULAR, CustomerType.REGULAR, 500); // 25
calculateDiscount(OrderType.REGULAR, CustomerType.PREMIUM, 500); // 50Excessive Type Casting
Ocorre quando um código realiza muitas conversões de tipos, ou quando há conversões de tipos que não são necessárias, o que pode dificultar a legibilidade e aumentar a complexidade do código. O excesso de castings pode indicar que o código não está bem estruturado e pode ser refatorado para usar tipos mais apropriados ou técnicas que evitem essas conversões.
calculateDiscount(price: any, discount: any): any {
// Excessive casting
// (tornar explícito para string e depois de volta para number)
let priceNumeric = <number>price;
let discountNumeric = <number>discount;
if (isNaN(priceNumeric) || isNaN(discountNumeric)) {
throw new Error('Invalid input');
}
let discountedPrice = priceNumeric - (priceNumeric * discountNumeric) / 100;
return <string>discountedPrice.toFixed(2); // Excessive conversion to string
}
// exemplo de uso
calculateDiscount('100', '20'); // '80.00'// solução
calculateDiscount(price: number, discount: number): number {
if (isNaN(price) || isNaN(discount)) {
throw new Error('Invalid input');
}
const discountedPrice = price - (price * discount) / 100;
return parseFloat(discountedPrice.toFixed(2));
}
// exemplo de uso
calculateDiscount(100, 20); // 80.00Monolithic Class
Ocorre quando uma única classe assume muitas responsabilidades ou funções que deveriam estar distribuídas entre várias classes menores. Essa abordagem vai contra o princípio da responsabilidade única do SOLID e torna o código difícil de entender, testar e manter.
class OrderManager {
addProduct() { ... }
removeProduct() { ... }
calculateTotal() { ... }
applyDiscount() { ... }
processPayment() { ... }
sendInvoice() { ... }
trackShipment() { ... }
notifyCustomer() { ... }
}// solução
class ProductManager {
addProduct() { ... }
removeProduct() { ... }
}
class PaymentProcessor {
calculateTotal() { ... }
applyDiscount() { ... }
processPayment() { ... }
}
class NotificationService {
sendInvoice() { ... }
notifyCustomer() { ... }
}
class ShipmentTracker {
trackShipment() { ... }
}Unreachable Code
Ocorre quando há partes do código que nunca serão executadas devido à lógica existente ou à estrutura de controle. Isso pode resultar de condicionais mal projetadas, loops que nunca terminam, ou código que segue uma declaração de retorno antecipado.
// problema A
processOrder(order: string): void {
if (!order) {
return;
}
console.log('Order received'); // este código é alcançável
return;
console.log('Processing order'); // este código nunca será executado
}// solução A
processOrder(order: string): void {
if (!order) {
return;
}
console.log('Order received');
}// problema B
processData(data: string[]): void {
let index = 0;
while (true) { // loop infinito com alguma condiçao que sempre retorna true
if (index >= data.length) {
break; // pode quebrar o loop, mas até lá o código abaixo não é alcançado
}
index++;
}
console.log('Processing complete'); // nunca será alcançado
}// solução B
processData(data: string[]): void {
let index = 0;
while (index < data.length) { // loop com condição de saída
console.log(data[index]);
index++;
}
console.log('Processing complete');
}Overuse of Try-Catch
Ocorre quando o bloco de código usa a estrutura try-catch de maneira excessiva, mesmo para situações que não requerem tratamento de exceções. Isso pode gerar uma sobrecarga no código, diminuir a legibilidade e prejudicar o desempenho. Usar try-catch de maneira inadequada pode ocultar falhas inesperadas ou criar um fluxo de controle não claro.
processData(data: any): string {
try {
let processedData = parseData(data); // não precisa do try-catch here
return `Processed Data: ${processedData}`;
} catch (error) {
console.error('Error during data processing', error);
return 'Error during processing';
}
try {
let result = computeResult(data); // não precisa
return `Result: ${result}`;
} catch (error) {
console.error('Error during result computation', error);
return 'Error during computation';
}
try {
let formattedResult = formatResult(data); // não precisa
return `Formatted Result: ${formattedResult}`;
} catch (error) {
console.error('Error during result formatting', error);
return 'Error during formatting';
}
}// solução
processData(data: any): string {
const processedData = parseData(data);
const result = computeResult(data);
const formattedResult = formatResult(data);
return `Processed Data: ${processedData}, Result: ${result}, Formatted Result: ${formattedResult}`;
}Excessive Exceptions
Ocorre quando uma aplicação lança um número excessivo de exceções, muitas vezes por situações que podem ser tratadas de outras maneiras mais eficazes ou sem o uso de exceções. Lançar exceções frequentemente pode tornar o código mais difícil de entender, diminuir a performance e tornar o gerenciamento de erros mais complexo do que realmente deveria ser.
Exceções são projetadas para situações excepcionais e imprevistas, e não para controle normal de fluxo ou para eventos que podem ser previstos e tratados de forma mais apropriada. O uso excessivo de exceções pode gerar sobrecarga no código e no desempenho da aplicação, especialmente em linguagens como JavaScript/TypeScript, onde as exceções podem ser caras em termos de desempenho.
class OrderProcessor {
processOrder(orderId: string) {
try {
if (orderId === '') {
throw new Error('Order ID cannot be empty');
}
if (orderId === 'invalid') {
throw new Error('Order ID is invalid');
}
if (orderId === 'unavailable') {
throw new Error('Order ID is unavailable');
}
...
console.log(`Order ${orderId} processed successfully`);
} catch (error) {
console.error('Error processing order:', error.message);
}
}
}// solução
class OrderProcessor {
processOrder(orderId: string) {
if (orderId === '') {
console.error('Error: Order ID cannot be empty');
return;
}
if (orderId === 'invalid') {
console.error('Error: Order ID is invalid');
return;
}
if (orderId === 'unavailable') {
console.error('Error: Order ID is unavailable');
return;
}
...
console.log(`Order ${orderId} processed successfully`);
}
}Invalid Error Handling
Ocorre quando o tratamento de erros é inadequado ou mal implementado, causando comportamentos inesperados, perda de informações importantes ou dificultando a depuração. Exemplos comuns incluem:
- Uso excessivo de
catchgenérico. - Supressão de erros sem registro ou notificação.
- Lançar erros genéricos em vez de específicos.
- Uso indevido de estruturas de controle para mascarar erros.
class FileManager {
readFile(filePath: string): string {
try {
if (!filePath) {
throw new Error("File path is missing");
}
return "File content"; // simulação de retorno do arquivo
} catch (error) {
console.log("An error occurred");
return ""; // Retorna um valor vazio sem tratar o problema
}
}
}// solução
class FileManager {
readFile(filePath: string): string {
try {
if (!filePath) {
throw new Error("File path is missing");
}
return "File content";
} catch (error) {
// Registra o erro com detalhes
console.error(`Error reading file: ${(error as Error).message}`);
throw new Error(
"Failed to read file. Please check the file path and try again."
);
}
}
}Improper Resource Handling
Ocorre quando recursos como arquivos, conexões de rede, ou conexões de banco de dados não são gerenciados corretamente, levando a vazamentos de recursos, bloqueios ou outros problemas de desempenho. Recursos precisam ser abertos e fechados de forma apropriada para garantir que sejam liberados corretamente, mesmo em caso de erros.
readFile(filePath: string): string {
const fileHandle = openFile(filePath); // abre o arquivo
const data = fileHandle.read();
// não fecha o arquivo, o que pode causar vazamentos de recursos
return data;
}// solução
readFile(filePath: string): string {
const fileHandle = openFile(filePath);
try {
return fileHandle.read();
}
catch(err) {
...
}
finally {
fileHandle.close(); // fecha o arquivo, mesmo em caso de erros
}
}Incomplete Exception Handling
Ocorre quando um programa lança ou captura exceções de forma incompleta, sem tratá-las adequadamente. Isso pode ocorrer quando as exceções são capturadas sem uma lógica clara de tratamento ou quando exceções são lançadas mas não são manipuladas corretamente. Isso pode levar a falhas no sistema, mensagens de erro genéricas ou comportamento inesperado do programa.
readFile(fileName: string): string {
try {
const fs = require('fs');
return fs.readFileSync(fileName, 'utf8');
} catch (error) {
// a exceção é capturada mas sem tratar adequadamente
console.error('Erro ao ler o arquivo.');
return '';
}
}// solução
readFile(fileName: string): string {
const fs = require('fs');
try {
return fs.readFileSync(fileName, 'utf8');
} catch (error) {
// fornecendo informações detalhadas sobre o erro
console.error(`Erro ao ler o arquivo: ${error.message}`);
// lançando novamente a exceção, assim o chamador cria o tratamento
throw new Error(`Não foi possível ler o arquivo ${fileName}`);
}
}Unchecked Exceptions | Empty Catch Block
Ocorrem quando exceções são lançadas ou propagadas sem serem verificadas ou tratadas adequadamente, muitas vezes sem a devida tentativa de captura ou de fornecimento de informações suficientes para o diagnóstico do problema.
Essas exceções são chamadas de “unchecked” porque o compilador não exige que elas sejam tratadas explicitamente em um bloco try-catch ou que sejam declaradas como parte de uma assinatura de função, como ocorre com exceções verificadas (checked exceptions) em outras linguagens, como Java.
divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Divisão por zero não é permitida.");
}
return a / b;
}
process(): void {
// a exceção não é tratada, o código vai quebrar aqui
const result = divide(10, 0);
}// solução
divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Divisão por zero não é permitida.");
}
return a / b;
}
process() {
try {
const result = divide(10, 0); // a exceção é capturada aqui
...
} catch (error) {
// feedback claro para o usuário
console.error("Erro ao processar a divisão: ", error.message);
...
}
}Overuse of Annotations
Ocorre quando anotações (como tipos ou comentários) são usadas em excesso ou de maneira desnecessária, prejudicando a legibilidade e manutenibilidade do código. Isso geralmente acontece quando se tenta adicionar um nível de detalhamento excessivo em áreas que não requerem explicações ou quando as anotações acabam sendo redundantes.
// funcao que calcula a área passando largura e altura em inteiros
function calc(width, height) {
let area: number = width * height; // variável que armazena a área
return area; // retornando a área
}
// a variável 'result' é do tipo 'number' e contem o valor da área
let result: number = calc(10, 20);Simplifique os métodos e variáveis com os nomes adequados explicando o que cada um deles faz.
// solução
calculateRectangleArea(width: number, height: number): number {
return width * height;
}
let rectangleArea = calculateRectangleArea(10, 20);Improper Loop Usage
Ocorre quando um loop é usado de maneira ineficiente ou incorreta, resultando em um desempenho ruim ou em um código difícil de entender e manter. Isso pode incluir o uso de loops desnecessários, loops que poderiam ser substituídos por uma operação mais simples ou até mesmo loops que são mal projetados, levando a um comportamento inesperado.
Exemplos comuns de Improper Loop Usage incluem:
- Usar um loop onde uma operação direta poderia ser realizada (
map,forEach,reduce). Visite o os métodos mais utilizados para arrays no javascript. - Usar um loop aninhado sem necessidade, o que resulta em complexidade desnecessária.
- Utilizar um loop de forma que ele execute mais vezes do que o necessário, causando ineficiência no código.
// problema A
sumNumbers(numbers: number[]): number {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]; // loop desnecessário para soma
}
return sum;
}
const numbers = [1, 2, 3, 4, 5];
const total = sumNumbers(numbers); // funciona, mas o loop é ineficiente// solução A
sumNumbers(numbers: number[]): number {
return numbers.reduce((acc, num) => acc + num, 0);
}
const numbers = [1, 2, 3, 4, 5];
sumNumbers(numbers); // correto com código mais eficiente e legível// problema B
findItem(numbers: number[], target: number): boolean {
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] === target) {
return true; // loop desnecessário
}
}
return false;
}
const numbers = [1, 2, 3, 4, 5];
findItem(numbers, 3); // funciona, mas é ineficiente// solução B
findItem(numbers: number[], target: number): boolean {
return numbers.includes(target);
}
const numbers = [1, 2, 3, 4, 5];
console.log(findItem(numbers, 3));// problema C
hasDuplicates(arr: number[]): boolean {
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
if (arr[i] === arr[j]) {
return true; // loop aninhado desnecessário para verificar duplicatas
}
}
}
return false;
}// solução C funcional:
hasDuplicates(arr: number[]): boolean {
const seen = new Set();
for (let num of arr) {
if (seen.has(num)) {
return true;
}
seen.add(num);
}
return false;
}
// solução C simplificada:
hasDuplicates(arr: number[]): boolean {
return arr.some((num, idx) => arr.indexOf(num) !== idx);
};Unoptimized Algorithms
Similar ao Improper Loop Usage mas focado no desempenho. Se referem a algoritmos que não estão implementados da maneira mais eficiente possível, levando a uso excessivo de recursos como tempo de execução ou memória. Esse code smell pode resultar em desempenho insatisfatório, especialmente em casos de grandes volumes de dados ou sistemas em produção com alta demanda..
function getAllPairs(arr: number[]): number[][] {
const pairs: number[][] = [];
for (let i = 0; i < arr.length; i++) {
for (let j = i + 1; j < arr.length; j++) {
pairs.push([arr[i], arr[j]]);
}
}
return pairs;
}
// exemplo de uso
const numbers = [1, 2, 3, 4, 5];
console.log(getAllPairs(numbers));
// Saída: [[1, 2], [1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5], [3, 4], [3, 5], [4, 5]]// solução
function* generatePairs(arr: number[]): Generator<[number, number]> {
let i = 0;
while (i < arr.length) {
for (let j = i + 1; j < arr.length; j++) {
yield [arr[i], arr[j]]; // gerando os pares sob demanda
}
i++;
}
}
// exemplo de uso
const numbers = [1, 2, 3, 4, 5];
const pairsGenerator = generatePairs(numbers);
for (const pair of pairsGenerator) {
console.log(pair); // os pares são gerados conforme necessário
}
Improper Null Handling
Ocorre quando o código não lida corretamente com valores null ou undefined, o que pode causar falhas inesperadas em tempo de execução. Esse code smell geralmente é associado a verificações insuficientes para valores nulos ou à falta de tratamento adequado de objetos que podem ser null ou undefined.
interface User {
name: string;
address?: { street: string; city: string };
}
function printCity(user: User): void {
// Erro: Cannot read property 'city' of undefined
console.log(user.address.city);
}
// exemplo de uso
const user: User = { name: "Alice" };
printCity(user); // o código quebra porque `address` é `undefined`// solução
class Address {
constructor(public street = '', public city = '') {}
get isEmpty(): boolean {
return !(this.street && this.city);
}
}
class User {
constructor(public name: string, public address = new Address()) {}
printCity(): void {
if (!this.address.isEmpty) {
console.log(this.address.city);
} else {
console.warn("Endereço não disponível.");
}
}
}
// exemplo de uso
const user = new User("Alice");
user.printUserCity(); // Saída: "Endereço não disponível"
const userWithAddress = new User("Bob", new Address("123 Main St", "New York"));
userWithAddress.printCity(); // Saída: "New York"Circular Logic
Ocorre quando a lógica de um programa ou função depende de si mesma de maneira redundante ou infinita, criando um ciclo que não permite que o sistema evolua ou termine de maneira adequada. Esse code smell pode resultar em loops infinitos ou em um comportamento inesperado onde a solução de um problema depende de uma condição que só pode ser resolvida após uma outra ser completada, criando um ciclo sem fim.
A principal característica da lógica circular é que ela tende a criar dependências cíclicas, onde uma condição depende de outra que, por sua vez, depende da primeira..
class BankAccount {
balance: number;
constructor(balance: number) {
this.balance = balance;
}
withdraw(amount: number): void {
if (this.balance < 0) {
this.balance = this.checkBalance();
}
if (this.balance >= amount) {
this.balance -= amount;
console.log("Saque realizado com sucesso!");
} else {
console.log("Saldo insuficiente.");
}
}
checkBalance(): number {
// verificação do saldo chama a função que tenta realizar o saque, criando um ciclo
this.withdraw(0); // Essa linha cria a lógica circular chamando o checkBalance novamente
return this.balance;
}
}
// exemplo de uso
const account = new BankAccount(100);
account.withdraw(50); // O código entra em um ciclo de chamadas// solução
class BankAccount {
balance: number;
constructor(balance: number) {
this.balance = balance;
}
withdraw(amount: number): void {
if (this.balance >= amount) {
this.balance -= amount;
console.log("Saque realizado com sucesso!");
return;
}
console.log("Saldo insuficiente.");
}
checkBalance(): number {
return this.balance;
}
}
// exemplo de uso
const account = new BankAccount(100);
console.log("Saldo atual:", account.checkBalance()); // "Saldo atual: 100"
account.withdraw(50); // "Saque realizado com sucesso!"
console.log("Saldo atual:", account.checkBalance()); // "Saldo atual: 50"Overuse of Inheritance
Ocorre quando a herança é utilizada de maneira inadequada, frequentemente criando uma hierarquia de classes complexa e difícil de manter. Em vez de aproveitar o polimorfismo e a reutilização de código de maneira eficiente, o uso excessivo de herança pode levar a dependências desnecessárias entre as classes, acoplamento forte e falta de flexibilidade.
O uso de herança é adequado quando há uma relação “é um” clara entre as classes, mas quando as classes se tornam muito especializadas ou a hierarquia de classes se torna muito profunda, o código pode se tornar inflexível, difícil de entender e difícil de modificar.
class Shape {
constructor(public color: string) {}
draw(): void { ... }
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}
draw(): void { ... }
}
class Square extends Shape {
constructor(color: string, public sideLength: number) {
super(color);
}
draw(): void { ... }
}
class Triangle extends Shape {
constructor(color: string, public base: number, public height: number) {
super(color);
}
draw(): void { ... }
}// solução
class Circle {
constructor(public color: string, public radius: number) {}
draw(): void { ... }
}
class Square {
constructor(public color: string, public sideLength: number) {}
draw(): void { ... }
}
class Triangle {
constructor(public color: string, public base: number, public height: number) {}
draw(): void { ... }
}Improper Synchronization
Ocorre quando múltiplas operações que alteram o estado compartilhado entre threads ou processos não são devidamente sincronizadas. Isso pode levar a condições de corrida (race conditions), onde o comportamento do programa depende da ordem em que as threads ou processos são executados, gerando resultados inesperados ou errôneos.
Em ambientes de execução paralela ou concorrente, é essencial garantir que apenas uma thread/processo acesse ou modifique o estado compartilhado de cada vez, para evitar conflitos.
class SharedList {
private list: number[] = [];
addToList(value: number) {
const currentList = this.list;
setTimeout(() => { // simulando uma operação demorada ( não utilize setTimeout nos seus projetos )
currentList.push(value); // tentando adicionar à lista
}, Math.random() * 100);
}
getList(): number[] {
return this.list;
}
}
// exemplo de uso
const sharedList = new SharedList();
// simulação de concorrência: duas funções tentando adicionar à lista simultaneamente
sharedList.addToList(1);
sharedList.addToList(2);
setTimeout(() => {
// esperado: [1, 2], mas pode ser incorreto
console.log(sharedList.getList());
}, 200);// solução
class SharedList {
private list: number[] = [];
private lock: Promise<void> = Promise.resolve();
addToList(value: number) {
// usando um lock para garantir que a operação de adição seja sequencial
this.lock = this.lock.then(() => {
return new Promise<void>((resolve) => {
setTimeout(() => {
this.list.push(value);
resolve();
}, Math.random() * 100);
});
});
}
getList() {
return this.list;
}
}
// exemplo de uso
const sharedList = new SharedList();
// simulação de concorrência: agora a adição à lista é sincronizada
sharedList.addToList(1);
sharedList.addToList(2);
setTimeout(() => {
console.log(sharedList.getList()); // agora o valor será sempre [1, 2], com sincronização correta
}, 200);Hardcoded Paths
ocorre quando caminhos (diretórios ou URLs) são codificados diretamente no código-fonte, tornando o sistema inflexível e propenso a erros. Isso acontece especialmente quando os caminhos para arquivos, diretórios ou recursos são fixados diretamente no código, em vez de serem parametrizados ou configurados externamente.
Os problemas de hardcoding incluem:
- Manutenção difícil: Se o caminho precisar mudar, o código precisa ser editado em vários lugares, o que pode ser suscetível a erros.
- Falta de flexibilidade: O sistema fica preso a um caminho específico e não pode ser facilmente movido ou configurado para diferentes ambientes (desenvolvimento, teste, produção).
- Portabilidade comprometida: O código pode depender de caminhos específicos do sistema operacional (ex: caminhos absolutos no Windows vs. Linux).
const fs = require('fs');
function readDataFromFile() {
const filePath = '/Users/username/Documents/data.txt'; // path hardcoded
const data = fs.readFileSync(filePath, 'utf-8');
console.log(data);
}
// exemplo de uso
readDataFromFile();// solução
const fs = require('fs');
function readDataFromFile() {
// variável de ambiente
const filePath = process.env.DATA_FILE_PATH || './data.txt';
const data = fs.readFileSync(filePath, 'utf-8');
console.log(data);
}
// exemplo de uso
readDataFromFile();Improper Dependency Management
ocorre quando um projeto tem dependências mal gerenciadas, o que pode incluir:
- Dependências desnecessárias: Quando o projeto inclui pacotes ou bibliotecas que não são utilizados, aumentando o tamanho do projeto e dificultando a manutenção.
- Dependências não declaradas: Quando as dependências de um projeto não estão claramente especificadas, o que pode causar problemas ao tentar configurar ou rodar o projeto em outro ambiente.
- Dependências desatualizadas: Quando o projeto depende de versões desatualizadas de bibliotecas que podem ter falhas de segurança ou bugs corrigidos em versões mais recentes.
- Dependências incorretas: Quando o projeto inclui bibliotecas que não são compatíveis ou que têm versões conflitantes, o que pode resultar em erros de execução.
Problemas principais:
- Aumento de complexidade: Dependências desnecessárias tornam o projeto mais pesado e difícil de gerenciar.
- Falhas de segurança: Bibliotecas desatualizadas podem conter vulnerabilidades.
- Erros de execução: Dependências incompatíveis ou mal gerenciadas podem causar falhas em tempo de execução.
- Dificuldade de manutenção: Ao não controlar corretamente as dependências, torna-se difícil saber qual versão de uma biblioteca deve ser usada ou como configurar o projeto corretamente..
// package.json com dependências desnecessárias e versões conflitantes
{
"name": "my-app",
"dependencies": {
"lodash": "^4.17.21",
"axios": "^0.21.1",
"moment": "^2.29.1", // Não sendo usado no código
"axios": "^0.18.1" // Versão conflitante de axios
"example_only": "^0.2.3", // não atualizado a 5 anos
}
}// solução
{
"name": "my-app",
"dependencies": {
"lodash": "^4.17.21",
"axios": "^0.21.1" // Somente uma versão de axios, e a mais recente
}
}Unused Imports
Ocorre quando módulos, bibliotecas ou funções são importados no código, mas nunca são utilizados. Isso pode acontecer durante o desenvolvimento, quando as dependências são adicionadas, mas depois não são usadas, ou quando o código é refatorado e as importações antigas não são removidas.
import { isEmpty } from 'lodash'; // importação não utilizada
import { format } from 'date-fns'; // importação não utilizada
import { add } from 'lodash'; // usado no código
function sum(a: number, b: number): number {
return add(a, b); // usando a função 'add'
}
// exemplo de uso
sum(5, 3);// solução
import { add } from 'lodash'; // mantemos apenas a importação usada
function sum(a: number, b: number): number {
return add(a, b);
}
// exemplo de uso
sum(5, 3);Memory Leaks
Ocorrem quando a memória é alocada, mas não é liberada corretamente. Esse vazamento de memória impede que o coletor de lixo (garbage collector) libere a memória que não está mais sendo usada, o que pode resultar em aumento no uso de memória e, eventualmente, falhas ou degradação do desempenho da aplicação.
class UserList {
users: string[];
constructor(users: string[]) {
this.users = users;
}
render() {
const listElement = document.createElement('ul');
this.users.forEach(user => {
const listItem = document.createElement('li');
listItem.textContent = user;
// adiciona um listener de clique para cada item
listItem.addEventListener('click', () => {
alert(`User clicked: ${user}`);
});
listElement.appendChild(listItem);
});
document.body.appendChild(listElement);
}
destroy() {
// o listener não é removido, o que causa o vazamento de memória
const listElement = document.querySelector('ul');
if (listElement) {
document.body.removeChild(listElement);
}
}
}
// exemplo de uso
const userList = new UserList(['Alice', 'Bob', 'Charlie']);
userList.render();
// após algum tempo, remove o componente, mas o listener não é removido
setTimeout(() => {
userList.destroy();
}, 5000);// solução
class UserList {
users: string[];
constructor(users: string[]) {
this.users = users;
}
render() {
const listElement = document.createElement('ul');
this.users.forEach(user => {
const listItem = document.createElement('li');
listItem.textContent = user;
// Adiciona o listener de clique
const handleClick = () => {
alert(`User clicked: ${user}`);
};
listItem.addEventListener('click', handleClick);
// Armazenar o listener para removê-lo mais tarde
listItem.dataset.listener = 'true';
listElement.appendChild(listItem);
});
document.body.appendChild(listElement);
}
destroy() {
const listElement = document.querySelector('ul');
if (!listElement) {
return;
}
listElement.querySelectorAll('li').forEach(item => {
// Remove o listener de clique
item.removeEventListener('click', () => {});
});
document.body.removeChild(listElement);
}
}
// exemplo de uso
const userList = new UserList(['Alice', 'Bob', 'Charlie']);
userList.render();
// Após algum tempo, remove o componente e o listener
setTimeout(() => {
userList.destroy();
}, 5000);Unbounded Loops
São laços (loops) que não têm uma condição de parada clara ou um limite bem definido, fazendo com que o loop continue a rodar indefinidamente, consumindo recursos do sistema, como CPU e memória. Esses loops podem levar a um comportamento inesperado, como a sobrecarga do sistema, travamentos ou até mesmo falhas.
infiniteLoop(): void {
let count = 0;
while (count < 10) {
console.log(count);
// acondição de parada nunca muda, o loop é infinito
}
}
infiniteLoop();// solução
function boundedLoop() {
let count = 0;
while (count < 10) {
console.log(count);
count++; // agora valor de count é incrementado, o loop terminará
}
}
boundedLoop();Poor Error Messages
São aquelas que fornecem informações insuficientes, vagas ou confusas sobre o que deu errado em uma aplicação. Isso dificulta a depuração e a solução de problemas, tanto para desenvolvedores quanto para usuários finais.
Problemas principais:
- Dificuldade para identificar a causa raiz do erro: Mensagens vagas ou incompletas não ajudam os desenvolvedores a entender rapidamente o problema.
- Frustração do usuário: Se um usuário encontra uma mensagem de erro incompreensível, isso pode gerar frustração e dificultar a resolução do problema.
- Aumento no tempo de resolução de problemas: Erros com mensagens imprecisas podem levar mais tempo para serem diagnosticados e corrigidos.
calculatePrice(price: number, tax: number): number {
if (price < 0 || tax < 0) {
throw new Error('Error');
}
return price + (price * tax);
}
// exemplo de uso
try {
const total = calculatePrice(-10, 0.15); // valor negativo, o que gera erro
} catch (error) {
console.log(error.message); // exibe apenas 'Error', sem detalhes úteis
}// solução
calculatePrice(price: number, tax: number): number {
if (price < 0) {
throw new Error(`Invalid price: ${price}. Price must be a non-negative number.`);
}
if (tax < 0) {
throw new Error(`Invalid tax: ${tax}. Tax must be a non-negative number.`);
}
return price + (price * tax);
}
// exemplo de uso
try {
const total = calculatePrice(-10, 0.15); // valor negativo, o que gera erro
} catch (error) {
console.log(error.message); // mensagem de erro mais clara e útil
}Improper Interface Usage
Ocorre quando uma interface é mal projetada ou usada de forma inadequada em um código. Interfaces são definidas para garantir uma estrutura consistente e facilitar a reutilização de código, mas quando usadas de maneira equivocada, podem tornar o código mais confuso, difícil de manter e menos flexível.
interface Vehicle {
speed: number; // velocidade do veículo
fuel: number; // combustível do veículo
drive: () => void; // método para dirigir o veículo
}
class Car implements Vehicle {
speed: number;
fuel: number;
constructor(speed: number, fuel: number) {
this.speed = speed;
this.fuel = fuel;
}
drive() {
console.log("Carro dirigindo");
}
}
class Bicycle implements Vehicle {
speed: number;
fuel: number; // a bicicleta não tem combustível, mas é forçada a ter
constructor(speed: number) {
this.speed = speed;
this.fuel = 0; // não faz sentido ter combustível para a bicicleta
}
drive() {
console.log("Bicicleta pedalando");
}
}// solução
// interface para veículos com combustível
interface FuelVehicle {
fuel: number;
drive: () => void;
}
// interface para qualquer veículo que tenha velocidade
interface SpeedyVehicle {
speed: number;
}
class Car implements FuelVehicle, SpeedyVehicle {
speed: number;
fuel: number;
constructor(speed: number, fuel: number) {
this.speed = speed;
this.fuel = fuel;
}
drive() {
console.log("Carro dirigindo");
}
}
class Bicycle implements SpeedyVehicle {
speed: number;
constructor(speed: number) {
this.speed = speed;
}
drive() {
console.log("Bicicleta pedalando");
}
}UI Logic in Backend
Lógica de interface no backend.
// backend com lógica de UI
function getProductDetails(productId: number) {
// lógica de negócios para obter o produto
const product = getProductFromDatabase(productId);
// lógica de UI - deveria estar no frontend
product.price = `$${product.price.toFixed(2)}`;
// definindo cor com base no estoque
product.color = product.stock > 0 ? "green" : "red";
return product;
}
// exemplo de uso
// backend retornando dados formatados para o frontend
const product = getProductDetails(123);
console.log(product.price); // '$25.00'
console.log(product.color); // 'green'// solução
// BACKEND: responsável pela lógica de negócios, dados puros sem formatação
getProductDetails(productId: number) {
const product = getProductFromDatabase(productId);
return product;
}
...
// FRONTEND: responsável pela formatação e exibição
formatProductForDisplay(product) {
product.price = `$${product.price.toFixed(2)}`;
product.color = product.stock > 0 ? "green" : "red";
return product;
}
// exemplo de uso
const product = getProductDetails(123);
const formattedProduct = formatProductForDisplay(product);
console.log(formattedProduct.price); // '$25.00'
console.log(formattedProduct.color); // 'green'Excessive Events
Ocorre quando há um número excessivo de eventos ou listeners registrados, frequentemente desnecessários, que tornam o código difícil de entender, manter e podem impactar o desempenho da aplicação. Isso pode acontecer quando eventos são registrados em excesso, ou em momentos desnecessários, ou ainda, quando a lógica por trás de um evento pode ser mais eficiente.
Eventos devem ser registrados de forma concisa e específica, e sua remoção deve ser feita corretamente para evitar vazamentos de memória. Ter muitos listeners ou um grande número de eventos para interações simples pode resultar em um código difícil de fazer debug e manter.
class ButtonComponent {
constructor() {
this.button = document.getElementById('myButton');
}
// registrando muitos eventos desnecessários
bindEvents() {
this.button.addEventListener('click', this.handleClick);
this.button.addEventListener('click', this.handleAnotherClick);
this.button.addEventListener('click', this.handleYetAnotherClick);
this.button.addEventListener('click', this.handleClick);
// adicionando múltiplos handlers para o mesmo evento
}
handleClick() { ... }
handleAnotherClick() { ... }
handleYetAnotherClick() { ... }
}// solução
class ButtonComponent {
constructor() {
this.button = document.getElementById('myButton');
}
// Registrando o evento de forma eficiente
bindEvents() {
this.button.addEventListener('click', this.handleClick);
}
handleClick() { ... }
}Missing Validation
Ocorre quando o código não realiza a validação adequada dos dados de entrada. A validação de dados é essencial para garantir que os dados recebidos em um sistema estejam no formato esperado e que não causem comportamentos inesperados, erros ou falhas de segurança. Falhar em validar entradas pode resultar em falhas de segurança, erros inesperados e problemas de integridade de dados.
Validar os dados de entrada pode incluir verificações de tipo, comprimento, formato, valores válidos e até mesmo a prevenção de ataques, como injeções de SQL ou XSS (Cross-Site Scripting).
Problemas principais:
- Erros no processamento: Dados inválidos podem levar a falhas, exceções ou comportamentos inesperados no sistema.
- Vulnerabilidades de segurança: A falta de validação pode permitir ataques como injeção de código, XSS, ou SQL Injection.
- Problemas de integridade de dados: Dados incorretos ou malformados podem comprometer a integridade da base de dados e dos sistemas que dependem dessas informações.
- Experiência do usuário comprometida: O sistema pode apresentar resultados inesperados ou falhas para o usuário final.
registerUser(user: { name: string; email: string }) {
const userDatabase = [];
// adicionando diretamente o usuário sem verificar o formato do email ou nome
userDatabase.push(user);
console.log('User registered successfully!');
}
// exemplo de uso
const user = new User('Alice', 'alice@example.com');
registerUser(user);// solução
isValidEmail(email: string): boolean {
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/;
return emailRegex.test(email);
}
isValidName(name: string): boolean {
return name.trim().length > 0;
}
registerUser(user: User) {
if (!isValidName(user.name)) { // validação do nome
console.warn('Invalid name.');
return;
}
if (!isValidEmail(user.email)) { // validação do email
console.warn('Invalid email format.');
return;
}
const userDatabase = [];
userDatabase.push(user); // só registra se os dados forem válidos
console.log('User registered successfully!');
}
// exemplo de uso
const user = new User('Alice', 'alice@example.com');
registerUser(user); // registro bem-sucedido
const invalidUser = new User('', 'invalid');
registerUser(invalidUser); // exibe 'Invalid name.' e 'Invalid email format.'Database Logic in Code
Ocorre quando a lógica de acesso e manipulação de dados, que deveria estar encapsulada no banco de dados (por exemplo, através de stored procedures, views ou triggers), é implementada diretamente no código da aplicação. Isso pode levar a um acoplamento forte entre o banco de dados e a aplicação, dificultando a manutenção, o teste, e a escalabilidade do sistema.
Esse code smell surge quando operações complexas de banco de dados, como consultas SQL extensas ou manipulação de dados, são diretamente inseridas no código da aplicação. Isso não só pode dificultar a leitura e manutenção do código, mas também pode gerar problemas de desempenho e flexibilidade.
// problema A
...
<body>
<h1>Lista de Usuários</h1>
<!-- Consulta SQL diretamente embutida no HTML -->
<script>
const query = "SELECT * FROM users WHERE role = 'admin'";
fetch('/execute-query', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
})
.then(response => response.json())
.then(data => {
data.forEach(user => {
document.body.innerHTML += `<p>${user.name}</p>`;
});
});
</script>
</body>
...// solução A
// arquivo .ts
import express from 'express';
import { Connection } from 'mysql';
const app = express();
app.use(express.json());
const connection: Connection = /* configuração da conexão com o DB */;
app.post('/users', (req, res) => {
connection.query("SELECT * FROM users WHERE ...", (error, results) => {
if (error) {
res.status(500).send('Erro no servidor');
return;
}
res.json(results);
});
});
app.listen(3000, () => {
console.log('Servidor rodando na porta 3000');
});
// arquivo .html
...
<h1>Lista de Usuários</h1>
<script>
fetch('/users')
.then(response => response.json())
.then(data => {
data.forEach(user => {
document.body.innerHTML += `<p>${user.name}</p>`;
});
});
</script>
...// problema B
// entidade de domínio User com lógica de banco de dados embutida
class User {
constructor(public id: number, public name: string) {}
// lógica de acesso a banco de dados dentro da entidade de domínio
static findById(id: number): User | null {
const query = `SELECT * FROM users WHERE id = ${id}`;
// simulando a execução de uma consulta SQL
const result = database.query(query);
if (!result) {
return null;
}
return new User(result.id, result.name);
}
}
// exemplo de uso
const user = User.findById(1);
console.log(user);// solução B
// DDD - Entidade de domínio User sem lógica de banco de dados
class User {
constructor(public id: number, public name: string) {}
}
// DDD - Repositório responsável por acessar o banco de dados
class UserRepository {
findById(id: number): User | null {
const query = `SELECT * FROM users WHERE id = ?`;
const result = database.query(query, [id]); // Usando prepared statements
if (result) {
return new User(result.id, result.name);
}
return null;
}
}
// DDD - Serviço de aplicação usando o repositório
class UserService {
constructor(private userRepository: UserRepository) {}
getUserById(id: number): User | null {
return this.userRepository.findById(id);
}
}
// Simulação de uso
const userRepository = new UserRepository();
const userService = new UserService(userRepository);
const user = userService.getUserById(1);
console.log(user);Incorrect Access Modifiers
Ocorre quando modificadores de acesso (como public, protected, private) são usados de forma inadequada, comprometendo o encapsulamento e a segurança do código. O uso incorreto pode expor dados sensíveis, dificultar a manutenção e levar a comportamentos inesperados em subclasses ou outras partes do código.
class User {
// problema: 'password' deve ser privado
constructor(public name: string, public password: string) {}
// problema: 'getPassword' expõe o password diretamente
getPassword(): string {
return this.password;
}
}
// exemplo de uso
const user = new User('Alice', 'secret123');
console.log(user.password); // está acessível diretamente// solução
class User {
// 'password' é agora privado
constructor(public name: string, private password: string) {}
// método público que não retorna o password com um mecanismo seguro
checkPassword(inputPassword: string): boolean {
return this.password === inputPassword; // simples de verificação
}
}
// exemplo de uso
const user = new User('Alice', 'secret123');
user.checkPassword('secret123'); // trueImproper Caching
ocorre quando o processo de cache é implementado de forma inadequada. Caching é uma técnica importante para melhorar o desempenho de sistemas, mas quando feito de maneira errada, pode resultar em problemas como dados desatualizados, uso excessivo de memória, inconsistências de estado ou até degradação do desempenho.
Alguns exemplos comuns de Improper Caching incluem:
- Cache sem expiração: O cache é armazenado indefinidamente sem uma estratégia de expiração, resultando em dados desatualizados.
- Cache excessivo: Caches sendo criados para dados que não precisam ser armazenados em cache, levando ao uso excessivo de memória.
- Cache ineficiente: Usar o cache para dados que não trazem um ganho real de desempenho.
- Cache inconsistente: Dados no cache que não são sincronizados com o banco de dados ou outras fontes de dados, resultando em informações desatualizadas ou inconsistentes.
class UserService {
private cache: { [key: string]: string } = {};
// método de consulta com cache inadequado
getUserName(userId: string): string {
if (this.cache[userId]) {
return this.cache[userId]; // se no cache, retorna imediatamente
}
const userName = this.getUserNameFromDB(userId); // consulta DB
this.cache[userId] = userName; // armazena em cache
return userName;
}
private getUserNameFromDB(userId: string): string { // simulando DB
...
}
}
// exemplo de uso
const userService = new UserService();
console.log(userService.getUserName('1')); // chama o banco de dados
console.log(userService.getUserName('1')); // retorna do cache// solução
class UserService {
private cache: { [key: string]: { value: string, timestamp: number } } = {};
private cacheExpirationTime: number = 60000; // 1 minuto
// método de consulta com cache adequado
getUserName(userId: string): string {
const currentTime = Date.now();
// retorna do cache, se dentro do tempo de expiração
if (this.cache[userId] && currentTime - this.cache[userId].timestamp < this.cacheExpirationTime) {
return this.cache[userId].value;
}
const userName = this.getUserNameFromDB(userId); // consulta DB
this.cache[userId] = { value: userName, timestamp: currentTime };
return userName;
}
private getUserNameFromDB(userId: string): string { // simulando DB
return `User_${userId}`;
}
}
// exemplo de uso
const userService = new UserService();
console.log(userService.getUserName('1')); // chama o banco de dados
// retorna do cache (dentro de 30s)
setTimeout(() => console.log(userService.getUserName('1')), 30000);
// chama o banco de dados (expirou o cache)
setTimeout(() => console.log(userService.getUserName('1')), 70000);Unnecessary Threading
Ocorre quando o uso de threads ou concorrência (multi-threading) é implementado sem necessidade. Isso acontece quando o problema que está sendo resolvido não requer múltiplas threads, mas mesmo assim, threads são utilizadas, o que pode levar a um aumento de complexidade, consumo desnecessário de recursos e até pior desempenho.
O uso de threading é uma técnica importante quando precisamos realizar tarefas que podem ser paralelizadas para melhorar a performance, mas quando usado sem justificativa, pode resultar em:
- Maior complexidade: O gerenciamento de threads aumenta a complexidade do código e pode introduzir erros como condições de corrida, deadlocks e outros problemas difíceis de depurar.
- Desempenho piorado: Threads adicionais podem adicionar overhead, consumindo mais memória e recursos do sistema sem proporcionar benefícios significativos.
- Gestão inadequada de recursos: O gerenciamento de threads pode causar problemas de recursos, como vazamento de memória ou falhas de sincronização.
class DataProcessor {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
// usando uma Promise para simular threading sem necessidade
processData(): void {
new Promise((resolve, reject) => {
const processedData = this.data.map(num => num * 2);
resolve(processedData);
}).then(processedData => {
console.log(processedData);
});
}
}
// exemplo de uso
const processor = new DataProcessor([1, 2, 3, 4, 5]);
processor.processData(); // o código cria uma promessa desnecessária// solução
class DataProcessor {
private data: number[];
constructor(data: number[]) {
this.data = data;
}
processData(): void {
const processedData = this.data.map(num => num * 2);
console.log(processedData);
}
}
// exemplo de uso
const processor = new DataProcessor([1, 2, 3, 4, 5]);
processor.processData();Blocking Calls
Ocorre quando uma chamada ao sistema (geralmente I/O, como uma requisição de rede, leitura de arquivo ou consulta a banco de dados) bloqueia o fluxo de execução do programa. Isso pode resultar em um desempenho ruim, já que o processo fica “parado” esperando a resposta de uma operação, em vez de continuar com outras tarefas enquanto espera.
Esse problema é comum quando o código faz chamadas síncronas a operações que podem levar um tempo considerável, como ler arquivos grandes ou aguardar uma resposta de rede. Isso torna a aplicação mais lenta e menos responsiva, principalmente em sistemas que lidam com muitos usuários ou operações simultâneas.
class DataService {
private fetchDataFromAPI(): Promise<string> {
return new Promise((resolve) =>
// simula requisição de API que leva 2 segundos
setTimeout(() => resolve('Data from API'), 2000)
);
}
// usa await de forma ineficiente, o que leva a bloqueio desnecessário
async processData(): Promise<Data> {
const data = await this.fetchDataFromAPI(); // aguarda a resposta
... // realiza outras ações, mas precisa esperar o retorno do data
return data;
}
}
// exemplo de uso
const service = new DataService();
// execução fica bloqueada até a resposta da API
const data = await service.processData();
console.log('After processData');// solução
class DataService {
private fetchDataFromAPI(): Promise<string> {
return new Promise((resolve) =>
setTimeout(() => resolve('Data from API'), 2000);
);
}
async processData(): Promise<void> {
// usando Promise.all para processar várias promessas ao mesmo tempo (não de forma sequencial)
const dataPromise = this.fetchDataFromAPI(); // inicia a requisição sem bloquear
... // realiza outras ações enquando o "dataPromise" é executado
const data = await dataPromise;
return data;
}
}
const service = new DataService();
service.processData(); // A execução não fica bloqueada, permite outras tarefas
console.log('After processData'); // Essa linha agora é executada antes do término da requisiçãoImproper API Design
Ocorre quando a interface da API (ou a forma como ela é projetada para ser utilizada) não é intuitiva, eficiente ou bem estruturada. Uma API mal projetada pode ser difícil de entender, usar ou manter, resultando em uma experiência ruim para os desenvolvedores que a consomem. Alguns exemplos incluem:
- APIs que expõem métodos ou endpoints que não fazem sentido ou são difíceis de usar.
- Falta de consistência na nomeação de métodos e parâmetros.
- Exposição excessiva de detalhes internos.
- APIs que não seguem os padrões e melhores práticas, o que dificulta a integração e a manutenção.
class Product {
id: number;
productName: string;
price: number;
quantityInStock: number; // exposição de detalhes internos
isDiscounted: boolean;
discountPercentage: number; // exposição de detalhes internos
...
}
class Product {
// falta de consistência na nomeação, todos os retornos são um tipo de dados
async getProductData(productId: number): Promise<Product>;
// nomeação repetitiva, parametro sem sentido, retorno sem tipagem
async getAllProducts(time: any): Promise<any>;
async create(
name: string,
price: number,
quantity: number,
isOnSale: boolean,
discount: number
): Promise<Product> {
// parâmetros excessivos e sem significado claro (isOnSale, discount)
// lógica de negócio misturada com a API (cálculo de desconto)
}
async updatePrice(productId: number, newPrice: number): Promise<void> {
// falta de tratamento de erros (e.g., produto não encontrado)
}
}// solução
class ProductResponse {
constructor(public id: number, public name: string, public price: number){}
}
class ProductCreateRequest {
constructor(public name: string, public price: number, public discount = 0){}
}
class Product {
async getById(productId: number): Promise<ProductResponse | null> { ... }
async getAllByDate(date: Date): Promise<ProductResponse[]> { ... }
async create(request: ProductCreateRequest): Promise<ProductResponse> { ... }
async updatePrice(productId: number, newPrice: number): Promise<boolean> { ... }
}SOLID Principles
Muitas falhas em projetos surgem da violação dos princípios SOLID, um conjunto de boas práticas para desenvolvimento orientado a objetos.
Este tema é abordado com mais detalhes em outro artigo: medium.com/@gilvam/solid-6ba55001fe7f.
Referências:










