Apa keuntungan menggunakan kelas abstrak alih-alih sifat?

371

Apa keuntungan menggunakan kelas abstrak alih-alih suatu sifat (terlepas dari kinerja)? Sepertinya kelas abstrak dapat digantikan oleh ciri-ciri dalam banyak kasus.

Ralf
sumber

Jawaban:

371

Saya dapat memikirkan dua perbedaan

  1. Kelas abstrak dapat memiliki parameter konstruktor dan juga parameter tipe. Ciri hanya dapat memiliki parameter tipe. Ada beberapa diskusi yang di masa depan bahkan sifat dapat memiliki parameter konstruktor
  2. Kelas abstrak sepenuhnya dapat dioperasikan dengan Java. Anda dapat memanggil mereka dari kode Java tanpa pembungkus apa pun. Ciri-ciri sepenuhnya dapat dioperasikan hanya jika mereka tidak mengandung kode implementasi apa pun
Mushtaq Ahmed
sumber
172
Tambahan yang sangat penting: Suatu kelas dapat mewarisi dari banyak sifat tetapi hanya satu kelas abstrak. Saya pikir ini harus menjadi pertanyaan pertama yang diajukan pengembang ketika mempertimbangkan mana yang akan digunakan di hampir semua kasus.
BAR
15
penyelamat: "Ciri-ciri sepenuhnya dapat dioperasikan hanya jika mereka tidak mengandung kode implementasi"
Walrus the Cat
2
abstrak - ketika perilaku kolektif mendefinisikan atau mengarah ke objek (cabang objek) tetapi masih belum dikomposisikan sebagai objek (siap). Ciri-ciri, ketika Anda perlu menginduksi kemampuan yaitu kemampuan tidak pernah berasal dengan penciptaan objek, itu berkembang atau diperlukan ketika suatu objek keluar dari isolasi dan harus berkomunikasi.
Ramiz Uddin
5
Perbedaan kedua tidak ada di Java8, pikirkan.
Duong Nguyen
14
Per Scala 2.12, suatu sifat mengkompilasi ke antarmuka Java 8 - scala-lang.org/news/2.12.0#traits-compile-to-interfaces .
Kevin Meredith
209

Ada bagian dalam Pemrograman dalam Scala yang disebut "To trait, or not to trait?" yang membahas pertanyaan ini. Karena edisi pertama tersedia online, saya harap tidak masalah mengutip semuanya di sini. (Setiap programmer Scala yang serius harus membeli buku itu):

Setiap kali Anda menerapkan kumpulan perilaku yang dapat digunakan kembali, Anda harus memutuskan apakah Anda ingin menggunakan sifat atau kelas abstrak. Tidak ada aturan yang pasti, tetapi bagian ini berisi beberapa pedoman untuk dipertimbangkan.

Jika perilakunya tidak akan digunakan kembali , maka buatlah kelas yang konkret. Bagaimanapun juga, itu bukan perilaku yang dapat digunakan kembali.

Jika itu dapat digunakan kembali dalam beberapa, kelas yang tidak terkait , buatlah suatu sifat. Hanya sifat-sifat yang dapat dicampur ke berbagai bagian hirarki kelas.

Jika Anda ingin mewarisi darinya dalam kode Java , gunakan kelas abstrak. Karena sifat-sifat dengan kode tidak memiliki analog Java yang dekat, itu cenderung canggung untuk mewarisi dari sifat dalam kelas Java. Mewarisi dari kelas Scala, sementara itu, persis seperti mewarisi dari kelas Java. Sebagai satu pengecualian, sifat Scala dengan anggota abstrak hanya menerjemahkan langsung ke antarmuka Java, jadi Anda harus merasa bebas untuk mendefinisikan sifat-sifat tersebut bahkan jika Anda mengharapkan kode Java untuk mewarisi darinya. Lihat Bab 29 untuk informasi lebih lanjut tentang bekerja dengan Java dan Scala bersama.

Jika Anda berencana untuk mendistribusikannya dalam bentuk yang dikompilasi , dan Anda mengharapkan grup luar untuk menulis kelas yang mewarisi darinya, Anda mungkin cenderung menggunakan kelas abstrak. Masalahnya adalah ketika suatu sifat mendapatkan atau kehilangan anggota, setiap kelas yang mewarisinya harus dikompilasi ulang, bahkan jika mereka belum berubah. Jika klien luar hanya akan memanggil perilaku, alih-alih mewarisinya, maka menggunakan sifat baik-baik saja.

Jika efisiensi sangat penting , condong ke arah menggunakan kelas. Kebanyakan Java runtimes membuat doa metode virtual anggota kelas menjadi operasi yang lebih cepat daripada doa metode antarmuka. Ciri-ciri dikompilasi ke antarmuka dan karenanya dapat membayar sedikit overhead kinerja. Namun, Anda harus membuat pilihan ini hanya jika Anda tahu bahwa sifat tersebut merupakan hambatan kinerja dan memiliki bukti bahwa menggunakan kelas bukannya benar-benar menyelesaikan masalah.

Jika Anda masih belum tahu , setelah mempertimbangkan hal di atas, maka mulailah dengan menjadikannya sebagai sifat. Anda selalu dapat mengubahnya nanti, dan secara umum menggunakan suatu sifat membuat lebih banyak opsi terbuka.

Seperti @Mushtaq Ahmed sebutkan, suatu sifat tidak dapat memiliki parameter apa pun yang diteruskan ke konstruktor utama suatu kelas.

Perbedaan lain adalah perawatan super.

Perbedaan lain antara kelas dan sifat adalah bahwa sedangkan di kelas, superpanggilan terikat secara statis, dalam sifat, mereka terikat secara dinamis. Jika Anda menulis super.toStringdi kelas, Anda tahu persis implementasi metode mana yang akan dipanggil. Ketika Anda menulis hal yang sama dalam suatu sifat, implementasi metode untuk memanggil panggilan super tidak ditentukan saat Anda mendefinisikan sifat tersebut.

Lihat sisa Bab 12 untuk lebih jelasnya.

Edit 1 (2013):

Ada perbedaan kecil dalam perilaku kelas abstrak dibandingkan dengan sifat. Salah satu aturan linierisasi adalah bahwa ia mempertahankan hierarki warisan kelas-kelas, yang cenderung mendorong kelas-kelas abstrak belakangan dalam rantai sementara sifat-sifat dapat dengan senang hati dicampurkan. Dalam keadaan tertentu, sebenarnya lebih disukai berada pada posisi terakhir dari linierisasi kelas. , jadi kelas abstrak bisa digunakan untuk itu. Lihat membatasi linierisasi kelas (urutan mixin) di Scala .

Edit 2 (2018):

Pada Scala 2.12, perilaku kompatibilitas biner sifat telah berubah. Sebelum 2.12, menambahkan atau menghapus anggota ke sifat diperlukan kompilasi ulang semua kelas yang mewarisi sifat itu, bahkan jika kelas tidak berubah. Ini karena cara pengkodean dalam JVM.

Pada Scala 2.12, ciri mengkompilasi ke antarmuka Java , sehingga persyaratan telah sedikit santai. Jika sifat melakukan salah satu dari yang berikut, subclassnya masih memerlukan kompilasi ulang:

  • mendefinisikan bidang ( valatau var, tetapi konstanta ok - final valtanpa tipe hasil)
  • panggilan super
  • pernyataan inisialisasi dalam tubuh
  • memperluas kelas
  • mengandalkan linierisasi untuk menemukan implementasi di supertrait yang tepat

Tetapi jika sifat itu tidak, Anda sekarang dapat memperbaruinya tanpa merusak kompatibilitas biner.

