Y a-t-il des situations où vous devriez préférer une classe sans cas?
Martin Odersky nous donne un bon point de départ dans son cours Principes de programmation fonctionnelle en Scala (Lecture 4.6 - Pattern Matching) que nous pourrions utiliser lorsque nous devons choisir entre la classe et la classe de cas. Le chapitre 7 de Scala By Example contient le même exemple.
Disons que nous voulons écrire un interpréteur pour les expressions arithmétiques. Pour garder les choses simples au départ, nous nous limitons aux nombres et aux opérations +. De telles expressions peuvent être représentées sous la forme d'une hiérarchie de classes, avec une classe de base abstraite Expr comme racine, et deux sous-classes Number et Sum. Ensuite, une expression 1 + (3 + 7) serait représentée par
nouvelle somme (nouveau nombre (1), nouvelle somme (nouveau nombre (3), nouveau nombre (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
}
De plus, l'ajout d'une nouvelle classe Prod n'entraîne aucune modification du code existant:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
En revanche, l'ajout d'une nouvelle méthode nécessite la modification de toutes les classes existantes.
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(")")
}
}
Le même problème résolu avec les classes de cas.
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
L'ajout d'une nouvelle méthode est un changement 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(")")
}
}
}
L'ajout d'une nouvelle classe Prod nécessite potentiellement de modifier toutes les correspondances de modèles.
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) => ...
}
}
Transcription de la vidéolecture 4.6 Pattern Matching
Ces deux modèles sont parfaitement bien et le choix entre eux est parfois une question de style, mais il y a néanmoins certains critères qui sont importants.
Un critère pourrait être: créez-vous plus souvent de nouvelles sous-classes d'expression ou créez-vous plus souvent de nouvelles méthodes? C'est donc un critère qui regarde l'extensibilité future et le passage d'extension possible de votre système.
Si vous créez principalement de nouvelles sous-classes, la solution de décomposition orientée objet a le dessus. La raison en est qu'il est très facile et très local de créer simplement une nouvelle sous-classe avec une méthode eval , où, comme dans la solution fonctionnelle, vous devrez revenir en arrière et modifier le code dans la méthode eval et ajouter un nouveau cas à lui.
D'un autre côté, si ce que vous faites est de créer beaucoup de nouvelles méthodes, mais que la hiérarchie des classes elle-même restera relativement stable, alors la correspondance de modèles est en fait avantageuse. Parce que, encore une fois, chaque nouvelle méthode dans la solution de correspondance de modèle n'est qu'un changement local , que vous la placiez dans la classe de base ou peut-être même en dehors de la hiérarchie des classes. Alors qu'une nouvelle méthode telle que show dans la décomposition orientée objet nécessiterait une nouvelle incrémentation dans chaque sous-classe. Il y aurait donc plus de parties que vous devez toucher.
Ainsi, la problématique de cette extensibilité en deux dimensions, où vous pourriez vouloir ajouter de nouvelles classes à une hiérarchie, ou vous pourriez vouloir ajouter de nouvelles méthodes, ou peut-être les deux, a été nommée le problème d'expression .
N'oubliez pas: nous devons utiliser cela comme un point de départ et non comme les seuls critères.