Czy są sytuacje, w których wolisz zajęcia bez przypadków?
Martin Odersky daje nam dobry punkt wyjścia w swoim kursie Zasady programowania funkcjonalnego w Scali (Wykład 4.6 - Dopasowywanie wzorców), z którego moglibyśmy skorzystać, gdy musimy wybrać między klasą a klasą przypadku. Rozdział 7 książki Scala By Example zawiera ten sam przykład.
Powiedzmy, że chcemy napisać interpretera dla wyrażeń arytmetycznych. Aby początkowo wszystko było proste, ograniczamy się tylko do liczb i + operacji. Takie wyrażenia mogą być reprezentowane jako hierarchia klas, z abstrakcyjną klasą bazową Wyr jako rdzeniem i dwiema podklasami Liczba i Suma. Następnie wyrażenie 1 + (3 + 7) byłoby reprezentowane jako
nowa suma (nowa liczba (1), nowa suma (nowa liczba (3), nowa liczba (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
}
Ponadto dodanie nowej klasy Prod nie pociąga za sobą żadnych zmian w istniejącym kodzie:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
Natomiast dodanie nowej metody wymaga modyfikacji wszystkich istniejących klas.
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(")")
}
}
Ten sam problem rozwiązany z klasami przypadków.
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
Dodanie nowej metody jest zmianą lokalną.
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(")")
}
}
}
Dodanie nowej klasy Prod wymaga potencjalnie zmiany całego dopasowania wzorców.
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) => ...
}
}
Transkrypcja z wykładu wideo 4.6 Dopasowywanie wzorców
Oba te projekty są w porządku, a wybór między nimi jest czasami kwestią stylu, ale mimo to są pewne kryteria, które są ważne.
Jednym z kryteriów może być: czy częściej tworzysz nowe podklasy ekspresji, czy częściej tworzysz nowe metody? Jest to więc kryterium, które dotyczy przyszłej rozszerzalności i możliwego przebiegu rozszerzenia twojego systemu.
Jeśli robisz głównie tworzenie nowych podklas, to w rzeczywistości rozwiązanie do dekompozycji obiektowej ma przewagę. Powodem jest to, że bardzo łatwo i bardzo lokalną zmianą jest po prostu utworzenie nowej podklasy z metodą eval , gdzie podobnie jak w rozwiązaniu funkcjonalnym należałoby cofnąć się i zmienić kod wewnątrz metody eval i dodać nowy przypadek do tego.
Z drugiej strony, jeśli tym, co zrobisz, będzie utworzenie wielu nowych metod, ale sama hierarchia klas będzie względnie stabilna, wtedy dopasowywanie wzorców jest w rzeczywistości korzystne. Ponownie, każda nowa metoda w rozwiązaniu dopasowywania wzorców jest tylko lokalną zmianą , niezależnie od tego, czy umieścisz ją w klasie bazowej, czy może nawet poza hierarchią klas. Podczas gdy nowa metoda, taka jak show w dekompozycji obiektowej, wymagałaby nowej inkrementacji każdej podklasy. Więc byłoby więcej części, których musisz dotknąć.
Tak więc problematyka tej rozszerzalności w dwóch wymiarach, w przypadku której możesz chcieć dodać nowe klasy do hierarchii lub możesz chcieć dodać nowe metody, a może oba, została nazwana problemem z wyrażeniami .
Pamiętaj: musimy traktować to jako punkt wyjścia, a nie jako jedyne kryteria.