Pular para o conteúdo

Funções (e métodos, no contexto de orientação a objetos) são a unidade mais frequente de leitura, teste e manutenção em um sistema. Para uma pessoa que está começando no ambiente Java, “escrever uma função” costuma significar “fazer o código funcionar”. Neste tópico, o objetivo é dar o próximo passo: escrever funções que continuam fáceis de entender e mudar daqui a semanas (ou semestres).

Uma boa regra mental é imaginar que seu código será lido por alguém que não estava com você quando você o escreveu (frequentemente: você mesmo no futuro).

Funções e métodos (e por que a diferença importa)

Seção intitulada “Funções e métodos (e por que a diferença importa)”
  • Função: pode existir “solta” em algumas linguagens.
  • Método: é uma função associada a um objeto/classe e recebe implicitamente um this.

Em Java, na prática você escreverá métodos, mas os princípios aqui valem para ambos.

  • Parâmetros: variáveis declaradas na assinatura do método.
  • Argumentos: valores passados na chamada.
public void saudacao(String nome) { // "nome" é parâmetro
System.out.println("Olá, " + nome);
}
saudacao("Fulano"); // "Fulano" é argumento

Analogia: parâmetro é o “molde”; argumento é o “objeto” que você coloca dentro do molde.

Responsabilidade Única (SRP) aplicada a funções

Seção intitulada “Responsabilidade Única (SRP) aplicada a funções”

O SRP (Single Responsibility Principle) diz que uma unidade de código deve ter um motivo para mudar. Em função/método, isso vira: um método deve fazer uma coisa só.

Sinais de que um método faz “mais de uma coisa”:

  • valida dados, calcula algo e também persiste no banco;
  • faz lógica de regra de negócio e ao mesmo tempo formata texto para UI;
  • mistura cálculos com IO (disco, rede, console).

Ruim (muitas etapas e motivos de mudança no mesmo método):

public void processCustomerData(Customer customer) {
if (customer.getName() == null || customer.getName().isEmpty()) {
System.out.println("Error: Customer name is missing.");
return;
}
if (customer.getEmail() == null || customer.getEmail().isEmpty()) {
System.out.println("Error: Customer email is missing.");
return;
}
boolean returning = checkIfReturningCustomer(customer);
if (returning) {
System.out.println("Returning customer found: " + customer.getName());
} else {
System.out.println("New customer: " + customer.getName());
}
String address = customer.getAddress();
if (address == null || address.isEmpty()) {
System.out.println("Error: Customer address is missing.");
return;
}
System.out.println("Customer address validated: " + address);
}

Melhor (cada método com uma intenção clara):

public void processCustomerData(Customer customer) {
validateCustomerData(customer);
checkCustomerStatus(customer);
processCustomerAddress(customer);
}

Isso melhora:

  • legibilidade (você “lê a história” do método);
  • testabilidade (cada parte isolada);
  • manutenção (mudança em validação não quebra status/endereço).

Quando alguém abre a classe, ela deve conseguir ler de cima para baixo como um texto:

  1. entradas principais (métodos públicos)
  2. detalhes (métodos privados auxiliares)

Analogia: comece pelo “sumário” e só depois vá aos “capítulos”.

Exemplo preferível:

public class GerenciadorDePedidos {
public void processarPedido(String pedido) {
if (validarPedido(pedido)) {
double valor = calcularValorTotal(pedido);
gerarNotaFiscal(pedido, valor);
enviarConfirmacao(pedido);
} else {
System.out.println("Pedido inválido.");
}
}
private boolean validarPedido(String pedido) { /* ... */ return true; }
private double calcularValorTotal(String pedido) { /* ... */ return 0.0; }
private void gerarNotaFiscal(String pedido, double valor) { /* ... */ }
private void enviarConfirmacao(String pedido) { /* ... */ }
}

Switches e decisões: quando evitar e o que fazer no lugar

Seção intitulada “Switches e decisões: quando evitar e o que fazer no lugar”

switch (ou cadeias de if/else) costumam piorar quando:

  • novas opções aparecem com frequência;
  • o comportamento muda por “tipo” (ex.: EmployeeType);
  • o código replica o mesmo switch em vários lugares.

Nesses casos, prefira polimorfismo (OO): cada tipo sabe como se comportar.

Em vez de:

public Money calculatePay(Employee e) {
switch (e.type) {
case COMMISSIONED: return commissionedPay(e);
case HOURLY: return hourlyPay(e);
case SALARIED: return salariedPay(e);
default: throw new InvalidEmployeeType(e.type);
}
}

Prefira:

public abstract class Employee {
public abstract Money calculatePay();
}

E cada subclasse implementa calculatePay().

Benefícios:

  • reduz condicionais;
  • facilita extensão (novo tipo = nova classe);
  • favorece testes unitários por tipo.

