HE:labs

Postado por Pedro Nascimento em 13/03/2013

Métodos estáticos, porque evitá-los

Aparentemente existe na cultura de alguns desenvolvedores uma certa convenção sobre quando usar métodos estáticos. Se é um método relativamente simples, que itera sobre uma coleção dos objetos da classe em si, implementar um método estático é uma forma possível de implementação.

Diga-se que uma API está sendo feita, e é preciso retornar os usuários novos em um JSON bem específico às características da aplicação. Segue um exemplo, extraído de um projeto real:

 1 class User < ActiveRecord::Base
 2   # ...
 3   def self.last_users_since(time)
 4     response_data = {new_users: []}
 5     where(updated_at: time..Time.now).each do |user|
 6       user_hash = user.as_new_user
 7       response[:new_users] << (user_hash) if user_hash.present?
 8     end
 9     response_data
10   end
11   # ...
12 end

Da forma como se encontra, temos alguns problemas:

  • É um método relativamente complexo;
  • Não é um método fácil de ler;
  • Existe um método no objeto user que só serve pra essa API (User#as_new_user);
  • É chato de testar, pois só temos o retorno para ser testado.

Mas até então, ainda não é um problema.

Só que software é uma coisa que muda constantemente, e o cliente resolveu alterar a API. Agora existe um campo booleano em user chamado synced que dita quais objetos vão ser retornados para a API e, em seguida, considerar os mesmos como sincronizados. Ou seja, uma nova chamada à API não vai mais retornar os mesmos objetos, e sim somente os não sincronizados. Existe também um novo campo no JSON que indica quando a chamada foi resolvida.

Altera-se o método para a nova necessidade:

 1 class User < ActiveRecord::Base
 2   # ...
 3   def self.sync_unsynchronized_users
 4     response_data = {new_users: [], synced_at: Time.now}
 5     where(synced: false).each do |user|
 6       user_hash = user.as_new_user
 7       response[:new_users] << (user_hash) if user_hash.present?
 8       user.sync!
 9     end
10     response_data
11   end
12   # ...
13 end

Todos os problemas ainda existem e dificilmente serão resolvidos se mantermos o método estático.

E o pior de tudo: Mesmo com a introdução acima, a implementação é um pouco confusa. Imagine daqui há 2 meses quando algum desenvolvedor (ou até mesmo o próprio que escreveu) tiver que pegar esse código pra entender. Ainda que seja perfeitamente possível que se compreenda como o mesmo funciona, um método estático não declara intenção, os testes normalmente não são tão claros quanto se gostaria, e introduzir qualquer nova funcionalidade traz uma certa insegurança.

Extraindo um método estático para uma nova classe

Extrair um método complexo para uma classe é um dos refactors mais clássicos e, no caso dos métodos estáticos, ataca-se os principais problemas:

  • Facilita a leitura;
  • Declara-se intenções através de nomes de métodos;
  • Melhora a testabilidade.

O método acima extraído para uma classe ficaria assim:

 1 class User < ActiveRecord::Base
 2   # ...
 3   def self.sync_unsynchronized_users
 4     UsersSyncer.new.sync!
 5   end
 6   # ...
 7 end
 8 
 9 class UsersSyncer
10   attr_reader :recently_synchronized_users
11   def initialize
12     @recently_synchronized_users = []
13   end
14 
15   def unsyncronized_users
16     User.where(synced: false)
17   end
18 
19   def sync!
20     add_and_sync_users
21     response_hash
22   end
23 
24   private
25     def response_hash
26       {new_users: recently_synchronized_users, synced_at: Time.now}
27     end
28 
29     def add_and_sync_users
30       unsyncronized_users.each do |user|
31         mark_as_sync(user)
32         add_user_to_list_if_present(user)
33       end
34     end
35 
36     def mark_as_sync(user)
37       user.sync!
38     end
39 
40     def add_user_to_list_if_present(user)
41       user_hash = format_user_for_api(user)
42       @recently_synchronized_users << if user_hash.present?
43     end
44 
45     def format_user_for_api(user)
46       # método extraído de User
47     end
48 end

Nota-se que o método estático ainda existe, mas somente como uma interface. Esse é um dos poucos casos em que se é aceitável a criação de métodos estáticos, já que é bem prático chamar User.sync_unsynchronized_users.

O método as_new_user foi extraído de User porque neste caso somente era usado somente uma vez. Poderia ficar em User, mas acredito ficar mais claro desta forma.

Ler a classe acima 2 meses depois requer muito menos esforço por parte do desenvolvedor para compreender o funcionamento da mesma, e com certeza os testes estarão mais claros, além da complexidade ter diminuído consideravelmente.

Comentários
Included file post/disqus_thread.html not found in _includes directory