warisan ruby ​​vs mixins

127

Di Ruby, karena Anda dapat menyertakan beberapa mixin tetapi hanya memperpanjang satu kelas, sepertinya mixin akan lebih disukai daripada warisan.

Pertanyaan saya: jika Anda menulis kode yang harus diperluas / dimasukkan agar bermanfaat, mengapa Anda membuatnya menjadi kelas? Atau dengan kata lain, mengapa Anda tidak selalu membuatnya menjadi modul?

Saya hanya bisa memikirkan satu alasan mengapa Anda ingin kelas, dan itu adalah jika Anda perlu membuat instance kelas. Dalam kasus ActiveRecord :: Base, Anda tidak pernah membuat instantiate secara langsung. Jadi bukankah itu seharusnya modul?

Brad Cupit
sumber

Jawaban:

176

Saya baru saja membaca tentang topik ini di The Well-Grounded Rubyist (buku bagus, omong-omong). Penulis melakukan pekerjaan menjelaskan dengan lebih baik daripada saya, jadi saya akan mengutipnya:


Tidak ada aturan atau formula tunggal yang selalu menghasilkan desain yang tepat. Tetapi penting untuk mengingat beberapa pertimbangan saat Anda membuat keputusan kelas-versus-modul:

  • Modul tidak memiliki instance. Oleh karena itu entitas atau hal-hal umumnya dimodelkan terbaik di kelas, dan karakteristik atau properti entitas atau hal-hal terbaik dikemas dalam modul. Sejalan dengan itu, sebagaimana dicatat dalam bagian 4.1.1, nama kelas cenderung menjadi kata benda, sedangkan nama modul sering merupakan kata sifat (Stack versus Stacklike).

  • Kelas hanya dapat memiliki satu superclass, tetapi ia dapat mencampur modul sebanyak yang diinginkan. Jika Anda menggunakan warisan, berikan prioritas untuk menciptakan hubungan superclass / subclass yang masuk akal. Jangan menggunakan hubungan satu-satunya kelas superclass untuk memberkahi kelas dengan apa yang bisa berubah menjadi hanya satu dari beberapa set karakteristik.

Ringkas aturan-aturan ini dalam satu contoh, inilah yang tidak boleh Anda lakukan:

module Vehicle 
... 
class SelfPropelling 
... 
class Truck < SelfPropelling 
  include Vehicle 
... 

Sebaliknya, Anda harus melakukan ini:

module SelfPropelling 
... 
class Vehicle 
  include SelfPropelling 
... 
class Truck < Vehicle 
... 

Versi kedua memodelkan entitas dan properti jauh lebih rapi. Truk turun dari Kendaraan (yang masuk akal), sedangkan SelfPropelling adalah karakteristik kendaraan (setidaknya, semua yang kita pedulikan dalam model dunia ini) —karakteristik yang diteruskan ke truk berdasarkan truk sebagai keturunan, atau bentuk khusus, Kendaraan.

Andy Gaskell
sumber
1
Contoh menunjukkan dengan rapi - Truk ADALAH Kendaraan - tidak ada Truk yang tidak akan menjadi Kendaraan.
PL J
1
Contoh menunjukkan dengan rapi - TruckIS A Vehicle- tidak ada Truckyang tidak akan menjadi Vehicle. Namun saya akan memanggil modul mungkin SelfPropelable(:?) Hmm SelfPropeledkedengarannya benar, tetapi hampir sama: D. Lagi pula saya tidak akan memasukkannya ke Vehicledalam Truck- karena ada Kendaraan yang TIDAK SelfPropeled. Juga indikasi yang baik untuk bertanya - apakah ada Hal-hal lain, BUKAN Kendaraan yang ADALAH SelfPropeled? - Yah mungkin, tapi aku akan lebih sulit ditemukan. Jadi Vehiclebisa mewarisi dari SelfPropelling kelas (sebagai kelas itu tidak akan cocok SelfPropeled- karena lebih dari peran)
PL J
39

Saya pikir mixin adalah ide bagus, tapi ada masalah lain di sini yang tidak seorang pun sebutkan: tabrakan namespace. Mempertimbangkan:

module A
  HELLO = "hi"
  def sayhi
    puts HELLO
  end
end

module B
  HELLO = "you stink"
  def sayhi
    puts HELLO
  end
end

class C
  include A
  include B
end

c = C.new
c.sayhi

Yang mana yang menang? Di Ruby, ternyata yang terakhir module B,, karena Anda memasukkannya setelah module A. Sekarang, sangat mudah untuk menghindari masalah ini: pastikan semua module Adan module B's konstanta dan metode dalam ruang nama tidak mungkin. Masalahnya adalah bahwa kompiler tidak memperingatkan Anda sama sekali ketika tabrakan terjadi.

Saya berpendapat bahwa perilaku ini tidak skala ke tim besar programmer - Anda tidak boleh berasumsi bahwa orang yang menerapkan class Ctahu tentang setiap nama dalam ruang lingkup. Ruby bahkan akan membiarkan Anda mengganti konstanta atau metode dari tipe yang berbeda . Saya tidak yakin yang bisa pernah dianggap perilaku yang benar.

