Apa kerugian dari mendeklarasikan kelas kasus Scala?

105

Jika Anda menulis kode yang menggunakan banyak struktur data yang indah dan tidak dapat diubah, kelas kasus tampak seperti berkah, memberi Anda semua hal berikut secara gratis hanya dengan satu kata kunci:

  • Semuanya tidak berubah secara default
  • Getter ditentukan secara otomatis
  • Penerapan toString () yang layak
  • Patuh sama dengan () dan hashCode ()
  • Objek pendamping dengan metode unapply () untuk pencocokan

Tapi apa kerugian dari mendefinisikan struktur data yang tidak dapat diubah sebagai kelas kasus?

Batasan apa yang diberlakukan pada kelas atau kliennya?

Adakah situasi di mana Anda harus memilih kelas non-case?

Graham Lea
sumber
Lihat pertanyaan terkait ini: stackoverflow.com/q/4635765/156410
David
18
Mengapa ini tidak konstruktif? Mod di situs ini terlalu ketat. Ini memiliki sejumlah kemungkinan jawaban faktual yang terbatas.
Eloff
5
Setuju dengan Eloff. Ini adalah pertanyaan yang ingin saya jawab juga, dan jawaban yang diberikan sangat berguna, dan tidak tampak subjektif. Saya telah melihat banyak pertanyaan 'bagaimana cara memperbaiki kutipan kode saya' yang menimbulkan lebih banyak perdebatan dan opini.
Herc

Jawaban:

51

Satu kerugian besar: kelas kasus tidak dapat memperluas kelas kasus. Itulah batasannya.

Keuntungan lain yang Anda lewatkan, tercantum untuk kelengkapan: serialisasi / deserialisasi yang sesuai, tidak perlu menggunakan kata kunci "baru" untuk membuat.

Saya lebih suka kelas non-case untuk objek dengan status bisa berubah, status privat, atau tanpa status (misalnya sebagian besar komponen tunggal). Kelas kasus untuk hampir semua hal lainnya.

Dave Griffith
sumber
48
Anda dapat membuat subkelas kelas kasus. Subclass tidak bisa menjadi kelas kasus juga - itulah batasannya.
Seth Tisue
99

Pertama, bagian yang bagus:

Semuanya tidak berubah secara default

Ya, dan bahkan dapat diganti (menggunakan var) jika Anda membutuhkannya

Getter ditentukan secara otomatis

Dimungkinkan di kelas mana pun dengan mengawali params dengan val

toString()Implementasi yang layak

Ya, sangat berguna, tetapi bisa dilakukan dengan tangan di kelas mana pun jika perlu

Sesuai equals()danhashCode()

Dikombinasikan dengan pencocokan pola yang mudah, inilah alasan utama orang menggunakan kelas kasus

Objek pendamping dengan unapply()metode pencocokan

Juga memungkinkan untuk dilakukan dengan tangan di semua kelas dengan menggunakan ekstraktor

Daftar ini juga harus menyertakan metode penyalinan yang sangat kuat, salah satu hal terbaik yang akan datang ke Scala 2.8


Lalu yang buruk, hanya ada beberapa batasan nyata dengan kelas kasus:

Anda tidak dapat menentukan applydalam objek pendamping menggunakan tanda tangan yang sama seperti metode yang dihasilkan kompilator

Namun dalam praktiknya, ini jarang menjadi masalah. Mengubah perilaku metode penerapan yang dihasilkan dijamin akan mengejutkan pengguna dan harus sangat tidak disarankan, satu-satunya alasan untuk melakukannya adalah dengan memvalidasi parameter masukan - tugas yang paling baik dilakukan di badan konstruktor utama (yang juga membuat validasi tersedia saat menggunakan copy)

Anda tidak bisa membuat subkelas

Benar, meskipun kelas kasus masih mungkin menjadi turunannya sendiri. Salah satu pola umum adalah membangun hierarki kelas dari ciri-ciri, menggunakan kelas kasus sebagai simpul daun dari pohon.

sealedPengubah juga perlu diperhatikan . Setiap subclass dari suatu sifat dengan pengubah ini harus dideklarasikan dalam file yang sama. Saat pencocokan pola terhadap instance dari sifat tersebut, compiler kemudian dapat memperingatkan Anda jika Anda belum memeriksa semua kemungkinan subkelas konkret. Saat digabungkan dengan kelas kasus, ini dapat menawarkan tingkat kepercayaan yang sangat tinggi pada kode Anda jika dikompilasi tanpa peringatan.

Sebagai subkelas Produk, kelas kasus tidak boleh memiliki lebih dari 22 parameter

Tidak ada solusi nyata, kecuali untuk berhenti menyalahgunakan kelas dengan parameter sebanyak ini :)

Juga...

Satu batasan lain yang kadang-kadang dicatat adalah bahwa Scala tidak (saat ini) mendukung parameter malas (seperti lazy vals, tetapi sebagai parameter). Solusi untuk ini adalah dengan menggunakan parameter menurut nama dan menetapkannya ke lazy val di konstruktor. Sayangnya, parameter dengan nama tidak bercampur dengan pencocokan pola, yang mencegah teknik yang digunakan dengan kelas kasus karena merusak ekstraktor yang dihasilkan kompiler.

Ini relevan jika Anda ingin mengimplementasikan struktur data malas yang berfungsi tinggi, dan mudah-mudahan akan diselesaikan dengan penambahan parameter malas ke rilis Scala di masa mendatang.

Kevin Wright
sumber
1
Terima kasih atas jawaban lengkapnya. Saya pikir semua kecuali "Anda tidak bisa membuat subkelas" mungkin tidak akan menghentikan saya dalam waktu dekat.
Graham Lea
15
Anda dapat membuat subkelas kelas kasus. Subclass tidak bisa menjadi kelas kasus juga - itulah batasannya.
Seth Tisue
5
Batas 22 parameter untuk kelas kasus dihapus di Scala 2.11. issues.scala-lang.org/browse/SI-7296
Jonathan Crosmer
Tidak benar untuk menyatakan "Anda tidak dapat mendefinisikan terapkan dalam objek pendamping menggunakan tanda tangan yang sama seperti metode yang dihasilkan kompilator". Meskipun memerlukan lompatan melalui beberapa rintangan untuk melakukannya (jika Anda bermaksud untuk mempertahankan fungsionalitas yang dulunya dihasilkan secara tidak terlihat oleh kompiler scala), ini pasti dapat dicapai: stackoverflow.com/a/25538287/501113
chaotic3quilibrium
Saya telah menggunakan kelas kasus Scala secara ekstensif dan telah menghasilkan "pola kelas kasus" (yang pada akhirnya akan berakhir sebagai makro Scala) yang membantu dengan sejumlah masalah yang diidentifikasi di atas: codereview.stackexchange.com/a/98367 / 4758
chaotic3quilibrium
10

Saya pikir prinsip TDD berlaku di sini: jangan mendesain berlebihan. Saat Anda mendeklarasikan sesuatu sebagai case class, Anda mendeklarasikan banyak fungsi. Itu akan mengurangi fleksibilitas yang Anda miliki dalam mengubah kelas di masa mendatang.

Misalnya, a case classmemiliki equalsmetode di atas parameter konstruktor. Anda mungkin tidak peduli tentang itu ketika Anda pertama kali menulis kelas Anda, tetapi, terakhir, mungkin memutuskan Anda ingin kesetaraan mengabaikan beberapa parameter ini, atau melakukan sesuatu yang sedikit berbeda. Namun, kode klien dapat ditulis dalam waktu yang bergantung pada case classkesetaraan.

Daniel C. Sobral
sumber
4
Saya tidak berpikir kode klien harus bergantung pada arti sebenarnya dari 'sama dengan'; terserah kelas untuk memutuskan apa arti 'sama' untuk itu. Penulis kelas harus bebas mengubah penerapan 'sama' di masa mendatang.
pkaeding
8
@pkaeding Anda bebas untuk tidak memiliki kode klien bergantung pada metode privat apa pun. Segala sesuatu yang bersifat publik adalah kontrak yang telah Anda setujui.
Daniel C. Sobral
3
@ DanielC.Sobral Benar, tetapi implementasi yang tepat dari equals () (bidang mana yang menjadi dasarnya) tidak harus ada dalam kontrak. Setidaknya, Anda bisa secara eksplisit mengecualikannya dari kontrak saat pertama kali menulis kelas.
herman
2
@ DanielC.Sobral Anda kontradiktif: Anda mengatakan bahwa orang-orang bahkan akan bergantung pada implementasi default yang sama (yang membandingkan identitas objek). Jika itu benar, dan Anda menulis implementasi sama yang berbeda nanti, kode mereka juga akan rusak. Bagaimanapun, jika Anda menentukan kondisi pra / pasca dan invarian, dan orang mengabaikannya, itulah masalah mereka.
herman
2
@herman Tidak ada kontradiksi dalam apa yang saya katakan. Adapun "masalah mereka", tentu, kecuali jika itu menjadi masalah Anda . Katakanlah, misalnya, karena mereka adalah klien besar dari startup Anda, atau karena manajer mereka meyakinkan manajemen atas bahwa terlalu mahal bagi mereka untuk mengubahnya, jadi Anda harus membatalkan perubahan Anda, atau karena perubahan tersebut menyebabkan jutaan dolar bug dan dikembalikan, dll. Tetapi jika Anda menulis kode untuk hobi dan tidak peduli dengan pengguna, silakan.
Daniel C. Sobral
7

