Side-Effect-Free Functions
Descrição
Side-Effect-Free Functions propõe separar as operações em duas categorias: comandos, que modificam o estado, e consultas, que apenas devolvem um resultado. Uma função é livre de efeitos colaterais quando calcula e retorna um valor sem alterar nada observável. Os comandos devem ser simples e não devolver dados de domínio.
A lógica complexa é colocada em funções puras, de preferência em Value Objects, que são imutáveis. Assim, uma operação que combina valores devolve um novo objeto em vez de modificar o existente. Quando o cálculo está em uma Entity, pode-se extrair parte dele para um Value Object e deixar a Entity apenas aplicando o resultado.
O efeito é uma redução forte da carga mental: o desenvolvedor pode chamar e compor funções sem medo de alterar o estado de outro objeto, e os testes ficam simples.
Problemas que resolve
- Operações que misturam cálculo e mudança de estado, difíceis de prever e de testar.
- Efeitos colaterais em cascata, em que chamar um método altera objetos que o chamador não imaginava.
- Dificuldade de reaproveitar um cálculo sem disparar sua mudança de estado.
- Bugs causados por ordem de chamada e por estado compartilhado mutável.
Quando usar
- Em cálculos complexos do domínio (preços, cores, prazos, taxas).
- Ao modelar conceitos como Value Objects, que devem ser imutáveis.
- Quando uma consulta é chamada em muitos lugares e precisa ser segura de compor.
Quando evitar
- Em objetos grandes cuja cópia a cada operação é cara e comprovadamente um gargalo.
- Quando o domínio é por natureza uma sequência de mudanças de estado (por exemplo, um processo longo): ali o comando é legítimo e deve ser explícito, não escondido.
Trade-offs
| Ganho | Custo / Risco |
|---|---|
| Testes simples, sem preparação de estado | Cria novos objetos a cada operação |
| Composição segura e raciocínio local | Exige disciplina para separar comandos de consultas |
| Cache e paralelismo facilitados | Pode ser pouco natural em bibliotecas mutáveis |
- Consistência: menos pontos onde o estado pode ser corrompido.
- Desempenho: a imutabilidade pode custar alocações; meça antes de abrir mão dela.
- Comandos: mantenha-os pequenos e com efeito claramente nomeado.
Diagrama
Paint.mixIn é um comando que muda o estado, enquanto PigmentColor.mixedWith é uma função pura que calcula a nova cor sem alterar nenhum objeto.
Exemplo
Em um sistema de tintas, a regra de mistura de pigmentos é complexa. Ela fica em PigmentColor, um Value Object imutável; o comando em Paint apenas aplica o resultado.
public final class PigmentColor {
private final double r, g, b;
public PigmentColor(double r, double g, double b) {
this.r = r; this.g = g; this.b = b;
}
// Função pura: devolve nova cor, nada é modificado
public PigmentColor mixedWith(PigmentColor other, int w1, int w2) {
int total = w1 + w2;
return new PigmentColor(
(r * w1 + other.r * w2) / total,
(g * w1 + other.g * w2) / total,
(b * w1 + other.b * w2) / total);
}
}
public class Paint {
private PigmentColor color;
private int volume;
// Comando: simples, só aplica o resultado
public void mixIn(Paint other) {
color = color.mixedWith(other.color, volume, other.volume);
volume += other.volume;
}
}
Padrões relacionados
- Value Object — lugar natural para funções puras, por serem imutáveis.
- Entity — mantém os comandos, delegando o cálculo a valores.
- Closure of Operations — operações que devolvem o mesmo tipo combinam bem com funções puras.
- Assertions — documentam os efeitos dos comandos que sobram.
- Intention-Revealing Interfaces — o nome deve deixar claro se é comando ou consulta.
- Money (PoEAA) — valor imutável com operações puras.
Referência
- DDD Reference, Eric Evans (2015)
- Domain-Driven Design: Tackling Complexity in the Heart of Software, Eric Evans (Addison-Wesley, 2003)
- Command Query Separation (bliki do Fowler)