Eugene Yokota
sumber
2
If outside clients will only call into the behavior, instead of inheriting from it, then using a trait is fine- Bisakah seseorang menjelaskan apa bedanya di sini? extendsvs with?
0fnt
2
@ 0fnt Perbedaannya bukan tentang memanjang vs dengan. Apa yang dia katakan adalah bahwa jika Anda hanya mencampur sifat dalam kompilasi yang sama, masalah kompatibilitas biner tidak berlaku. Namun, jika API Anda dirancang untuk memungkinkan pengguna untuk mencampurkan sifat itu sendiri, maka Anda harus khawatir tentang kompatibilitas biner.
John Colanduoni
2
@ 0fnt: Sama sekali tidak ada perbedaan semantik antara extendsdan with. Ini murni sintaksis. Jika Anda mewarisi dari banyak templat, yang pertama didapat extend, semua yang lain dapatkan with, itu saja. Pikirkan withsebagai koma: class Foo extends Bar, Baz, Qux.
Jörg W Mittag
77

Untuk apa pun nilainya, Pemrograman Odersky et al di Scala merekomendasikan bahwa, ketika Anda ragu, Anda menggunakan sifat-sifat. Anda selalu dapat mengubahnya menjadi kelas abstrak nanti jika diperlukan.

Daniel C. Sobral
sumber
20

Selain fakta bahwa Anda tidak dapat secara langsung memperluas beberapa kelas abstrak, tetapi Anda dapat menggabungkan beberapa sifat ke dalam kelas, perlu disebutkan bahwa sifat dapat ditumpuk, karena panggilan super dalam suatu sifat terikat secara dinamis (merujuk pada suatu kelas atau sifat yang dicampur sebelum saat ini).

Dari jawaban Thomas dalam Perbedaan antara Kelas Abstrak dan Sifat :

trait A{
    def a = 1
}

trait X extends A{
    override def a = {
        println("X")
        super.a
    }
}  


trait Y extends A{
    override def a = {
        println("Y")
        super.a
    }
}

scala> val xy = new AnyRef with X with Y
xy: java.lang.Object with X with Y = $anon$1@6e9b6a
scala> xy.a
Y
X
res0: Int = 1

scala> val yx = new AnyRef with Y with X
yx: java.lang.Object with Y with X = $anon$1@188c838
scala> yx.a
X
Y
res1: Int = 1
Nemanja Boric
sumber
9

Ketika memperluas kelas abstrak, ini menunjukkan bahwa subclass adalah sejenis. Ini tidak selalu terjadi ketika menggunakan ciri-ciri, saya pikir.

peter p
sumber
Apakah ini memiliki implikasi praktis, atau hanya membuat kode lebih mudah dipahami?
Ralf
8

Dalam Pemrograman Scala , penulis mengatakan bahwa kelas abstrak membuat hubungan objek "is-a" klasik berorientasi sedangkan sifat adalah scala-cara komposisi.

Kukus
sumber
5

Kelas abstrak dapat berisi perilaku - Mereka dapat parameter dengan konstruktor args (yang tidak dapat ciri) dan mewakili entitas kerja. Alih-alih sifat hanya mewakili satu fitur, antarmuka dari satu fungsi.

Dario
sumber
8
Semoga Anda tidak menyiratkan bahwa sifat tidak dapat mengandung perilaku. Keduanya dapat berisi kode implementasi.
Mitch Blevins
1
@ Nyonya Blevins: Tentu saja tidak. Mereka dapat berisi kode, tetapi ketika Anda mendefinisikan trait Enumerabledengan banyak fungsi pembantu, saya tidak akan menyebutnya perilaku tetapi hanya fungsionalitas yang terhubung dengan satu fitur.
Dario
4
@Dario Saya melihat "perilaku" dan "fungsionalitas" sebagai sinonim, jadi saya menemukan jawaban Anda sangat membingungkan.
David J.
3
  1. Kelas dapat mewarisi dari beberapa sifat tetapi hanya satu kelas abstrak.
  2. Kelas abstrak dapat memiliki parameter konstruktor dan juga parameter tipe. Ciri hanya dapat memiliki parameter tipe. Misalnya, Anda tidak bisa mengatakan sifat t (i: Int) {}; parameter i adalah ilegal.
  3. Kelas abstrak sepenuhnya dapat dioperasikan dengan Java. Anda dapat memanggil mereka dari kode Java tanpa pembungkus apa pun. Ciri-ciri sepenuhnya dapat dioperasikan hanya jika mereka tidak mengandung kode implementasi apa pun.
pavan.vn101
sumber