Pemrograman Meta Ruby Bahkan Lebih Keren Dari Kedengarannya

Diterbitkan: 2022-03-11

Anda sering mendengar bahwa metaprogramming adalah sesuatu yang hanya digunakan oleh ninja Ruby, dan itu bukan untuk manusia biasa. Tetapi kenyataannya adalah bahwa metaprogramming bukanlah sesuatu yang menakutkan sama sekali. Postingan blog ini akan menantang pemikiran seperti ini dan untuk mendekatkan metaprogramming dengan rata-rata pengembang Ruby sehingga mereka juga dapat menuai manfaatnya.

Ruby Metaprogramming: Kode Penulisan Kode
Menciak

Perlu dicatat bahwa metaprogramming bisa sangat berarti dan sering kali bisa sangat disalahgunakan dan menjadi ekstrem dalam hal penggunaan, jadi saya akan mencoba memberikan beberapa contoh dunia nyata yang dapat digunakan semua orang dalam pemrograman sehari-hari.

Pemrograman meta

Metaprogramming adalah teknik di mana Anda dapat menulis kode yang menulis kode dengan sendirinya secara dinamis saat runtime. Ini berarti Anda dapat menentukan metode dan kelas selama runtime. Gila, kan? Singkatnya, menggunakan metaprogramming Anda dapat membuka kembali dan memodifikasi kelas, menangkap metode yang tidak ada dan membuatnya dengan cepat, membuat kode yang KERING dengan menghindari pengulangan, dan banyak lagi.

Dasar

Sebelum kita menyelami metaprogramming yang serius, kita harus menjelajahi dasar-dasarnya. Dan cara terbaik untuk melakukannya adalah dengan memberi contoh. Mari kita mulai dengan satu dan memahami metaprogramming Ruby langkah demi langkah. Anda mungkin dapat menebak apa yang dilakukan kode ini:

 class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end

Kami telah mendefinisikan kelas dengan dua metode. Metode pertama di kelas ini adalah metode kelas dan yang kedua adalah metode instan. Ini adalah hal dasar di Ruby, tetapi ada lebih banyak lagi yang terjadi di balik kode ini yang perlu kita pahami sebelum melangkah lebih jauh. Perlu ditunjukkan bahwa kelas Developer itu sendiri sebenarnya adalah sebuah objek. Di Ruby semuanya adalah objek, termasuk kelas. Karena Developer adalah sebuah instance, itu adalah sebuah instance dari class Class . Berikut adalah bagaimana model objek Ruby terlihat:

Model objek rubi

 p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject

Satu hal yang penting untuk dipahami disini adalah arti dari self . Metode frontend adalah metode reguler yang tersedia pada instance kelas Developer , tetapi mengapa metode backend merupakan metode kelas? Setiap bagian kode yang dieksekusi di Ruby dieksekusi terhadap diri tertentu . Ketika penerjemah Ruby mengeksekusi kode apa pun, ia selalu melacak nilai self untuk setiap baris yang diberikan. self selalu merujuk ke beberapa objek tetapi objek itu dapat berubah berdasarkan kode yang dieksekusi. Misalnya, di dalam definisi kelas, self merujuk ke kelas itu sendiri yang merupakan turunan dari class Class .

 class Developer p self end # Developer

Di dalam metode instance, self merujuk ke instance kelas.

 class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>

Di dalam metode kelas, self merujuk ke kelas itu sendiri (yang akan dibahas lebih detail nanti di artikel ini):

 class Developer def self.backend self end end p Developer.backend # Developer

Ini baik-baik saja, tetapi apa itu metode kelas? Sebelum menjawab pertanyaan tersebut perlu disebutkan adanya sesuatu yang disebut metaclass, juga dikenal sebagai singleton class dan eigenclass. frontend metode kelas yang kita definisikan sebelumnya tidak lain adalah metode instans yang didefinisikan dalam metaclass untuk objek Developer ! Metaclass pada dasarnya adalah kelas yang dibuat dan disisipkan Ruby ke dalam hierarki pewarisan untuk menampung metode kelas, sehingga tidak mengganggu instance yang dibuat dari kelas.

kelas meta

Setiap objek di Ruby memiliki metaclass sendiri. Entah bagaimana tidak terlihat oleh pengembang, tetapi ada dan Anda dapat menggunakannya dengan sangat mudah. Karena Developer kelas kami pada dasarnya adalah sebuah objek, ia memiliki kelas meta sendiri. Sebagai contoh, mari kita buat objek dari kelas String dan manipulasi metaclass-nya:

 example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT

Apa yang kami lakukan di sini adalah kami menambahkan metode tunggal something ke objek. Perbedaan antara metode kelas dan metode tunggal adalah bahwa metode kelas tersedia untuk semua instance dari objek kelas sementara metode tunggal hanya tersedia untuk instance tunggal itu. Metode kelas banyak digunakan sementara metode tunggal tidak begitu banyak, tetapi kedua jenis metode ditambahkan ke metaclass dari objek itu.

Contoh sebelumnya dapat ditulis ulang seperti ini:

 example = "I'm a string object" class << example def something self.upcase end end

Sintaksnya berbeda tetapi secara efektif melakukan hal yang sama. Sekarang mari kembali ke contoh sebelumnya di mana kita membuat kelas Developer dan menjelajahi beberapa sintaks lain untuk mendefinisikan metode kelas:

 class Developer def self.backend "I am backend developer" end end

Ini adalah definisi dasar yang digunakan hampir semua orang.

 def Developer.backend "I am backend developer" end

Ini adalah hal yang sama, kami mendefinisikan metode kelas backend untuk Developer . Kami tidak menggunakan self tetapi mendefinisikan metode seperti ini secara efektif menjadikannya metode kelas.

 class Developer class << self def backend "I am backend developer" end end end

Sekali lagi, kami mendefinisikan metode kelas, tetapi menggunakan sintaks yang mirip dengan yang kami gunakan untuk mendefinisikan metode tunggal untuk objek String . Anda mungkin memperhatikan bahwa kami menggunakan self di sini yang merujuk ke objek Developer itu sendiri. Pertama kita buka kelas Developer , membuat self sama dengan kelas Developer . Selanjutnya, kita melakukan class << self , membuat self sama dengan metaclass Developer . Kemudian kita mendefinisikan backend metode pada metaclass Developer .

 class << Developer def backend "I am backend developer" end end

Dengan mendefinisikan blok seperti ini, kita menyetel self ke metaclass Developer selama durasi blok. Akibatnya, metode backend ditambahkan ke metaclass Developer , bukan kelas itu sendiri.

Mari kita lihat bagaimana metaclass ini berperilaku di pohon warisan:

Metaclass di pohon warisan

Seperti yang Anda lihat di contoh sebelumnya, tidak ada bukti nyata bahwa metaclass ada. Tetapi kita dapat menggunakan sedikit peretasan yang dapat menunjukkan kepada kita keberadaan kelas tak kasat mata ini:

 class Object def metaclass_example class << self self end end end

Jika kita mendefinisikan metode instance di kelas Object (ya, kita dapat membuka kembali kelas apa pun kapan saja, itu adalah keindahan lain dari metaprogramming), kita akan memiliki self yang merujuk ke objek Object di dalamnya. Kita kemudian dapat menggunakan sintaks class << self untuk mengubah self saat ini untuk menunjuk ke metaclass dari objek saat ini. Karena objek saat ini adalah kelas Object itu sendiri, ini akan menjadi kelas meta instance. Metode ini mengembalikan self yang pada titik ini merupakan metaclass itu sendiri. Jadi dengan memanggil metode instance ini pada objek apa pun, kita bisa mendapatkan metaclass dari objek itu. Mari kita definisikan kembali kelas Developer kita dan mulai menjelajah sedikit:

 class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"

Dan untuk crescendo, mari kita lihat bukti bahwa frontend adalah metode instance dari kelas dan backend adalah metode instance dari metaclass:

 p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]

Meskipun, untuk mendapatkan metaclass Anda tidak perlu benar-benar membuka kembali Object dan menambahkan peretasan ini. Anda dapat menggunakan singleton_class yang disediakan Ruby. Ini sama dengan metaclass_example yang kami tambahkan tetapi dengan peretasan ini Anda benar-benar dapat melihat cara kerja Ruby di bawah tenda:

 p developer.class.singleton_class.instance_methods false # [:backend]

Mendefinisikan Metode Menggunakan “class_eval” dan “instance_eval”

Ada satu cara lagi untuk membuat metode kelas, yaitu dengan menggunakan instance_eval :

 class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"

Potongan kode Ruby interpreter ini mengevaluasi dalam konteks sebuah instance, yang dalam hal ini adalah objek Developer . Dan ketika Anda mendefinisikan metode pada objek, Anda membuat metode kelas atau metode tunggal. Dalam hal ini adalah metode kelas - tepatnya, metode kelas adalah metode tunggal tetapi metode tunggal dari suatu kelas, sedangkan yang lain adalah metode tunggal dari suatu objek.

Di sisi lain, class_eval mengevaluasi kode dalam konteks kelas, bukan instance. Ini praktis membuka kembali kelas. Berikut adalah bagaimana class_eval dapat digunakan untuk membuat metode instance:

 Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"

Untuk meringkas, ketika Anda memanggil metode class_eval , Anda mengubah self untuk merujuk ke kelas asli dan ketika Anda memanggil instance_eval , self berubah untuk merujuk ke metaclass kelas asli.

Mendefinisikan Metode yang Hilang dengan Cepat

Satu lagi teka-teki metaprogramming adalah method_missing . Saat Anda memanggil metode pada suatu objek, Ruby pertama-tama masuk ke kelas dan menelusuri metode instance-nya. Jika tidak menemukan metode di sana, ia melanjutkan pencarian di rantai leluhur. Jika Ruby masih tidak menemukan metode tersebut, ia akan memanggil metode lain bernama method_missing yang merupakan metode turunan Kernel yang diwarisi oleh setiap objek. Karena kami yakin Ruby akan memanggil metode ini pada akhirnya untuk metode yang hilang, kami dapat menggunakan ini untuk menerapkan beberapa trik.

define_method adalah metode yang didefinisikan dalam kelas Module yang dapat Anda gunakan untuk membuat metode secara dinamis. Untuk menggunakan define_method , Anda memanggilnya dengan nama metode baru dan blok di mana parameter blok menjadi parameter metode baru. Apa perbedaan antara menggunakan def untuk membuat metode dan define_method ? Tidak ada banyak perbedaan kecuali Anda dapat menggunakan define_method dalam kombinasi dengan method_missing untuk menulis kode KERING. Tepatnya, Anda dapat menggunakan define_method alih-alih def untuk memanipulasi cakupan saat mendefinisikan kelas, tetapi itu adalah cerita lain. Mari kita lihat contoh sederhana:

 class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"

Ini menunjukkan bagaimana define_method digunakan untuk membuat metode instance tanpa menggunakan def . Namun, masih banyak lagi yang bisa kita lakukan dengan mereka. Mari kita lihat cuplikan kode ini:

 class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"

Kode ini tidak KERING, tetapi menggunakan define_method kita dapat membuatnya KERING:

 class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"

Itu jauh lebih baik, tapi masih belum sempurna. Mengapa? Jika kita ingin menambahkan metode baru coding_debug misalnya, kita perlu memasukkan "debug" ini ke dalam array. Tetapi menggunakan method_missing kita dapat memperbaikinya:

 class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug

Bagian kode ini sedikit rumit jadi mari kita uraikan. Memanggil metode yang tidak ada akan menjalankan method_missing . Di sini, kami ingin membuat metode baru hanya jika nama metode dimulai dengan "coding_" . Kalau tidak, kami hanya memanggil super untuk melakukan pekerjaan melaporkan metode yang sebenarnya hilang. Dan kami hanya menggunakan define_method untuk membuat metode baru itu. Itu dia! Dengan potongan kode ini kita dapat membuat ribuan metode baru yang dimulai dengan "coding_" , dan fakta itulah yang membuat kode kita KERING. Karena define_method bersifat pribadi untuk Module , kita perlu menggunakan send untuk menjalankannya.

Membungkus

Ini hanyalah puncak gunung es. Untuk menjadi Ruby Jedi, ini adalah titik awalnya. Setelah Anda menguasai blok bangunan metaprogramming ini dan benar-benar memahami esensinya, Anda dapat melanjutkan ke sesuatu yang lebih kompleks, misalnya membuat Domain-specific Language (DSL) Anda sendiri. DSL adalah topik tersendiri tetapi konsep dasar ini merupakan prasyarat untuk memahami topik lanjutan. Beberapa permata yang paling sering digunakan di Rails dibuat dengan cara ini dan Anda mungkin menggunakan DSL-nya tanpa menyadarinya, seperti RSpec dan ActiveRecord.

Semoga artikel ini dapat membawa Anda selangkah lebih dekat untuk memahami metaprogramming dan bahkan mungkin membangun DSL Anda sendiri, yang dapat Anda gunakan untuk membuat kode dengan lebih efisien.