Чи бувають ситуації, коли вам слід віддати перевагу некласичному класу?
Мартін Одерський дає нам хороший вихідний пункт у своєму курсі Принципи функціонального програмування в Scala (Лекція 4.6 - Узгодження зразків), який ми могли б використати, коли мусимо вибирати між класом і класом випадку. Розділ 7 Scala за прикладом містить той же приклад.
Скажімо, ми хочемо написати інтерпретатора для арифметичних виразів. Щоб спочатку все було простим, ми обмежуємося лише числами та + операціями. Такі вирази можуть бути представлені у вигляді ієрархії класів, з кореневим абстрактним базовим класом Expr та двома підкласами Number і Sum. Тоді вираз 1 + (3 + 7) буде представлено як
нова сума (нове число (1), нова сума (нове число (3), нове число (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
}
Крім того, додавання нового класу Prod не тягне за собою змін до існуючого коду:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
На противагу цьому, додавання нового методу вимагає модифікації всіх існуючих класів.
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(")")
}
}
Та ж проблема вирішена і з класами кейсів.
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
Додавання нового методу - це локальна зміна.
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(")")
}
}
}
Додавання нового класу Prod вимагає потенційної зміни всіх відповідностей шаблонів.
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) => ...
}
}
Стенограма з відеолекції 4.6 Узгодження зразків
Обидва ці дизайни ідеально чудові, а вибір між ними іноді є питанням стилю, але все ж є деякі критерії, які є важливими.
Одним із критеріїв може бути: чи частіше ви створюєте нові підкласи вираження чи частіше створюєте нові методи? Отже, це критерій, який враховує майбутню розширюваність та можливий пропуск у вашій системі.
Якщо ви, головним чином, створюєте нові підкласи, то насправді об'єктно-орієнтоване рішення декомпозиції має верх. Причина полягає в тому, що дуже просто і дуже локально змінити просто створити новий підклас методом eval , де, як і у функціональному рішенні, вам доведеться повернутися назад і змінити код всередині методу eval і додати новий випадок йому.
З іншого боку, якщо те, що ви робите, буде створювати багато нових методів, але сама ієрархія класів буде підтримуватися відносно стабільною, то відповідність шаблонів насправді вигідна. Тому що, знову ж таки, кожен новий метод у рішенні зіставлення шаблонів - це лише локальна зміна , будь-то, якщо ви ставите його в базовий клас чи, можливо, навіть поза ієрархією класів. Тоді як новий метод, такий як показ у об'єктно-орієнтованому розкладі, потребує нового збільшення кожного підкласу. Так було б більше деталей, до яких ти повинен торкнутися.
Таким чином, проблема цієї розширюваності в двох вимірах, де ви можете додати нові класи до ієрархії, або ви можете додати нові методи, а може бути і те, і інше, було названо проблемою вираження .
Пам'ятайте: ми повинні використовувати це як вихідний пункт, а не як єдиний критерій.