Dan Barowy
sumber
2
Ini adalah kata hati yang bijaksana. Mengingatkan pada beberapa jebakan warisan di C ++.
Chris Tonkinson
1
Apakah ada mitigasi yang baik untuk ini? Ini terlihat seperti alasan mengapa pewarisan berganda Python adalah solusi yang unggul (tidak mencoba memulai kecocokan p * bahasa; hanya membandingkan fitur spesifik ini).
Marcin
1
@ Bazz Itu hebat dan semuanya, tetapi komposisi dalam kebanyakan bahasa rumit. Ini juga sebagian besar relevan dalam bahasa yang diketik bebek. Ini juga tidak menjamin bahwa Anda tidak mendapatkan status aneh.
Marcin
Posting lama, saya tahu, tetapi masih dalam pencarian. Jawabannya sebagian salah - C#sayhimenghasilkan B::HELLObukan karena Ruby mencampur konstanta, tetapi karena ruby ​​menyelesaikan konstanta dari lebih dekat ke jauh - jadi HELLOdirujuk di Bakan selalu diselesaikan B::HELLO. Ini berlaku bahkan jika kelas C mendefinisikannya sendiri C::HELLOjuga.
Laas
13

Pilihan saya: Modul adalah untuk berbagi perilaku, sedangkan kelas untuk memodelkan hubungan antar objek. Secara teknis Anda bisa menjadikan semuanya sebagai instance dari objek dan menggabungkan modul apa pun yang Anda inginkan untuk mendapatkan serangkaian perilaku yang diinginkan, tetapi itu akan menjadi desain yang buruk, serampangan, dan agak tidak terbaca.

Membuang
sumber
2
Ini menjawab pertanyaan dengan cara langsung: pewarisan memberlakukan struktur organisasi tertentu yang dapat membuat proyek Anda lebih mudah dibaca.
Amril
10

Jawaban atas pertanyaan Anda sebagian besar kontekstual. Menyaring pengamatan pubb, pilihan terutama didorong oleh domain yang dipertimbangkan.

Dan ya, ActiveRecord seharusnya dimasukkan daripada diperluas oleh subkelas. ORM lain - datamapper - justru mencapai itu!

nareshb
sumber
4

Saya sangat menyukai jawaban Andy Gaskell - hanya ingin menambahkan bahwa ya, ActiveRecord tidak boleh menggunakan pewarisan, melainkan menyertakan modul untuk menambahkan perilaku (kebanyakan kegigihan) ke model / kelas. ActiveRecord hanya menggunakan paradigma yang salah.

Untuk alasan yang sama, saya sangat menyukai MongoId daripada MongoMapper, karena membuat pengembang berkesempatan untuk menggunakan warisan sebagai cara memodelkan sesuatu yang bermakna dalam domain masalah.

Sangat menyedihkan bahwa tidak ada seorang pun di komunitas Rails yang menggunakan "Ruby inheritance" seperti yang seharusnya digunakan - untuk mendefinisikan hierarki kelas, bukan hanya untuk menambahkan perilaku.

Tilo
sumber
1

Cara terbaik saya memahami mixin adalah sebagai kelas virtual. Mixin adalah "kelas virtual" yang telah disuntikkan dalam rantai leluhur kelas atau modul.

Ketika kita menggunakan "include" dan memberikannya sebuah modul, modul itu menambahkan modul ke rantai leluhur tepat sebelum kelas yang kita warisi dari:

class Parent
end 

module M
end

class Child < Parent
  include M
end

Child.ancestors
 => [Child, M, Parent, Object ...

Setiap objek di Ruby juga memiliki kelas tunggal. Metode yang ditambahkan ke kelas singleton ini dapat langsung dipanggil pada objek dan karenanya mereka bertindak sebagai metode "kelas". Ketika kita menggunakan "extended" pada objek dan meneruskan objek modul, kami menambahkan metode modul ke kelas tunggal objek:

module M
  def m
    puts 'm'
  end
end

class Test
end

Test.extend M
Test.m

Kita dapat mengakses kelas singleton dengan metode singleton_class:

Test.singleton_class.ancestors
 => [#<Class:Test>, M, #<Class:Object>, ...

Ruby menyediakan beberapa kait untuk modul ketika mereka dicampur ke dalam kelas / modul. includedadalah metode kait yang disediakan oleh Ruby yang dipanggil setiap kali Anda memasukkan modul dalam beberapa modul atau kelas. Sama seperti yang disertakan, ada extendedkait terkait untuk memperpanjang. Ini akan dipanggil ketika modul diperluas oleh modul atau kelas lain.

module M
  def self.included(target)
    puts "included into #{target}"
  end

  def self.extended(target)
    puts "extended into #{target}"
  end
end

class MyClass
  include M
end

class MyClass2
  extend M
end

Ini menciptakan pola menarik yang dapat digunakan pengembang:

module M
  def self.included(target)
    target.send(:include, InstanceMethods)
    target.extend ClassMethods
    target.class_eval do
      a_class_method
    end
  end

  module InstanceMethods
    def an_instance_method
    end
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end

class MyClass
  include M
  # a_class_method called
end

Seperti yang Anda lihat, modul tunggal ini menambahkan metode instance, metode "class", dan bertindak langsung pada kelas target (memanggil a_class_method () dalam kasus ini).

ActiveSupport :: Concern merangkum pola ini. Berikut modul yang sama ditulis ulang untuk menggunakan ActiveSupport :: Concern:

module M
  extend ActiveSupport::Concern

  included do
    a_class_method
  end

  def an_instance_method
  end

  module ClassMethods
    def a_class_method
      puts "a_class_method called"
    end
  end
end
Donato
sumber
-1

Saat ini, saya sedang memikirkan templatepola desain. Itu tidak akan terasa benar dengan modul.

Geo
sumber