Metaprogramação em Ruby - Criando métodos de Classe

Nem falo nada sobre quanto tempo sem escrever nada, mas vou contar uma novidade rapidinha: Agora Gaveteiro virou NEI. Mas vamos ao que interessa nesse post.

Métodos de Classe? Bom, sabemos que isso não existe em Ruby. Na verdade são métodos singleton da classe, mas enfim, vamos abistrair essa parte.

Bom, então, digamos que você vai mapear um recurso de uma API e ela tem um campo/atributo type, e essa pode retornar os valores ClientA.administrador, ClientA.manager e ClientA.buyer como valores para este atributo. Mas você quer ter um código mais limpo, algo que você possa perguntar ao seu objeto: resource.admin? em vez de ficar espalhado pelo seu código algo como resource.type == "ClientA.administrador" e etc. E o mesmo para criar uma instância desse objeto, teria que fazer algo como Resource.new(type: 'ClientA.administrador'). Não seria melhor ter algo como Resource.new_admin(...)?. Nesse caso, vou apresentar dois métodos para criar dinamicamente métodos. o define_method serve para criar os métodos de instância e o define_singleton_method para criar os “métodos de classe”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Resource < OpenStruct
  include ActiveModel::Validations

  TYPES = {
    "admin"   => "ClientA.administrator",
    "manager" => "ClientA.manager",
    "buyer"   => "ClientA.buyer"
  }.freeze

  validates :api_id, presence: true
  validates :type, inclusion: TYPES.values

  TYPES.each do |key, value|
    # define "question" methods for each type (e.g.: def admin?; type == "ClientA.administrador"; end)
    define_method "#{key}?" do
      type == value
    end

    # define constructors for each type (e.g.: Resource.new_admin({...}))
    self.define_singleton_method("new_#{key}".to_sym) do |**args|
      new(args.merge(type: value))
    end
  end
end

Então agora, é só usar

resource = Resource.new_admin
resource.type   # => it returns "ClientA.administrator"
resource.admin? # => it returns true
resource.buyer? # => it returns false

resource = Resource.new_buyer(name: "Tino")
resource.type   # => it returns "ClientA.buyer"
resource.admin? # => it returns false
resource.buyer? # => it returns true
resource.name   # => it returns "Tino" - YOU DON'T SAY?

O que achou?

PS1: Claro que ainda estamos limitados a conhecer todos os valores que virão no atributo type. Se quisermos que seja 100% dinâmico… (Será que rola outro post?)

PS2: Deu para perceber que desde o último post, estou fazendo integrações com APIs.

Referências: