Які недоліки в декларуванні класів справ Scala?


105

Якщо ви пишете код, який використовує безліч красивих, незмінних структур даних, класи регістрів, як видається, є знахідкою, що дає вам все наступне безкоштовно лише одним ключовим словом:

  • Усе незмінне за замовчуванням
  • Геттери визначаються автоматично
  • Гідне виконання toString ()
  • Сумісний дорівнює () і хеш-код ()
  • Об'єкт супутника з методом unapply () для відповідності

Але які недоліки у визначенні незмінної структури даних як класу випадків?

Які обмеження він ставить перед класом або його клієнтами?

Чи бувають ситуації, коли вам слід віддати перевагу некласичному класу?


Дивіться це пов’язане питання: stackoverflow.com/q/4635765/156410
Девід

18
Чому це не конструктивно? Моди на цьому сайті занадто суворі. Це має обмежену кількість можливих фактичних відповідей.
Елофф

5
Погодьтеся з Елоффом. Це питання, на яке я хотів відповісти, і надані відповіді дуже корисні і не видаються суб'єктивними. Я бачив багато питань "як виправити уривок коду", що викликає більше дискусій та думок.
Герк

Відповіді:


51

Один великий недолік: класи класів не можуть поширювати клас регістру. Це обмеження.

Інші переваги, які ви пропустили, перелічені для повноти: сумісна серіалізація / десеріалізація, не потрібно використовувати "нове" ключове слово для створення.

Я віддаю перевагу класам non-case для об'єктів із станом, що змінюється, приватним станом або не має стану (наприклад, більшість однотонних компонентів). Класи кейсів для майже всього іншого.


48
Ви можете підкласити клас речей. Підклас теж не може бути класом регістру - це обмеження.
Seth Tisue

99

Спочатку хороші шматочки:

Усе незмінне за замовчуванням

Так, і навіть можна відмінити (використовувати var), якщо це потрібно

Геттери визначаються автоматично

Можливо в будь-якому класі за допомогою префіксації парам з val

Гідна toString()реалізація

Так, дуже корисно, але при необхідності виконувати вручну на будь-якому занятті

Сумісні equals()таhashCode()

У поєднанні з легким узгодженням візерунка це основна причина, що люди використовують класи кейсів

Об'єкт-супровід з unapply()методом відповідності

Також це можливо зробити вручну на будь-якому класі, використовуючи витяжки

Цей список також повинен включати убер-потужний метод копіювання, що є однією з найкращих речей для Scala 2.8


Тоді погано, є лише кілька реальних обмежень із класами справ:

Ви не можете визначити applyоб'єкт-супутник, використовуючи той самий підпис, що і генерований компілятором метод

На практиці це рідко є проблемою. Зміна поведінки згенерованого методу застосувань гарантовано здивує користувачів і має бути сильно відсторонена. Єдине виправдання для цього - перевірити вхідні параметри - завдання, найкраще виконане в основному органі конструктора (що також робить перевірку доступною при використанні copy)

Ви не можете підклас

Щоправда, хоча клас випадків все ж може бути самим нащадком. Однією загальною моделлю є побудова ієрархії класів ознак, використовуючи класові регістри як вузли листя дерева.

Також варто відзначити sealedмодифікатор. Будь-який підклас ознаки з цим модифікатором повинен бути оголошений в одному файлі. У разі зіставлення шаблонів із екземплярами ознаки, компілятор може попередити вас, якщо ви не перевірили всі можливі конкретні підкласи. У поєднанні з класами справ це може запропонувати вам дуже високий рівень впевненості у своєму коді, якщо він складається без попередження.

Як підклас продукту, класи регістрів не можуть мати більше 22 параметрів

Ніякого реального вирішення, окрім припинення зловживань з класами з такою кількістю парам :)

Також ...

Ще одне обмеження, яке іноді зазначається, полягає в тому, що Scala (на даний момент) не підтримує ледачі парами (як lazy vals, але як параметри). Вирішення цього полягає в тому, щоб скористатись іменним парам і призначити його ледачому валу в конструкторі. На жаль, параметри з назвою не поєднуються з узгодженням шаблону, що запобігає використанню методики з класами регістрів, оскільки вона порушує генерований компілятором екстрактор.

Це актуально, якщо ви хочете реалізувати високофункціональні ліниві структури даних, і, сподіваємось, це буде вирішено за допомогою додавання ледачих парам до майбутнього випуску Scala.


1
Дякую за вичерпну відповідь. Я думаю, що все виняток "Ви не можете підклас", мабуть, навряд чи поступить у мене скоро.
Грем Леа

15
Ви можете підкласити клас речей. Підклас теж не може бути класом регістру - це обмеження.
Seth Tisue

5
Ліміт 22 параметрів для класів випадків видалений у Scala 2.11. issues.scala-lang.org/browse/SI-7296
Джонатан Кросмер

