Extendendo o comportamento de clonagem de objetos em Ruby

Alguns dias atrás, me deparei com um comportamento inesperado em um código. Basicamente uma das minhas classes Ruby tinha como atributo uma coleção de objetos que quando processada por outras instâncias tinha alguns objetos dessa coleção alterados e com isso alguns efeitos colaterais aconteciam.

Vamos a um exemplo

Vou criar aqui um código bem simples apenas para lhe dar uma ideia melhor do que estou falando. Se quiser, fique a vontade para abrir o seu IRB e ir me acompanhando nos códigos abaixo.

Vamos criar duas classes, Father e Child (Pai e Filho). Child tem um atributo “name” e Father recebe um “child como atributo:

1
2
3
4
5
6
7
class Father
  attr_accessor :child
end

class Child
  attr_accessor :name
end

Para fins de praticidade não estou definindo os initializers. Então crio uma instância de Child e passo ela para Father.

1
2
3
4
5
child = Child.new
child.name = "Mr. White"

father = Father.new
father.child = child

Nesse momento então “father” tem uma cópia do objeto “child”:

1
2
3
father.child.name #=> "Mr. White"

father.child.onject_id == child.object_id # => true

Logo a seguir então crio um clone de Father, que carinhosamente irei chamar de dolly:

1
2
3
dolly = father.clone

dolly.child.name #=> "Mr. White"

Tudo ok até aqui.

Como já esperávamos dolly e father são objetos diferentes:

1
father.object_id == dolly.object_id # => false

Resolvemos então, sem desconfiar de nada, manipular o valor do atributo “name” na cópia de child que foi clonada para dolly.

1
2
3
dolly.child.name = "Jesse Pinkman"

dolly.child.name #=> "Jesse Pinkman"

Finalmente então podemos ver claramente o problema proposto aqui.

Era esperado que o objeto “father” tivesse sido completamente clonado para “dolly” e o seu estado original preservado, certo? Porém, quando atribuimos o valor “Jesse Pinkman” ao atributo “child.name” em “dolly”, alteramos ao mesmo tempo também o valor de “child.name” no objeto clonado “father”. Dê só uma olhada:

1
2
3
father.child.name #=> "Jesse Pinkman"

father.child.object_id == dolly.child.object_id # => true

father#child e dolly#child guardam a refência para o mesmo objeto. Que legal, né?

Se tentarmos usar o método dup ao invés de clone, vamos ter o mesmo resultado no final. Se quiser, faça o teste para conferir.

initialize_copy para o resgate

A nossa alternativa então é definir na classe Father um método initialize_copy, que vai nos permitir extender o comportamento padrão de clonagem do objeto para incluir as partes que ficaram de fora e que nos interessam, que no nosso caso seria clonar também as instâncias de child:

1
2
3
4
5
6
7
8
class Father
  attr_accessor :child

  def initialize_copy(cloning)
      super
      self.child = cloning.child.clone
  end
end

O nosso novo método initialize_copy recebe o objeto que estamos clonando e atribui um clone dele ao nosso atributo child.

Vamos conferir então se agora tudo esta funcionando da forma como esperávamos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
child = Child.new
child.name = "Mr. White"

father = Father.new
father.child = child

dolly = father.clone

dolly.child.name = "Jesse Pinkman"

dolly.child.name #=> "Jesse Pinkman"
father.child.name #=> "Mr. White"

father.child.object_id == dolly.child.object_id # => false

Perfeito, era isso mesmo que eu esperava que acontecesse!

Espero que se você tiver utilizando alguma técnica, como a de Dependency Injection por exemplo, e tiver uma situação parecida com essa que acabei de descrever você possa se lembrar de que é possivel extender o comportamento de clonagem de objetos em Ruby para que ela atenda específicamente as suas necessidades.

Comentários