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:
User#as_new_user
);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.
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:
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