Adakah situasi di mana Anda harus memilih kelas non-case?

Martin Odersky memberi kita titik awal yang baik dalam kursusnya Prinsip-Prinsip Pemrograman Fungsional dalam Skala (Kuliah 4.6 - Pencocokan Pola) yang dapat kita gunakan ketika kita harus memilih antara kelas dan kelas kasus. Bab 7 Scala By Example berisi contoh yang sama.

Katakanlah, kami ingin menulis penerjemah untuk ekspresi aritmatika. Untuk menjaga kesederhanaan pada awalnya, kami membatasi diri kami hanya pada angka dan + operasi. Ekspresi seperti itu dapat direpresentasikan sebagai hierarki kelas, dengan kelas dasar abstrak Ekspr sebagai root, dan dua subkelas Number dan Sum. Kemudian, ekspresi 1 + (3 + 7) akan direpresentasikan sebagai

Jumlah baru (Nomor baru (1), Jumlah baru (Nomor baru (3), Nomor baru (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
}

Selain itu, menambahkan kelas Prod baru tidak memerlukan perubahan apa pun pada kode yang sudah ada:

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

Sebaliknya, menambahkan metode baru membutuhkan modifikasi semua kelas yang ada.

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(")")
  }
}

Masalah yang sama diselesaikan dengan kelas kasus.

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

Menambahkan metode baru adalah perubahan lokal.

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(")")
    }
  }
}

Menambahkan kelas Prod baru membutuhkan kemungkinan mengubah semua pencocokan pola.

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) => ...
  }
}

Transkrip dari videolecture 4.6 Pencocokan Pola

Kedua desain ini baik-baik saja dan memilih di antara keduanya terkadang merupakan masalah gaya, namun demikian ada beberapa kriteria yang penting.

Salah satu kriterianya adalah, apakah Anda lebih sering membuat subkelas ekspresi baru atau lebih sering membuat metode baru? Jadi, ini adalah kriteria yang melihat perpanjangan masa depan dan kemungkinan perpanjangan sistem Anda.

Jika apa yang Anda lakukan kebanyakan membuat subclass baru, maka sebenarnya solusi dekomposisi berorientasi objek lebih unggul. Alasannya adalah sangat mudah dan perubahan yang sangat lokal untuk hanya membuat subclass baru dengan metode eval , dimana seperti dalam solusi fungsional, Anda harus kembali dan mengubah kode di dalam metode eval dan menambahkan kasus baru untuk itu.

Di sisi lain, jika yang Anda lakukan adalah membuat banyak metode baru, tetapi hierarki kelas itu sendiri akan dijaga agar relatif stabil, maka pencocokan pola sebenarnya menguntungkan. Karena, sekali lagi, setiap metode baru dalam solusi pencocokan pola hanyalah perubahan lokal , baik Anda meletakkannya di kelas dasar, atau bahkan mungkin di luar hierarki kelas. Sedangkan metode baru seperti show dalam dekomposisi berorientasi objek akan membutuhkan incrementasi baru pada setiap sub kelas. Jadi akan ada lebih banyak bagian, Yang harus Anda sentuh.

Jadi masalah ekstensibilitas ini dalam dua dimensi, di mana Anda mungkin ingin menambahkan kelas baru ke hierarki, atau Anda mungkin ingin menambahkan metode baru, atau mungkin keduanya, telah dinamai masalah ekspresi .

Ingat: kita harus menggunakan ini sebagai titik awal dan bukan sebagai satu-satunya kriteria.

masukkan deskripsi gambar di sini

gabrielgiussi
sumber
0

Saya mengutip ini dari Scala cookbookoleh Alvin Alexanderpasal 6: objects.

Ini adalah salah satu dari banyak hal yang menurut saya menarik dalam buku ini.

Untuk menyediakan beberapa konstruktor untuk kelas kasus, penting untuk mengetahui apa yang sebenarnya dilakukan deklarasi kelas kasus.

case class Person (var name: String)

Jika Anda melihat kode yang dihasilkan kompilator Scala untuk contoh kelas kasus, Anda akan melihat bahwa kode tersebut membuat dua file keluaran, Person $ .class dan Person.class. Jika Anda membongkar Person $ .class dengan perintah javap, Anda akan melihat bahwa ini berisi metode terapan, bersama dengan banyak metode lainnya:

$ 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);
    }

Anda juga dapat membongkar Person.class untuk melihat isinya. Untuk kelas sederhana seperti ini, ini berisi 20 metode tambahan; pembengkakan tersembunyi ini adalah salah satu alasan beberapa pengembang tidak menyukai kelas kasus.

arglee
sumber