Existem situações em que você deve preferir uma aula que não seja o caso?
Martin Odersky nos dá um bom ponto de partida em seu curso Princípios de Programação Funcional em Scala (Aula 4.6 - Correspondência de Padrões) que podemos usar quando devemos escolher entre classe e classe de caso. O capítulo 7 de Scala por exemplo contém o mesmo exemplo.
Digamos que queremos escrever um intérprete para expressões aritméticas. Para manter as coisas simples inicialmente, nos restringimos a apenas números e operações +. Essas expressões podem ser representadas como uma hierarquia de classes, com uma classe base abstrata Expr como raiz e duas subclasses Número e Soma. Então, uma expressão 1 + (3 + 7) seria representada como
nova soma (novo número (1), nova soma (novo número (3), novo número (7)))
abstract class Expr {
def eval: Int
}
class Number(n: Int) extends Expr {
def eval: Int = n
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
}
Além disso, adicionar uma nova classe Prod não acarreta nenhuma alteração no código existente:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
Em contraste, adicionar um novo método requer a modificação de todas as classes existentes.
abstract class Expr {
def eval: Int
def print
}
class Number(n: Int) extends Expr {
def eval: Int = n
def print { Console.print(n) }
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
def print {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
}
O mesmo problema foi resolvido com classes de caso.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
Adicionar um novo método é uma mudança local.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
def print = this match {
case Number(n) => Console.print(n)
case Sum(e1,e2) => {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
}
}
Adicionar uma nova classe Prod requer, potencialmente, alterar toda a correspondência de padrões.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
case Prod(e1,e2) => e1.eval * e2.eval
}
def print = this match {
case Number(n) => Console.print(n)
case Sum(e1,e2) => {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
case Prod(e1,e2) => ...
}
}
Transcrição da videoleta 4.6 Correspondência de padrões
Ambos os designs são perfeitamente bons e escolher entre eles às vezes é uma questão de estilo, mas, no entanto, existem alguns critérios que são importantes.
Um critério poderia ser : você está criando com mais frequência novas subclasses de expressão ou está criando novos métodos com mais frequência? Portanto, é um critério que olha para a extensibilidade futura e a possível passagem de extensão de seu sistema.
Se o que você faz é principalmente criar novas subclasses, então, na verdade, a solução de decomposição orientada a objetos tem a vantagem. A razão é que é muito fácil e uma mudança muito local para apenas criar uma nova subclasse com um método eval , onde como na solução funcional, você teria que voltar e alterar o código dentro do método eval e adicionar um novo caso para isso.
Por outro lado, se o que você fizer for criar muitos métodos novos, mas a própria hierarquia de classes for mantida relativamente estável, então a correspondência de padrões é realmente vantajosa. Porque, novamente, cada novo método na solução de correspondência de padrões é apenas uma mudança local , independentemente de você colocá-lo na classe base ou talvez até mesmo fora da hierarquia de classes. Considerando que um novo método, como show na decomposição orientada a objeto, exigiria uma nova incrementação em cada subclasse. Portanto, haveria mais partes, que você teria que tocar.
Portanto, a problemática dessa extensibilidade em duas dimensões, onde você pode querer adicionar novas classes a uma hierarquia, ou pode querer adicionar novos métodos, ou talvez ambos, foi chamada de problema de expressão .
Lembre-se: devemos usar isso como um ponto de partida e não como o único critério.