Esse assunto é bem complexo, e envolve certas situações interessantes. Mas antes, precisamos de uma definição do que é um efeito colateral em código: quando se escreve um código em uma linguagem, cada procedimento/função/método deveria fazer uma, e apenas uma tarefa. Mais do que isso, e estamos inserindo um efeito colateral no código. Quando programamos em linguagens puramente funcionais, como por exemplo Haskell, fica evidente que estamos mexendo com uma função com efeitos colaterais porque ela fica, de certa forma, “marcada”.
Ok, e em Ruby, por exemplo? Como saber? A idéia é simples (e talvez isso possa até ser uma definição formal): “Uma função não tem efeitos colaterais quando não importa quantas vezes ela é chamada, ela sempre traz os mesmos resultados”. Embora simples, na teoria, identificar uma função assim nem sempre é fácil. Por exemplo, imaginemos um caso bem interessante: a função “save” do ActiveRecord. Ela parece, em teoria, não ter efeitos colaterais, mas ela possui um: vamos para um código na prática:
class Person < ActiveRecord::Base validates_uniqueness_of :name end me = Person.new :name => "Maurício" myself = Person.new :name => "Maurício" me.save #true. me.save #false. FALSE?
Tecnicamente falando, as duas funções deveriam retornar true: eu passei dois objetos exatamente iguais para elas. O segredo, se é que há algum, é que o método “save” na verdade faz duas coisas: valida os objetos e salva-os no banco. O principal problema é o “salvar no banco”, na verdade: estamos definindo um estado “global”, digamos assim. Todos os nossos novos objetos “Person” vão, automaticamente, ter que consultar o banco (nosso “estado global”) e verificar se a propriedade “name” já existe. Como se, por causa desse “estado” todos os saves automaticamente ganham um “if” a mais. Outra coisa também que o “save” faz que indica um efeito colateral é mudar o “id” do objeto:
me = Person.new :name => "Maurício" p me.id #retorna nil me.save p me.id #retorna um número qualquer
Obviamente, esse número mudará de acordo com o que temos no banco de dados. Com isso, mais uma vez, não garantimos que o método retorna SEMPRE o mesmo valor (ou, nesse caso, modifica o objeto do mesmo jeito). Por fim, o método faz duas coisas: retorna E modifica o estado do objeto, trazendo mais um efeito colateral.
Bom, com isso também é possível saber mais uma coisa: não é possível escrever um código completamente isento de efeitos colaterais (na verdade, até é possível, mas provavelmente ninguém vai querer usar um programa no qual você roda-o e ele imprime na tela um resultado baseado em valores hard-coded no código…). Então, a idéia é evitar ao máximo efeitos colaterais em seu código (ao contrário do que você aprendeu a programar na faculdade, se sua faculdade for igual à minha…)
Enfim, a idéia principal é fazer os métodos fazerem poucas coisas. Se você precisa criar um método que vai identificar se aquele registro é válido, certifique-se de que ele APENAS faz isso. Logo, se você chamar “valid?” quatro ou cinco vezes, garanta que o resultado é sempre o mesmo. E principalmente, se você criar cinquenta objetos iguais e chamar “valid?” para todos eles, todos dão o mesmo resultado.
Outra dica interessante é que para uma função não ter efeitos colaterais, ela não pode envolver-se com E/S (IO) (ou seja, não pode imprimir na tela, salvar num arquivo, ler um caractere, etc), nem gerar ou consumir número randômicos, acessar a data e hora atual, etc… isso porque qualquer E/S modifica o estado do sistema, gerar número randomicos é, por definição, algo que gera um resultado diferente sempre que é chamado, e acessar a data e hora é basicamente ter um resultado diferente a cada milisegundo (por isso que fazemos “stub” desse tipo de operação em testes).
Por fim, sem efeitos colaterais, é mais fácil de testar o código. E embora seja impossível fazer um sistema completo sem nenhum efeito colateral, nos testes isso é imprescindível (teardown, ou o “after” do rspec, são funções que obrigatoriamente devem limpar qualquer “sujeira” que seu teste tenha feito: criar arquivos, criar registros no banco de dados, etc).
Resumindo o post: qualquer efeito colateral em código define um novo estado. Cada estado definido tem suas regras, consequentemente, têm-se uma série de novas condições no código. No caso de E/S, esse estado é global (todas as classes Person tem um novo comportamento no “valid?” por exemplo), no caso de alteração no estado do objeto, os métodos daquele objeto são afetados (e quantos são os métodos afetados, é difícil de saber) e se algum outro objeto depende do primeiro, esse novo objeto também é afetado. Lembram do “baixo acoplamento”? Esse é o motivo pra ele ser desejado.