Posts recentes


Comentários


Arquivos


Categorias


RSS RSS 2



Ruby: Metaprogramming, Open Classes e o Object Model (Parte I)

Esse é o início de uma série de posts voltados a Ruby, aos detalhes da linguagem e à utilização desse conhecimento, aliado ao conceito de metaprogramming, para elucidar técnicas de como escrever código Ruby mais eficiente e entender as vantagens e desvantagens de algumas técnicas. Essa série de artigos é fortemente baseada no livro Metaprogramming Ruby 2, que pode ser encontrado aqui.

Mas chega de enrolação, vamos ao assunto de hoje, metaprogramming, open classes e um pedaço do object model 🙂

O que é metaprogramming?

Metaprogramming é um conjunto de técnicas que auxiliam os desenvolvedores a escreverem código compacto, legível e eficiente, reduzindo problemas comuns de boilerplate (reutilização de um mesmo código sem grandes alterações em sua estrutura básica). É uma forma de prover dinamicidade ao código através de código que se auto escreve. Mais formalmente, em computação, metaprogramming é a capacidade de um programa tratar a outros programas (e as vezes até a si mesmo) como dado, alterando, em runtime (tempo de execução), esse código, permitindo alterar os parâmetros e a forma de execução de uma tarefa, bem como injetá-la em algum lugar específico. Uma das grandes vantagens de Ruby é que a metalanguage (a linguagem em que se escreve o código meta) e a object language (a linguagem que interpreta e manipula o código meta) são a mesma, ou seja, Ruby, esse princípio é descrito como reflexividade. Graças a reflexividade, Ruby apresenta uma característica chamada introspecção. Então, quando rodamos um código, podemos fazer perguntas aos nossos objetos, classes e atributos durante a execução. A esse conjunto de dados (objetos, classes, módulos, etc…) damos o nome de construções da linguagem (language constructs). Então, em Ruby, podemos perguntar para um objeto, por exemplo, se ele é instância de uma determinada classe através do método #instance_of? ou recuperar a sua classe diretamente através do método #class, essa forma de dinamicidade, em que as construções da linguagem interagem entre si e parecem vivas durante a execução, permite uma grande flexibilidade, mas antes de ver algumas formas de se aproveitar dessa flexibilidade (que virão em outros posts), é necessário ver como são modelados os language constructs através do object model.

Mas porque entender o Object Model?

Object model é, por definição, um termo genérico que descreve a coleção de fundações e abstrações que descrevem os princípios básicos da orientação a objetos, entre eles, alguns termos bem famosos como abstração, encapsulamento, modularidade, concorrência e persistência. Mas qual a importância disso? Sendo Ruby uma linguagem predominantemente dinâmica e reflexiva, o entendimento de como as construções da linguagem interagem entre si e de como são implementadas permite-nos escrever código mais semântico e pronto para as eventuais mudanças que deverão ocorrer ao longo dos diversos ciclos de desenvolvimento. Isso nos permite antecipar algumas necessidades que poderão surgir a ponto de reduzir a necessidade de se refatorar uma classe, um módulo ou uma biblioteca. Deve-se notar aqui que o seu código não deve tentar prever quais são as eventuais novas funcionalidades, mas sim como facilmente se adaptar para inserir novas funcionalidades. Esse entendimento também nos proporciona formas de manipular efetivamente nossas criações, permitindo construções mais elaboradas e sintéticas, encapsulando devidamente as responsabilidades de cada construção em seu devido lugar e dando mais manutenabilidade ao código a medida que este desacopla as suas partes. Ou seja, tudo que queremos! 🙂

“O seu código não deve tentar prever quais são as eventuais novas funcionalidades, mas sim como facilmente se adaptar a elas.”

Agora que listamos os benefícios de entender de forma geral o Object Model, vamos brincar um pouco com as classes em Ruby e nos aprofundar no que está acontecendo de fato:

Classes e o conceito de Open Class

