Ci sono situazioni in cui dovresti preferire una classe senza casi?
Martin Odersky ci dà un buon punto di partenza nel suo corso Principi di programmazione funzionale in Scala (Lezione 4.6 - Pattern Matching) che potremmo usare quando dobbiamo scegliere tra classe e classe caso. Il capitolo 7 di Scala By Example contiene lo stesso esempio.
Diciamo, vogliamo scrivere un interprete per espressioni aritmetiche. Per mantenere le cose semplici inizialmente, ci limitiamo solo a numeri e operazioni +. Tali espressioni possono essere rappresentate come una gerarchia di classi, con una classe base astratta Expr come radice e due sottoclassi Number e Sum. Quindi, un'espressione 1 + (3 + 7) sarebbe rappresentata come
nuova somma (nuovo numero (1), nuova somma (nuovo numero (3), nuovo numero (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
}
Inoltre, l'aggiunta di una nuova classe Prod non comporta alcuna modifica al codice esistente:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
Al contrario, l'aggiunta di un nuovo metodo richiede la modifica di tutte le classi esistenti.
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(")")
}
}
Lo stesso problema risolto con le classi di casi.
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'aggiunta di un nuovo metodo è una modifica locale.
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'aggiunta di una nuova classe Prod richiede potenzialmente la modifica di tutti i criteri di corrispondenza.
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) => ...
}
}
Trascrizione della videolettura 4.6 Pattern Matching
Entrambi questi modelli vanno benissimo e la scelta tra di loro a volte è una questione di stile, ma comunque ci sono alcuni criteri che sono importanti.
Un criterio potrebbe essere: crei più spesso nuove sottoclassi di espressione o crei più spesso nuovi metodi? Quindi è un criterio che guarda all'estensibilità futura e al possibile passaggio di estensione del tuo sistema.
Se quello che fai è principalmente creare nuove sottoclassi, allora in realtà la soluzione di decomposizione orientata agli oggetti ha il sopravvento. Il motivo è che è molto semplice e una modifica molto locale creare una nuova sottoclasse con un metodo eval , dove come nella soluzione funzionale, dovresti tornare indietro e cambiare il codice all'interno del metodo eval e aggiungere un nuovo caso ad esso.
D'altra parte, se quello che farai creerà molti nuovi metodi, ma la gerarchia delle classi stessa sarà mantenuta relativamente stabile, allora il pattern matching è effettivamente vantaggioso. Perché, ancora una volta, ogni nuovo metodo nella soluzione di pattern matching è solo un cambiamento locale , sia che lo mettiate nella classe base, o forse anche al di fuori della gerarchia delle classi. Considerando che un nuovo metodo come show nella scomposizione orientata agli oggetti richiederebbe un nuovo incremento per ogni sottoclasse. Quindi ci sarebbero più parti, che devi toccare.
Quindi la problematica di questa estensibilità in due dimensioni, in cui potresti voler aggiungere nuove classi a una gerarchia, o potresti voler aggiungere nuovi metodi, o forse entrambi, è stata chiamata problema dell'espressione .
Ricorda: dobbiamo usarlo come punto di partenza e non come unico criterio.