Неправильно стверджувати, що "Ви не можете визначити застосування у супутньому об'єкті, використовуючи той самий підпис, що і генерований компілятором метод". Хоча це вимагає стрибків через деякі обручі , щоб зробити це (якщо ви маєте намір зберегти функціональність , яка використовується для бути невидимо генерується компілятор) сходи, вона, безумовно , може бути досягнута: stackoverflow.com/a/25538287/501113
chaotic3quilibrium

Я широко використовую класи класів Scala і придумав "шаблон класового випадку" (який врешті-решт виявиться як макрос Scala), який допомагає вирішити ряд проблем, визначених вище: codereview.stackexchange.com/a/98367 / 4758
хаотичний3рівноважний

10

Я думаю, що тут застосовується принцип TDD: не надмірно розробляйте дизайн. Коли ви декларуєте щось як " case class, ви заявляєте про багато функціональності. Це зменшить гнучкість у зміні класу у майбутньому.

Наприклад, a case classмає equalsметод над параметрами конструктора. Ви можете не перейматися цим, коли ви вперше пишете свій клас, але, нарешті, ви можете вирішити, що ви хочете рівність ігнорувати деякі з цих параметрів або зробити щось трохи інше. Однак клієнтський код може бути записаний у середній час, що залежить від case classрівності.


4
Я не думаю, що клієнтський код не повинен залежати від точного значення "дорівнює"; Клас вирішує, що для нього означає "рівний". Автору класу слід вільно змінювати реалізацію 'рівних' за рядком.
pkaeding

8
@pkaeding Ви не можете мати код клієнта, який залежить від будь-якого приватного методу. Все, що є публічним, - це договір, на який ви погодилися.
Даніель К. Собрал

3
@ DanielC.Sobral Щоправда, але точна реалізація рівняння () (на яких полях він базується) не обов'язково в договорі. Принаймні, ви могли явно виключити це з договору, коли вперше пишете клас.
герман

2
@ DanielC.Sobral Ви суперечите собі: ви кажете, що люди навіть покладаються на реалізацію рівних за замовчуванням (яка порівнює ідентичність об'єкта). Якщо це правда, і ви пізніше напишете іншу рівну реалізацію, їхній код також порушиться. У будь-якому випадку, якщо ви вкажете умови до / після публікації та інваріанти, а люди їх ігнорують, це їхня проблема.
герман

2
@herman У тому, що я говорю, немає суперечності. Що стосується "їхньої проблеми", то обов'язково, якщо це не стане вашою проблемою. Скажімо, наприклад, тому, що вони є величезним клієнтом вашого запуску, або тому, що їх менеджер переконує верхнє керівництво, що це занадто дорого для них, щоб змінити, тож вам доведеться скасувати свої зміни, або тому що зміна спричиняє багатомільйонні долари помилка та повернеться тощо. Але якщо ви пишете код для хобі і не піклуєтесь про користувачів, вперед.
Даніель К. Собрал

7

Чи бувають ситуації, коли вам слід віддати перевагу некласичному класу?

Мартін Одерський дає нам хороший вихідний пункт у своєму курсі Принципи функціонального програмування в 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 і додати новий випадок йому.

З іншого боку, якщо те, що ви робите, буде створювати багато нових методів, але сама ієрархія класів буде підтримуватися відносно стабільною, то відповідність шаблонів насправді вигідна. Тому що, знову ж таки, кожен новий метод у рішенні зіставлення шаблонів - це лише локальна зміна , будь-то, якщо ви ставите його в базовий клас чи, можливо, навіть поза ієрархією класів. Тоді як новий метод, такий як показ у об'єктно-орієнтованому розкладі, потребує нового збільшення кожного підкласу. Так було б більше деталей, до яких ти повинен торкнутися.

Таким чином, проблема цієї розширюваності в двох вимірах, де ви можете додати нові класи до ієрархії, або ви можете додати нові методи, а може бути і те, і інше, було названо проблемою вираження .

Пам'ятайте: ми повинні використовувати це як вихідний пункт, а не як єдиний критерій.

введіть тут опис зображення


0

Я цитую це з Scala cookbookпо Alvin Alexanderглаві 6: objects.

Це одна з багатьох речей, які мені здалися цікавими в цій книзі.

Щоб забезпечити декілька конструкторів для класу case, важливо знати, що насправді робить декларація класу case.

case class Person (var name: String)

Якщо ви подивитесь на код, який створює компілятор Scala для прикладу класу case, ви побачите, що бачите, що він створює два вихідні файли, Person $ .class та Person.class. Якщо ви демонтуєте Person $ .class за допомогою команди javap, ви побачите, що він містить метод застосунку разом з багатьма іншими:

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

Ви також можете розібрати Person.class, щоб побачити, що він містить. Для такого простого класу він містить додаткові 20 методів; ця прихована роздуття є однією з причин, що деякі розробники не люблять класи випадків.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.