Em Ruby, uma classe é mais próxima de um escopo do que de uma estrutura. Nos podemos definir métodos nela, de modo que as instâncias dessa classe (ou objetos) poderão usufruir desses métodos. Mas por não ser uma estrutura rígida como em outras linguagens, podemos simplesmente declarar uma classe mais de uma vez, adicionando novos métodos em diversos pontos distintos do código. Devido a essa natureza das classes em Ruby, o seguinte código funciona:


class A
  def say_something_funny
    puts "no"
  end
end

class A
  def say_something_at_least
    puts "ok! something at least"
  end
end

obj = A.new
obj.say_something_funny # => "no"
obj.say_something_at_least # => "ok! something at least"

Esse conceito é denominado Open Class, ou seja, você pode abrir uma classe a qualquer momento e escrever um novo método nela. A partir desse momento o método será parte integrante da classe. Mas, e se eu definir um método que já existe? Se logo em seguida do código acima reescrevermos o método #say_something_funny, o resultado que obteremos será o seguinte (você mesmo pode verificar isso no IRB):


class A
  def say_something_funny
    puts "stop dude!"
  end
end

obj.say_something_funny # => "stop dude!"

Esse caso é conhecido como monkeypatch. Monkeypatch é um termo com duas conotações, uma positiva e uma negativa. As vezes nos referimos a monkeypatch quando, acidentalmente, reescrevemos um método de uma classe. Nesse momento, podemos ter alterado o comportamento de vários objetos que usufruem dessa classe, de modo que apenas nossos testes poderão nos salvar dos eventuais problemas que poderão ocorrer. No entanto, do lado positivo, monkeypatch também nos permite adicionar funcionalidades a uma série de classes e módulos já existentes, principalmente da biblioteca principal. Dois exemplos são a gem money, que faz um monkeypatch da classe Numeric e o módulo Kernel (o qual falaremos mais para frente). No caso da money, podemos chamar explicitamente em qualquer número o método #to_money, que pode receber como parâmetro o tipo da moeda (USD, BRL) (por default USD), formatando o valor apropriadamente para exibição. No caso do Kernel, podemos definir métodos que queremos disponíveis para todos os objetos no nosso código, fornecendo-os de modo transparente e quase mágico a todas as instâncias. Uma gem que se utiliza disso é a Awesome Print, que define dois métodos, #ai e #ap diretamente no Kernel.

Mas voltando ao assunto de classes, uma dúvida que deve surgir é, principalmente para aqueles que trabalharam e que conhecem linguagens não dinâmicas, porque eu posso fazer tudo o que fiz até agora? Bom, como eu disse, classes em Ruby são mais como escopos do que como estruturas fixas e engessadas (na realidade classes são instâncias também – objetos – de uma classe, mas vamos com calma). Se pararmos para analisar, nossas instâncias de classe possuem de fato esses métodos? Em Ruby, as instâncias de uma classe servem apenas para guardar as variáveis de instância (‘@’) e uma referência para uma classe, ou seja, os métodos de instância em Ruby não se encontram no objeto, mas na classe desse objeto, o que faz sentido, já que um conjunto de objetos compartilha de um conjunto de métodos através de sua classe.

Se perguntarmos para o nosso objeto obj do exemplo anterior qual a sua classe, este prontamente nos responde obj.class # => A , se perguntarmos quem são suas variáveis de instância, veremos que não existem variáveis de instância. Se criarmos um novo objeto da classe A, veremos que esse objeto possui os mesmos métodos de obj, mas não necessariamente as mesmas variáveis de instância. Se abrirmos a classe A novamente e definirmos um método que atribui um valor para uma variável de instância qualquer, por exemplo, @b, observamos que enquanto esse novo método não for chamado, o objeto não possui nenhuma variável de instância, elas simplesmente nascem quando recebem algum valor. De modo mais geral, em Ruby não existe uma relação estrita entre as variáveis de instância de um objeto e a sua classe. Essas variáveis só existem a partir do momento em que atribuimos valor a elas:


class A
  def set_some_instance_variable
    @b = "Now I'm alive"
  end
