Back-End

20 fev, 2013

Cópias de defensive objects em Java e como Scala evita isso

Publicidade

Em uma palavra: imutabilidade. Mas continue lendo para conhecer a história toda =)

Em outro artigo, eu escrevi sobre o uso de um valor de long primitivo em vez de objetos de dados em atributos. Esse tipo de coisa é normalmente chamado de defensive copy, ou seja, nós copiamos o valor que recebemos em um compositor ou constructor, em vez de atribuí-lo diretamente. Poderíamos também termos copiado o dado como um novo objeto de data. Isso pode parecer simples, mas espere até que você tenha objetos grandes, com hierarquias complexas.

Em teoria, o método clone poderia nos ajudar a resolver esse problema. Eu escrevi sobre esse método muito tempo atrás: os males do método clone java, ilustrando um pouco como a linguagem Java, por vezes, nos faz pensar de forma errada. O livro Effective Java também explica esse problema, então talvez você queira dar uma olhada.

Mas essa análise sobre o método clone não foi de toda completa. Além do que o que eu mencionei anteriormente, existe pelo menos mais uma coisa que é ruim. Extraído do javadoc:

Por convenção, o objeto retornado por esse método deve ser independente do objeto (o qual está sendo clonado). Para alcançar essa independência, pode ser que seja necessário modificar um ou mais campos de um objeto retornado por super.clone antes de o retornar.

Isso significa que, qualquer coisa que ele faça o método clone não retornará uma cópia real do objeto, mas sim uma superficial. E vai mais longe ao dizer que você é responsável por fazer a cópia certa, modificando campos… Agora imagine fazer isso com um objeto que faz referência a alguns outros e que por sua vez faz referência a outros e outros… exponencialmente difícil de fazer isso certo.

class A(b: B) extends Cloneable {
  override def clone() = super.clone
  override def toString() = "[A: %s]".format(b)
}

class B(c: C) extends Cloneable {
  override def clone() = super.clone
  override def toString() = "[B: %s]".format(c)
}

class C(var x: Int) extends Cloneable {
  override def clone() = super.clone
  override def toString() = "[C: %d]".format(x)
}

val c = new C(10)
val b = new B(c)
val a = new A(b)

val a2 = a.clone
c.x = -99

O exemplo de código acima está no Scala, porém o código do Java seria muito semelhante. Então, esse é o código correto? Quando eu faço c.x = -99, isso deverá afetar apenas a, ou ambos a e a2? Atualmente ele faz a segunda opção:

scala> println(a)
[A: [B: [C: -99]]]

scala> println(a2)
[A: [B: [C: -99]]]

Então é aqui onde a clonagem recursiva teria lugar. Eu teria que fazer o método clone dentro de A fazer uma chamada para o método clone de B, e assim por diante. Isso não seria divertido. A linguagem Java não é agradável aqui, mas talvez o problema seja mais profundo. Vamos tentar mudar a nossa maneira de pensar.

Depois de alguns anos na terra do Scala, eu descobri que existe algo mais a acrescentar. Uma coisa da qual o Scala tenta convencê-lo é que você deve ter o máximo de objetos imutáveis possível. Isso significa que os objetos nunca mudam – você criará novos, se necessário. Isso também significa que eles podem ser compartilhados de forma fácil e segura – portanto, clone não é mais necessário.

De volta ao exemplo de código acima, c.x = -99 nunca seria permitido – então não é um problema de fato. Para que isso aconteça, a nossa nova classe C ficaria assim:

class C(x: Int)

Remover a palavra-chave var torna efetivamente x imutável. E se você realmente quiser um novo x? Você tem que criar uma nova A. Se essa classe teve mais campos, ela pode ser chata, então o Scala nos ajuda aqui. Vamos mudar nossas classes para serem imutáveis, esqueça as coisas de clonagem, e classes de caso. Vamos adicionar também outro atributo para A.

case class A(b: B, name: String)
case class B(c: C)
case class C(x: Int)

val c = C(10)
val b = B(c)
val a = A(b, "jcranky")

Primeiro, sendo classes de caso, não precisamos nos preocupar com as substituições do método toString. Nem com o operador new. E nós também temos um prêmio: a função copy. Com isso, podemos copiar um objeto e alterar apenas o que queremos. Este é um exemplo de execução fora de novas classes:

scala> println(a)
A(B(C(10)),jcranky)

val a2 = a.copy(b = B(C(-99))

scala> println(a)
A(B(C(10)),jcranky)

scala> println(a2)
A(B(C(-99)),jcranky)

A única desvantagem é que você tem que projetar o seu aplicativo com esse tipo de estrutura em mente. A partir daí, apenas bônus. Você não tem muito que se preocupar com o bloqueio, por exemplo, já que não existe estabilidade mutável para cuidar.

***

Texto original disponível em http://jcranky.com/2012/08/23/defensive-object-copies-in-java-and-how-scala-avoids-it/