Nome de método é um “contrato”. Boas práticas:

  • use verbos: calcularTotal, enviarEmail, removerUsuario;
  • evite doStuff, handle, process quando escondem intenção;
  • se um nome fica enorme, pode ser sinal de que o método faz demais.

Analogia: nome de método é como etiqueta de pasta. Se não diz o conteúdo, você abre (lê) à força.

O livro Clean Code recomenda preferir:

  • 0 parâmetros (quando possível),
  • depois 1,
  • depois 2,
  • e evitar 3+.

Muitos parâmetros aumentam:

  • combinações de uso;
  • chance de inversão/erro;
  • custo de leitura.

Ruim:

public void sendEmail(String to, String from, String subject, String body, boolean isImportant) { }

Melhor:

public void sendEmail(Email email) { }

Isso também ajuda validação (ex.: Email.validate()).

Um booleano frequentemente significa “dois comportamentos”.

Ruim:

public void render(boolean isSilent) { /* ... */ }

Melhor:

public void render() { /* ... */ }
public void renderInSilentMode() { /* ... */ }

Isso torna quem chama responsável pela decisão e deixa o método mais previsível.

Efeito colateral é quando o método:

  • altera estado fora do que parece (variáveis globais/singletons),
  • escreve em arquivo/banco/rede,
  • muda algo “por baixo dos panos” além do retorno.

Exemplo problemático:

public void updatePhysics() {
// ...atualiza posição...
RenderServer.update(this); // efeito colateral escondido
}

Melhor separar intenções:

public void updatePhysics() { /* ... */ }
public void updateRender() { /* ... */ }

Regra prática: se o método é “de cálculo”, tente mantê-lo puro (sem IO e sem mexer em estado externo).

Em Java, é comum ver “retornos via array/lista passada por parâmetro”. Isso torna o método menos intuitivo:

public void calculate(int a, int b, int[] result) {
result[0] = a + b;
result[1] = a - b;
result[2] = a * b;
}

Alternativas melhores:

  • retornar um objeto (ex.: CalculationResult { sum, diff, product })
  • retornar uma lista imutável
  • ou separar em métodos

Separação comando–consulta (Command–Query Separation)

Seção intitulada “Separação comando–consulta (Command–Query Separation)”

Um método deve ser:

  • Comando: muda estado (retorno geralmente void);
  • Consulta: retorna dado (não muda estado).

Evite misturar:

public Boolean setUsername(String username) {
this.username = username;
return this.username; // mistura comando e consulta
}

Prefira:

public void setUsername(String username) {
this.username = username;
}
public String getUsername() {
return this.username;
}

Isso reduz confusão e torna o código mais previsível.

Retornar “códigos de erro” cria ambiguidade e exige checagens em todo lugar.

Evite:

public int divide(int a, int b) {
if (b == 0) return -1;
return a / b;
}

Prefira exceção (falha explícita):

public int divide(int a, int b) {
if (b == 0) throw new IllegalArgumentException("Divisor cannot be zero");
return a / b;
}

Quando usar exceção?

  • situações excepcionais/invalidas;
  • violações de pré-condição;
  • falhas de IO.

Quando não usar?

  • fluxo normal (ex.: “não encontrou resultado” pode ser Optional em alguns casos).

Blocos try/catch “achatam” a leitura. Uma técnica é separar:

  • método que faz (e propaga exceção),
  • método que decide como tratar.

Exemplo:

public String readFirstLine(Path path) throws IOException {
try (BufferedReader br = Files.newBufferedReader(path)) {
return br.readLine();
}
}
public String getFileContentOrMessage(Path path) {
try {
return readFirstLine(path);
} catch (NoSuchFileException e) {
return "Arquivo não encontrado";
} catch (IOException e) {
return "Erro ao ler o arquivo";
}
}

Notas:

  • try-with-resources evita vazamento de recursos.
  • o método “core” é curto e claro.

Duplicação é cara porque:

  • você corrige um bug em um lugar e esquece do outro;
  • diferenças pequenas viram divergências grandes.

Estratégias:

  • extrair método comum;
  • criar uma função de mapeamento/validação reutilizável;
  • usar estruturas de dados e polimorfismo ao invés de copiar condicionais.

Cuidado: abstração cedo demais também cria complexidade. Extraia quando o padrão estiver claro.

  • O nome diz exatamente o que ele faz?
  • Dá para explicar a função sem usar “e” no meio? (“faz X e Y” é suspeito)
  • Quantos parâmetros tem? Dá para agrupar?
  • Tem efeito colateral escondido?
  • O fluxo de leitura é top-down?
  • Erros são tratados de forma consistente (exceção, Optional, validação)?
  • Robert C. Martin, Clean Code (capítulo sobre Functions)
  • Documentação Java: exceções, try-with-resources
  • Princípios SOLID (SRP)