end

obj.set_some_instance_variable # => "Now I'm alive"
obj.instance_variables # => [:@b]

new_obj = A.new
new_obj.instance_variables # => []

Antes de prosseguirmos, devemos nos atentar ao seguinte fato: Os métodos definidos numa classe não residem na instância dessa classe, mas na classe em si. No entanto, não é correto dizer que a classe A possui o método #say_something_funny, porque quem possui esse método são as instâncias de A. Para remover a ambiguidade, o ideal é dizer que o objeto possui o método #say_something_funny ou que A possui o método de instância #say_something_funny. Essa convenção fica mais evidente quando perguntamos diretamente para Ruby o que é igual com o que:


class A
  # instance methods ...
  # class methods ...
end

obj = A.new
obj.methods == A.instance_methods # => true, segundo a definição acima
obj.methods == A.methods # => false, porque A possui mais métodos do que apenas os de instância

O object model (Classes são objetos)

Falemos então, finalmente, do Object Model. Acho que a mais importante definição do object model em Ruby é que as classes que criamos não são nada além de simples objetos. Então, se perguntarmos para uma classe quem é a sua classe, observamos imediatamente que toda classe é instância de Class, ou seja, A.class => Class. Isso significa que, como objetos, os métodos de uma classe também são os métodos de instância de Class (assim como os métodos do objeto ‘obj’ eram os métodos de instância de A). Mas quais são os métodos de instância de Class? Bom, basicamente são três métodos, sendo que os dois métodos que nos interessam prioritariamente são #new e #superclass. O método #new já é um velho conhecido nosso, sempre que queremos criar uma nova instância de uma classe utilizamos esse método diretamente na classe (porque é um método da classe e um método de instância de Class). O segundo método, #superclass, nos informa a respeito de um conceito chamado herança (inheritance). No conceito de herança, falamos que uma classe A herda de sua superclasse. Se não especificarmos quem é essa classe na herança, então a classe herda de Object, que por sua vez herda da definição mais básica BasicObject. Se especificarmos de quem a classe herda, então #superclass retorna essa classe. Vejamos um exemplo:


class A
end

class B
  def hello
    "Hello world!"
  end
end

class C < B
end

A.superclass # => Object
B.superclass # => Object
B.instance_methods(false) # => [:hello]
B.new.hello # => "Hello world!"
C.superclass # => B
C.instance_methods(false) # => []
C.new.hello # => "Hello world!"

Nesse exemplo há algumas coisas apreciáveis. Primeiro, observa-se novamente que se não é definida uma classe da qual se herda, então a classe automaticamente herda de Object. Então, C, por herdar de B, responde a pergunta #superclass com a resposta B, como esperado. Observa-se também que, embora C herde de B, os métodos de instância definidos em B não existem em C. Se pararmos pra pensar nas definições anteriores, percebemos que isso faz todo o sentido, visto que os métodos de instância residem nas classes e C não declara nenhum método de instância em si, apenas utiliza os herdados de B, ou seja, o método reside em B (ele não é copiado para C). Disso não podemos dizer que B é um C, porque eventualmente C pode adicionar novos métodos em si, mas como C sempre terá os mesmos métodos que forem definidos em B, podemos dizer que C é um B. Mas então, será que Class possui uma superclass? A resposta é sim. O comportamento de Class é herdado de Module, ou seja, Class.superclass # => Module. Na realidade, a única diferença entre uma classe e um módulo é que Class possui 3 métodos adicionais de instância, aqueles que listamos acima (new, superclass e allocate). Essa ligeira diferença permite-nos escolher entre um módulo, quando o nosso desejo é adicioná-lo em algum lugar através de #include, ou uma classe, quando desejamos as suas instâncias ou herança. Resumindo esses conceitos, de uma forma geral, temos o seguinte diagrama:

Ruby Object Model - Básico

Ruby Object Model – Básico

