Case Classes com Scala | DClick
Posted in Scala, Tutorial	Vimos que Scala na verdade é uma linguagem puramente orientada a objetos, com conceitos de classe, instância, heirarquia e polimorfismo como Java por exemplo. Mas Scala traz também alguns conceitos próprios muito interessantes, que é o caso de Case Classes.
Case Classes são um caso de classes com regras pré-definidas em tempo de compilação, de maneira que se permita executar algumas outras operações sobre tais classes facilitando algumas modelagens, e ajudando na aplicação de algumas boas práticas. Vamos estudar mais a fundo o conceito de Case Classes nesse post. Usei como referência o Scala By Example do Martin Odresky, o qual citei em posts anteriores.
Para acompanhar o post, é muito recomendado alguma IDE de desenvolvimento de scala. Estou usando o scala-ide plugin para o Eclipse, que pode ser encontrado em http://www.scala-ide.org/.
Motivação para Case Classes
Eu gostei bastante do exemplo disponível no livro, por isso vou seguir a mesma idéia nesse post só que com as minhas palavras do que eu consegui aprender sobre assunto pesquisando um pouco mais na internet, e citando também as dificuldades que enfrentei.
Imagine que queremos modelar um interpretador de expressões matemáticas que são descritas em objetos do nosso domínio. Por exemplo, queremos interpretar o resultado de 1 + (3 + 7), e representando em nosso modelo, queremos obter algo como:
1new Sum(new Number(1), new Sum(new Number(3), new Number(7)))
Uma maneira orientada a objetos de se implemetar tal domínio poderia ser algo como:
123456789trait Expr {
A implementação é bastante limpa e nosso interpretador seria mais simples ainda:
1234object Inter {
def inter(e: Expr) =
Se você assistiu o screencast sobre integração com Java, e JUnit rodando testes de Scala, você pode fazer um teste para verificar que nosso interpretador está funcionando corretamente:
123456789101112package br.com.dclick
class InterTest {
def testInter = {
var res = Inter.inter(new Sum(new Number(1), new Sum(new Number(3), new Number(7))))
assertEquals(11, res)
Para essa modelagem, quando quisermos adicionar uma nova operação que pode ser interpretada, basta criar uma nova classe que extends Expr e pronto! Nada mais precisa ser alterado em nosso código, e nosso interpretador continuará funcionando.
Agora imagine que antes de executar a expressão, queremos imprimir no console de um jeito amigável a expressão matemática que está sendo executada. Nesse caso, de acordo com nossa modelagem, teríamos que definir em nosso Expr uma função print, para que todas as suas implementações definam o comportamento de tal função. Para isso temos que alterar o código em todas as classes que já existem, o que pode ser ruim dependendo do nosso sistema.
Aplicando Case Classes
Em Scala existe a definição de Case Class. Para criar uma Case Class basta adicionar o modificador case antes da definição da classe. Em nosso exemplo faça as seguintes mudanças:
12case class Number(n: Int) extends Expr
Case classes seguem as seguintes regras:
1 – Possuem um construtor definido exatamente com o mesmo nome definido na classe. Em nosso exemplo:
1def Number(n: Int) = new Number(n)
Dessa forma é possível escrever nossa expressão 1 + (3 + 7) da seguinte forma:
1Sum(Number(1), Sum(Number(3), Number(7)))
Substitua no teste para ter certeza do funcionamento.
2 – Os métodos toString, equals e hashCode já estão implementados seguindo a definição da classe com seus atributos. Para nosso exemplo com Number, esses métodos já levam em consideração o atributo n, deixando de comparar os objetos por endereço de memória. Os operadores == e != também já estão definidos em função do equals.
3 – Todos os atributos passados no construtor possuem os métodos de acesso público já definidos. Em nosso exemplo, Number e Sum já possuem os seguintes métodos:
1def n: Int
1def e1: Expr, e2: Expr
Fato importante é que tais classes se tornam imutáveis, ou seja, não existe um método para alterar o valor dos atributos, e nem é possível definí-los.
4 – Case Classes podem ser usadas para pattern matching, que é uma operação disponível em Scala e que veremos agora.
Match, não Switch
Vimos que a classe base do Scala é o Any, e que todos objetos podem ser tratados como tal. Any define uma função que permite verificar o tipo de classe que está sendo tratado e tomar a atitude correspondente. Vamos ao exemplo que explica melhor o comportamento. Em nosso object Inter, mude a função inter para o seguinte:
12345def inter(e: Expr): Int =
case Sum(a, b) => inter(a) + inter(b)
Rode nosso teste novamente e verifique que está tudo correto.
Para entender o que está acontecendo: estamos dizendo que um dos casos esperados é o caso em que e é do tipo Number, que recebe um parâmetro que chamamos de n. Note que não precisamos definir o tipo de n, pois o compilador consegue inferir baseado na definição do construtor de Number. Feito isso, n está disponível no contexto do case atual. Para nosso caso com Number, precisamos apenas devolver o valor de n.
No segundo caso, estamos esperando uma Expr do tipo Sum, que recebe dois parâmetros e que baseado no construtor definido na classe, sabemos que é do tipo Expr. Nesse caso devolvemos o inter de a somado ao inter de b.
Nessa nossa nova modelagem fica fácil adicionar uma nova funcionalidade ao comportamento das expressões, como por exemplo imprimir de maneira legível. Basta adicionar mais um método ao nosso interpretador. Claro que agora temos o problema adicionar um novo caso quando criarmos uma nova operação, mas a quantidade de código a ser implementada é consideravelmente menor.
Match é definida como uma função como qualquer outra em Scala, portanto pode ser atribuída a variáveis, ser devolvida como resultado e ser passada como parâmetro para outras funções. Pode-se também criar um bloco como o seguinte:
1{ case Number(n) => n case Sum(a, b) => a + b }
e usá-lo como uma função anônima. Dessa forma será criado uma função match para tratar com seus casos definidos no bloco.
Veremos Exceptions mais para frente, mas perceba como funciona o bloco try/catch em Scala, e veja se há alguma semelhança com o que acamos de ver:
1234567try {
case npe: NullPointerException => print(npe.getMessage)
case ioe: IOException => print(ioe.getMessage)
case e: Exception => print(e.getMessage)
Pois bem, catch possui o mesmo comportamento de match para execeções.
Vou me basear no exercício disponível no livro do Martin.
Considere que queremos implementar uma árvore binária ordenada de inteiros. E tome a seguinte implementação como base:
1abstract class IntTree
1case object EmptyTree extends IntTree {}
1case class Node(elem: Int, left: IntTree, right: IntTree) extends IntTree {}
Agora complete a seguinte implementação dentro de IntTree:
123456789101112abstract class IntTree {
def contains (t: IntTree, v: Int): Boolean =
def insert(t: IntTree, v: Int): IntTree =
Lembre-se que Node é imutável, e que é obrigatório usar Case Classes. Boa sorte 🙂 !
A reposta está no screencast a seguir, junto com a explicação:
« Desenvolvimento da UX
Usando Código Java em Scala – Exemplo JUnit 4! »
Last reply was 19/11/2010	Generics, como Scala trata o Problema de Generalização de Classes | DClick Team Weblog [pt]	View
[…] Implementação diferente do usual, né? Repare que criei dois tipos de célula: uma célula que está sempre vazia, e uma célula que contém de fato o valor a ser guardado. Fiz isso pois em Scala não existe (ou a idéia é não utilizar) null, assim a checagem é feita por células vazias e não por null. Como Scala utiliza código Java, é possível passar e receber valores nulos. Código puramente em Scala estão praticamente livres de NullPointerException, justamente por essa restrição com valores nulos. Vamos ver logo mais um caso em que é possível obter tal erro. Em Scala uma das alternativas para execução de funções que devolvem null como resultado para certas circunstâncias, é o Option. Option, como o próprio nome sugere, se trata de um opção por devolver, ou não um valor. No caso de se devolver algum valor, deve-se devolver um Some contendo o valor. No caso de não se devolver o valor, deve-se devolver um None. Ambos são Case Classes, portanto não necessitam da palavra new, e possuem todas a propriedades que vimos em um post anterior. […]