Gibt es Situationen, in denen Sie eine Nicht-Fall-Klasse bevorzugen sollten?
Martin Odersky gibt uns einen guten Ausgangspunkt in seinem Kurs Funktionsprogrammierprinzipien in Scala (Vorlesung 4.6 - Pattern Matching), den wir verwenden können, wenn wir zwischen Klasse und Fallklasse wählen müssen. Das Kapitel 7 von Scala By Example enthält dasselbe Beispiel.
Angenommen, wir möchten einen Interpreter für arithmetische Ausdrücke schreiben. Um die Dinge zunächst einfach zu halten, beschränken wir uns nur auf Zahlen und + Operationen. Solche Ausdrücke können als Klassenhierarchie mit einer abstrakten Basisklasse Expr als Wurzel und zwei Unterklassen Number und Sum dargestellt werden. Dann würde ein Ausdruck 1 + (3 + 7) dargestellt als
neue Summe (neue Nummer (1), neue Summe (neue Nummer (3), neue Nummer (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
}
Darüber hinaus führt das Hinzufügen einer neuen Prod-Klasse zu keinen Änderungen am vorhandenen Code:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
Im Gegensatz dazu erfordert das Hinzufügen einer neuen Methode die Änderung aller vorhandenen Klassen.
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(")")
}
}
Das gleiche Problem wurde mit Fallklassen gelöst.
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
Das Hinzufügen einer neuen Methode ist eine lokale Änderung.
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(")")
}
}
}
Das Hinzufügen einer neuen Prod-Klasse erfordert möglicherweise das Ändern aller Musterübereinstimmungen.
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) => ...
}
}
Transkript aus der Videolecture 4.6 Pattern Matching
Beide Designs sind vollkommen in Ordnung und die Wahl zwischen ihnen ist manchmal eine Frage des Stils, aber dennoch gibt es einige Kriterien, die wichtig sind.
Ein Kriterium könnte sein, erstellen Sie häufiger neue Unterklassen des Ausdrucks oder erstellen Sie häufiger neue Methoden? Es ist also ein Kriterium, das die zukünftige Erweiterbarkeit und den möglichen Erweiterungsdurchlauf Ihres Systems untersucht.
Wenn Sie hauptsächlich neue Unterklassen erstellen, hat die objektorientierte Zerlegungslösung tatsächlich die Oberhand. Der Grund ist, dass es sehr einfach und eine sehr lokale Änderung ist, nur eine neue Unterklasse mit einer Bewertungsmethode zu erstellen , bei der Sie wie bei der funktionalen Lösung zurückgehen und den Code innerhalb der Bewertungsmethode ändern und einen neuen Fall hinzufügen müssen dazu.
Auf der anderen Seite, wenn das, was Sie tun werden , viele neue Methoden schaffen werden, aber die Klassenhierarchie selbst relativ stabil gehalten werden, dann ist Musterabgleich tatsächlich vorteilhaft. Auch hier ist jede neue Methode in der Mustervergleichslösung nur eine lokale Änderung , unabhängig davon, ob Sie sie in die Basisklasse oder sogar außerhalb der Klassenhierarchie einfügen. Während eine neue Methode wie in der objektorientierten Zerlegung zeigen eine neue Inkrementierung erfordern würde, ist jede Unterklasse. Es würde also mehr Teile geben, die du anfassen musst.
Das Problem dieser Erweiterbarkeit in zwei Dimensionen, bei der Sie einer Hierarchie neue Klassen hinzufügen oder neue Methoden oder beides hinzufügen möchten, wurde als Ausdrucksproblem bezeichnet .
Denken Sie daran: Wir müssen dies als Ausgangspunkt verwenden und nicht als die einzigen Kriterien.