Mas Ruby vai ainda mais longe, é interessante notar que como classes são instâncias de Class, do mesmo modo que atribuímos a uma variável o valor de uma instância qualquer, podemos atribuir a uma variável a referência para uma classe, ou seja, myclass = A me permite escrever myclass.new.say_something_funny. Como myclass guarda uma referência para A, esse código funciona. Então porque não definimos as classes como myclass, mas sim como MyClass? Bom, em Ruby, qualquer referência que se inicie com uma letra maiúscula é dita uma constante. Constantes são muito parecidas com variáveis, de modo que podemos até mudar o seu valor. No entanto, uma constante possui um escopo diferenciado em relação a uma variável, de modo que duas constantes podem ter o mesmo nome se estiverem em ‘locais’ diferentes. Aqui local descreve onde a constante se encontra em relação a outra constante. Vejamos um exemplo:


Something = "Outside"
module AM
  Something = "In the AM Module"
  class A
    Something = "In the A Class"
  end
  class B
    Something = ::Something
  end
end

Something # => "Outside"
AM::Something # => "In the AM Module"
AM::A::Something # => "In the A Class"
AM::B::Something # => "Outside"

Nesse exemplo observamos algumas coisas. Primeiro, existem múltiplas constantes com o nome Something. Uma externa ao módulo (a essa damos o nome de root-level constant), uma dentro do módulo, que dizemos estar em AM, e constantes dentro das classes A e B (que também são constantes). Para acessar uma constante dentro de um caminho, usamos o operador ::, assim, podemos caminhar até a constante que desejamos. Se queremos acessar uma constante que se encontra no root-level, podemos usar :: no começo, como é feito na classe B. Assim, em B, a constante Something é uma referência para a constante Something no root-level, por isso o resultado foi “Outside”. Podemos inspecionar quais são as constantes de uma determinada classe ou módulo através do método #constants. Então, se perguntarmos para AM quem são suas constantes, a resposta vem em forma de Array AM.constants # => [:Something, :A, :B]. Observe que, como falamos antes, os nomes das classes também são constantes, por isso aparecem em nossa listagem.

Coffee Break

Depois de tudo isso, é bom pararmos pra absorver efetivamente o que foi visto, então vamos fazer uma rápida revisão: Vimos que uma classe é na realidade uma instância de Class, e que toda Class é um Módulo, porque herda deste. Vimos que a superclass de uma classe qualquer, quando não especificada qual a herança, é Object, e que Object herda de BasicObject. Por fim, vimos alguns métodos de Ruby para entendermos o que está acontecendo de fato, métodos como #class, #superclass, #constants, #instance_of?, entre outros… Que nos lembra do conceito de introspecção, que nos lembra que Ruby não é só dinâmico, como reflexivo. Agora, alguns pontos ainda estão soltos. Por exemplo, Module herda de quem? Object possui classe? E BasicObject? E qual a classe de Class? E quem é o Kernel? Essas respostas virão no próximo post, onde continuaremos a falar do object model e de como Ruby encontra um método quando chamamos esse método através do method lookup. Se você lembrar, quando falei de herança, no exemplo em que C herdava de B, falei que C não continha o método de B, que este continuava se encontrando em B. Isso é a base para entendermos como Ruby acha os métodos, que será o grande foco do próximo post! No próximo post também reformularemos o diagrama do Object Model com as respostas as perguntas acima 🙂 . Espero que tenham gostado do assunto, embora este seja extremamente denso! O bom entendimento disso permite entender como Ruby funciona mais a fundo, sendo uma base fundamental para utilizar metaprogramming efetivamente e decidir quando vale a pena ou não usar alguma técnica de metaprogramming de um problema específico. Até o próximo post!


Comentários

2
  • Pedro Bruno

    Pedro Bruno Pedro Bruno

    Responder Autor

    Otimo artigo, parabéns! Vou ler a parte 2 agora!!

    Postado em

  • Igor Melão

    Igor Melão Igor Melão

    Responder Autor

    Nossa parabéns pelo artigo.

    Essa base fundamental é essencial para começar a utilizar de forma consciente e inteligente a metaprogramming.

    Postado em