🎭 Ruby Metaprogramming

Ruby Metaprogramming

Tutorial lanjutan Ruby Metaprogramming — method_missing, define_method, singleton method, eigenclass, open class, hooks, dan quiz interaktif dengan contoh kode praktis

1. Pengenalan Metaprogramming

Metaprogramming adalah kemampuan sebuah program untuk menulis atau memodifikasi program itu sendiri saat runtime. Di Ruby, metaprogramming adalah kekuatan utama yang membuat bahasa ini begitu fleksibel dan ekspresif. Framework seperti Rails menggunakan metaprogramming secara intensif untuk menciptakan DSL (Domain Specific Language) yang elegan.

Metaprogramming memungkinkan Anda membuat method secara dinamis, memodifikasi class yang sudah ada, menangkap method yang tidak didefinisikan, dan banyak lagi. Ini adalah topik lanjutan yang membutuhkan pemahaman mendalam tentang Object Model Ruby.

Mengapa Metaprogramming Penting?

Keunggulan Penjelasan
DRY CodeHindari duplikasi kode dengan generate method secara dinamis
Elegant DSLBuat sintaks yang mirip bahasa manusia seperti di Rails
FlexibilityModifikasi class dan method di runtime sesuai kebutuhan
Less BoilerplateKurangi kode repetitif dengan pattern yang dinamis
Library DesignBuat library/framework yang lebih user-friendly
⚠️ Peringatan

Metaprogramming adalah pedang bermata dua. Jika digunakan berlebihan, kode bisa menjadi sulit dibaca dan di-debug. Gunakan metaprogramming hanya ketika benar-benar dibutuhkan dan ketika solusi biasa terlalu repetitif. Selalu prioritaskan keterbacaan kode!

Diagram: Object Model Ruby
┌───────────────────────────────────────────────────────┐
│                 RUBY OBJECT MODEL                     │
│                                                       │
│  ┌─────────────┐    ┌──────────────────────────┐      │
│  │  BasicObject │    │   Singleton/Eigenclass   │      │
│  └──────┬──────┘    └──────────────────────────┘      │
│         │                   ▲                         │
│  ┌──────▼──────┐    ┌──────┴────────────────────┐     │
│  │   Object     │    │  Setiap objek punya        │     │
│  │              │    │  eigenclass sendiri         │     │
│  └──────┬──────┘    └────────────────────────────┘     │
│         │                                             │
│  ┌──────▼──────┐     ┌──────────────────────────┐     │
│  │    Module    │────>│  Class < Module          │     │
│  └──────┬──────┘     └──────────┬───────────────┘     │
│         │                      │                      │
│         │              ┌───────▼───────┐              │
│         └──────────────│   User Class   │              │
│                        │               │              │
│                        │  instance:     │              │
│                        │  @name, @age   │              │
│                        └───────────────┘              │
└───────────────────────────────────────────────────────┘

2. Objects dan Introspection

Sebelum mempelajari metaprogramming, Anda harus memahami cara Ruby merepresentasikan objects dan bagaimana melakukan introspection (menginspeksi objek).

Semua adalah Objek

Ruby — Introspection
# Di Ruby, SEMUA adalah objek (termasuk angka, string, nil)
42.class           # Integer
"hello".class      # String
true.class         # TrueClass
nil.class          # NilClass
[1,2].class        # Array
{}.class           # Hash

# class (method) — mendapatkan class dari objek
puts 42.class            # Integer
puts "hello".class       # String

# .superclass — mendapatkan parent class
puts Integer.superclass      # Numeric
puts Numeric.superclass      # Comparable
puts String.superclass       # Object

# .ancestors — seluruh chain inheritance
puts Integer.ancestors.inspect
# [Integer, Numeric, Comparable, Object, Kernel, BasicObject]

# .methods — semua method yang tersedia
puts 42.methods.sort.inspect
puts "hello".methods.grep(/length|size/).inspect
# [:length, :size]

# .instance_variables — semua instance variable
class User
  def initialize(name, age)
    @name = name
    @age = age
  end
end

user = User.new("Budi", 25)
puts user.instance_variables.inspect
# [:@name, :@age]

# .respond_to? — cek apakah objek punya method tertentu
puts 42.respond_to?(:even?)      # true
puts 42.respond_to?(:length)     # false
puts "hello".respond_to?(:length) # true

# .send — panggil method secara dinamis
puts 42.send(:to_s)              # "42"
puts "hello".send(:upcase)       # "HELLO"
puts 42.send(:+, 8)              # 50

# .send bisa memanggil method private!
class Secret
  private
  def password
    "rahasia123"
  end
end
Secret.new.send(:password)       # "rahasia123"

Method Reflection

Ruby — Method Reflection
# .method — mendapatkan Method object
m = "hello".method(:length)
puts m.call    # 5
puts m.arity   # 0 (jumlah parameter)
puts m.source_location.inspect  # ["(irb)", line_number] atau nil untuk built-in

# .defined? — cek apakah method/variabel didefinisikan
puts defined?(puts)      # "method"
puts defined?(x)         # nil (x belum didefinisikan)
puts defined?(42)        # "expression"

# .owner — menemukan dimana method didefinisikan
puts 42.method(:even?).owner    # Integer
puts "hello".method(:length).owner  # String

# .protected_methods dan .private_methods
puts User.protected_methods.inspect
puts User.private_methods.sort.inspect

# Constants
puts User.constants.inspect
puts Object.constants.length    # Banyak!

3. Open Class dan Monkey Patching

Ruby mengizinkan Anda membuka class yang sudah ada dan menambahkan atau memodifikasi method-nya. Ini disebut Open Class atau Monkey Patching. Ini sangat powerful tapi juga berbahaya jika digunakan sembarangan.

Menambah Method ke Class Bawaan

Ruby — Open Class
# Membuka kembali class Integer dan menambah method baru
class Integer
  def faktorial
    return 1 if self <= 1
    (1..self).reduce(:*)
  end

  def ganjil?
    self % 2 != 0
  end

  def genap?
    self % 2 == 0
  end

  def ke_roman
    # Konversi angka ke romawi
    roman_map = {
      1000 => "M", 900 => "CM", 500 => "D", 400 => "CD",
      100 => "C", 90 => "XC", 50 => "L", 40 => "XL",
      10 => "X", 9 => "IX", 5 => "V", 4 => "IV", 1 => "I"
    }
    n = self
    result = ""
    roman_map.each do |value, symbol|
      while n >= value
        result << symbol
        n -= value
      end
    end
    result
  end
end

puts 5.faktorial        # 120
puts 7.ganjil?          # true
puts 42.genap?          # true
puts 1994.ke_roman      # "MCMXCIV"

Modifikasi Method String

Ruby — Monkey Patch String
# Modifikasi class String
class String
  def palindrome?
    clean = self.downcase.gsub(/[^a-z0-9]/, '')
    clean == clean.reverse
  end

  def word_count
    split(/\s+/).length
  end

  def ke_kategori
    case self.length
    when 0..5    then "pendek"
    when 6..15   then "sedang"
    else              "panjang"
    end
  end

  def truncate_words(max, suffix = "...")
    words = split(/\s+/)
    if words.length > max
      words[0...max].join(" ") + suffix
    else
      self
    end
  end
end

puts "Kasur rusak".palindrome?      # true
puts "A man a plan a canal Panama".palindrome?  # true
puts "Hello world program".word_count  # 3
puts "Python".ke_kategori              # "sedang"
puts "Ini adalah kalimat yang sangat panjang".truncate_words(4)
# "Ini adalah kalimat yang..."
⚠️ Monkey Patching Best Practices

Saat menggunakan monkey patching, selalu gunakan prepend dan super untuk mempertahankan behavior original. Gunakan Refinements di Ruby 2.0+ untuk scope yang lebih terbatas agar patch hanya berlaku di file tertentu.

Refinements — Monkey Patching yang Aman

Ruby — Refinements
# Refinements — membatasi scope monkey patch
module StringExtensions
  refine String do
    def shout
      upcase + "!"
    end

    def whisper
      downcase + "..."
    end
  end
end

# Hanya aktif di dalam file/scope ini
using StringExtensions

puts "hello".shout     # "HELLO!"
puts "HEY".whisper     # "hey..."

# Di luar scope ini, method .shout tidak tersedia
# Ini membuat monkey patching lebih aman

4. method_missing

method_missing adalah salah satu metaprogramming paling powerful di Ruby. Method ini dipanggil otomatis ketika Anda memanggil method yang tidak didefinisikan pada sebuah objek. Rails menggunakan method_missing secara masif untuk fitur seperti dynamic finders (find_by_name).

Basic method_missing

Ruby — method_missing
# Basic method_missing — menangkap method yang tidak didefinisikan
class Greeter
  def method_missing(method_name, *args)
    puts "Method '#{method_name}' belum didefinisikan!"
    puts "Args: #{args.inspect}" if args.any?
  end
end

g = Greeter.new
g.hello_world
# Method 'hello_world' belum didefinisikan!

g.say("Halo", "Dunia")
# Method 'say' belum didefinisikan!
# Args: ["Halo", "Dunia"]

# Contoh practical: Dynamic attribute access
class DynamicHash
  def initialize(data = {})
    @data = data
  end

  def method_missing(method_name, *args)
    key = method_name.to_s

    # Jika method diakhiri ?, cari boolean key
    if key.end_with?("?")
      @data.key?(key.chomp("?").to_sym)
    # Jika method diakhiri =, set value
    elsif key.end_with?("=")
      @data[key.chomp("=").to_sym] = args.first
    # Jika ada key yang cocok, return value
    elsif @data.key?(method_name)
      @data[method_name]
    else
      super  # Panggil method_missing asli
    end
  end

  # PENTING: Selalu implement respond_to_missing?
  def respond_to_missing?(method_name, include_private = false)
    key = method_name.to_s.chomp("?").chomp("=").to_sym
    @data.key?(key) || super
  end
end

obj = DynamicHash.new(nama: "Budi", umur: 25, aktif: true)
puts obj.nama      # Budi
puts obj.umur      # 25
puts obj.aktif?    # true
puts obj.email?    # false
obj.email = "budi@mail.com"
puts obj.email     # budi@mail.com
puts obj.respond_to?(:nama)  # true

Rails Dynamic Finders

Ruby — Dynamic Finders
# Implementasi sederhana dynamic finders seperti di Rails
class SimpleModel
  def self.find_by(*attributes)
    # Implementasi pencarian
  end

  # Simulasi method_missing seperti Rails
  def self.method_missing(method_name, *args)
    if method_name.to_s.start_with?("find_by_")
      # Extract field name dari method
      fields = method_name.to_s.sub("find_by_", "").split("_and_")

      puts "Dynamic finder: find_by_#{fields.join('_and_')}"
      puts "Values: #{args.inspect}"
      puts "Query: SELECT * FROM #{self.name.downcase}s WHERE #{fields.map { |f| "#{f} = ?" }.join(' AND ')}"

      # Di sini kita bisa benar-benar query database
      # find_by_conditions(Hash[fields.zip(args)])
    else
      super
    end
  end

  def self.respond_to_missing?(method_name, include_private = false)
    method_name.to_s.start_with?("find_by_") || super
  end
end

class User < SimpleModel; end

# Panggilan dinamis:
User.find_by_name("Budi")
# Dynamic finder: find_by_name
# Values: ["Budi"]
# Query: SELECT * FROM users WHERE name = ?

User.find_by_name_and_age("Budi", 25)
# Dynamic finder: find_by_name_and_age
# Values: ["Budi", 25]
# Query: SELECT * FROM users WHERE name = ? AND age = ?

User.find_by_email("budi@mail.com")
# Dynamic finder: find_by_email
# Query: SELECT * FROM users WHERE email = ?
💡 Best Practice

Saat menggunakan method_missing, selalu implementasikan juga respond_to_missing? agar .respond_to?(:method_name) mengembalikan true untuk method yang ditangkap. Jangan lupa memanggil super di akhir agar method_missing asli tetap berfungsi untuk method yang benar-benar tidak ada.

5. define_method

define_method memungkinkan Anda mendefinisikan method secara dinamis saat runtime. Ini sangat berguna untuk menghindari kode repetitif.

Basic define_method

Ruby — define_method
# define_method — membuat method baru secara dinamis
class MyModel
  ATTRIBUTES = [:name, :email, :age, :city]

  # Membuat getter dan setter untuk semua atribut
  ATTRIBUTES.each do |attr|
    # Getter
    define_method(attr) do
      instance_variable_get("@#{attr}")
    end

    # Setter
    define_method("#{attr}=") do |value|
      instance_variable_set("@#{attr}", value)
    end

    # Validator method
    define_method("validate_#{attr}") do
      value = send(attr)
      if value.nil? || (value.is_a?(String) && value.empty?)
        "#{attr} tidak boleh kosong"
      else
        nil
      end
    end
  end

  def initialize(attrs = {})
    attrs.each { |key, value| send("#{key}=", value) }
  end

  def valid?
    ATTRIBUTES.none? { |attr| send("validate_#{attr}") }
  end
end

user = MyModel.new(name: "Budi", email: "budi@mail.com", age: 25, city: "Jakarta")
puts user.name        # Budi
puts user.email       # budi@mail.com
puts user.valid?      # true
puts user.validate_name  # nil (valid)

Membuat DSL dengan define_method

Ruby — DSL dengan define_method
# Membuat validator DSL seperti ActiveModel
module ValidatesPresence
  def validates_presence_of(*attributes)
    attributes.each do |attr|
      # Buat validator method
      define_method("#{attr}_present?") do
        value = instance_variable_get("@#{attr}")
        value && !value.to_s.empty?
      end

      # Tambah ke daftar validasi
      @validations ||= []
      @validations << { attribute: attr, type: :presence }
    end
  end

  def validations
    @validations || []
  end
end

class Article
  extend ValidatesPresence

  validates_presence_of :title, :body, :author

  attr_accessor :title, :body, :author

  def initialize(title: nil, body: nil, author: nil)
    @title = title
    @body = body
    @author = author
  end

  def valid?
    self.class.validations.all? do |v|
      send("#{v[:attribute]}_present?")
    end
  end

  def errors
    self.class.validations.select do |v|
      !send("#{v[:attribute]}_present?")
    end.map { |v| "#{v[:attribute]} tidak boleh kosong" }
  end
end

artikel = Article.new(title: "Tutorial Ruby", body: "Isi artikel...", author: "Budi")
puts artikel.valid?          # true
puts artikel.title_present?  # true

artikel2 = Article.new(title: "", body: nil, author: "Budi")
puts artikel2.valid?         # false
puts artikel2.errors.inspect # ["title tidak boleh kosong", "body tidak boleh kosong"]

Method Delegation Pattern

Ruby — Delegation
# Delegate pattern — forwarding methods ke objek lain
module Delegator
  def delegate(*methods, to:)
    methods.each do |method|
      define_method(method) do |*args, &block|
        target = send(to)
        target.send(method, *args, &block)
      end
    end
  end
end

class UserPresenter
  extend Delegator

  attr_reader :user

  delegate :name, :email, :age, to: :user

  def initialize(user)
    @user = user
  end

  def formatted_info
    "#{name} (#{email}) - #{age} tahun"
  end
end

# Class User yang sederhana
class SimpleUser
  attr_accessor :name, :email, :age

  def initialize(name:, email:, age:)
    @name = name
    @email = email
    @age = age
  end
end

user = SimpleUser.new(name: "Budi", email: "budi@mail.com", age: 25)
presenter = UserPresenter.new(user)

puts presenter.name          # Budi (delegated)
puts presenter.email         # budi@mail.com (delegated)
puts presenter.formatted_info  # Budi (budi@mail.com) - 25 tahun

6. Eigenclass / Singleton Class

Setiap objek di Ruby memiliki class tersembunyi yang disebut eigenclass (atau singleton class). Eigenclass berisi method-method yang spesifik untuk objek tertentu saja. Ini adalah konsep kunci dalam metaprogramming Ruby.

Akses Eigenclass

Ruby — Eigenclass
# Setiap objek punya eigenclass sendiri
obj1 = "hello"
obj2 = "world"

# Membuka eigenclass dengan class << obj
class << obj1
  def greet
    "Halo dari eigenclass!"
  end

  def length
    super * 2  # Override length untuk obj1 saja
  end
end

puts obj1.greet    # "Halo dari eigenclass!"
puts obj1.length   # 10 (hello = 5, tapi 5 * 2)
puts obj2.length   # 5 (tidak terpengaruh!)
# puts obj2.greet  # Error! Method hanya ada di eigenclass obj1

# Eigenclass dari class
class Animal
  class << self
    def kingdom
      "Animalia"
    end

    def phylum
      "Chordata"
    end
  end
end

puts Animal.kingdom   # "Animalia"
puts Animal.phylum    # "Chordata"
# Sama dengan def self.kingdom

# Menelusuri eigenclass chain
class Dog < Animal; end

puts Dog.ancestors.inspect
# [Dog, Animal, Object, Kernel, BasicObject]
# Dog.eigenclass → Animal.eigenclass → Class → Module → ...

Eigenclass Inheritance

Ruby — Eigenclass Chain
# Method lookup di Ruby:
# 1. Eigenclass dari objek
# 2. Eigenclass dari class
# 3. Class itu sendiri
# 4. Module yang di-include
# 5. Superclass
# 6. Repeat sampai BasicObject

class MyClass
  def hello
    "hello from MyClass instance"
  end

  def self.hello
    "hello from MyClass class"
  end
end

obj = MyClass.new
puts obj.hello     # "hello from MyClass instance"
puts MyClass.hello  # "hello from MyClass class"

# Eigenclass method memiliki prioritas lebih tinggi
class MyClass
  # Tambah method ke eigenclass MyClass
  class << self
    def hello
      "hello from MyClass eigenclass"
    end
  end
end

puts MyClass.hello  # "hello from MyClass eigenclass"
# Eigenclass method menimpa class method biasa

# Cek apakah method ada di eigenclass
puts MyClass.singleton_methods.inspect
# [:hello, ...]

# Eigenclass dari instance
obj2 = "test"
class << obj2
  def custom_method
    "custom!"
  end
end

puts obj2.custom_method           # "custom!"
puts obj2.singleton_methods       # [:custom_method]

# Perbedaan class method vs singleton method pada class:
class Foo
  def self.bar    # Ini adalah singleton method pada Foo
    "bar"
  end
end
# equivalen dengan:
class Foo
  class << self
    def bar
      "bar"
    end
  end
end

7. Singleton Method

Singleton method adalah method yang didefinisikan hanya untuk satu objek tertentu. Ini bekerja melalui eigenclass.

Ruby — Singleton Method
# Singleton method — method khusus untuk satu objek
str1 = "hello"
str2 = "world"

# Definisikan singleton method pada str1
def str1.shout
  upcase + "!"
end

puts str1.shout    # "HELLO!"
# puts str2.shout  # Error! Method hanya ada di str1

puts str1.singleton_methods  # [:shout]
puts str2.singleton_methods  # []

# Singleton method pada class
class User
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def greet
    "Halo, saya #{@name}"
  end
end

admin = User.new("SuperAdmin")
regular = User.new("Budi")

# Tambah singleton method hanya untuk admin
def admin.special_greeting
  "Halo Admin #{@name}! Anda punya akses penuh."
end

puts admin.greet              # Halo, saya SuperAdmin
puts admin.special_greeting   # Halo Admin SuperAdmin! Anda punya akses penuh.
puts regular.greet            # Halo, saya Budi
# puts regular.special_greeting  # Error!

# Singleton class dengan method berulang
obj = "test_string"
class << obj
  def upcase_reverse
    upcase.reverse
  end

  def word_count
    split.length
  end
end

puts obj.upcase_reverse  # GNIRTS_TSET
puts obj.word_count      # 1

# Digunakan oleh RSpec untuk mock object
# allow(object).to receive(:method).and_return(value)
# Ini sebenarnya membuat singleton method pada object!

8. Hook Methods

Hook methods adalah callback yang dipanggil secara otomatis oleh Ruby ketika event tertentu terjadi. Ini memungkinkan Anda menambah perilaku saat class di-inherit, module di-include, method didefinisikan, dll.

Ruby — Hook Methods
# ===== inherited hook =====
# Dipanggil saat sebuah class di-inherit
class Parent
  def self.inherited(child_class)
    puts "#{child_class} meng-herit #{self}"
    super  # Good practice: panggil super
  end
end

class Child < Parent; end
# Output: Child meng-herit Parent

# ===== included hook =====
# Dipanggil saat module di-include
module Trackable
  def self.included(base)
    puts "Trackable di-include ke #{base}"

    # Tambah class method saat di-include
    base.extend(ClassMethods)

    # Tambah callback
    base.instance_variable_set(:@tracked_attributes, [])
  end

  module ClassMethods
    def track(*attributes)
      @tracked_attributes ||= []
      @tracked_attributes.concat(attributes)

      attributes.each do |attr|
        define_method("#{attr}_changed?") do
          instance_variable_get("@#{attr}_changed") || false
        end

        # Original setter wrapper
        original_setter = "#{attr}="
        define_method(original_setter) do |value|
          old_value = send(attr)
          instance_variable_set("@#{attr}_changed", old_value != value)
          super(value)
        end
      end
    end

    def tracked_attributes
      @tracked_attributes || []
    end
  end
end

class Article
  include Trackable

  track :title, :body

  attr_accessor :title, :body
end

artikel = Article.new
artikel.title = "Judul Baru"
puts artikel.title_changed?   # true
artikel.title = "Judul Baru"  # set nilai sama
puts artikel.title_changed?   # false
puts Article.tracked_attributes.inspect  # [:title, :body]

# ===== method_added hook =====
class Logger
  def self.method_added(method_name)
    puts "Method baru: #{method_name}"
  end

  def self.method_removed(method_name)
    puts "Method dihapus: #{method_name}"
  end
end

class MyClass < Logger
  def hello; end   # Output: Method baru: hello
  def world; end   # Output: Method baru: world
end

# ===== const_missing hook =====
class PluginLoader
  def self.const_missing(name)
    puts "Constant #{name} tidak ditemukan, mencoba load..."
    # Di Rails, ini digunakan untuk auto-loading class
    nil
  end
end

# ===== singleton_method_added hook =====
class Observer
  def self.singleton_method_added(method_name)
    puts "Singleton method '#{method_name}' ditambahkan ke #{self}"
  end
end

9. eval dan Keamanan

Ruby menyediakan beberapa cara untuk mengeksekusi string sebagai kode Ruby: eval, instance_eval, class_eval, dan module_eval. Ini sangat powerful tapi juga sangat berbahaya.

Ruby — eval Variants
# ===== eval — mengeksekusi string sebagai kode Ruby =====
x = 10
result = eval("x * 2 + 5")
puts result  # 25

# eval dengan binding
def get_binding(value)
  binding
end

b = get_binding(42)
puts eval("value", b)  # 42

# ===== instance_eval — eval dalam context instance =====
class Config
  def initialize
    @settings = {}
  end

  def configure(&block)
    instance_eval(&block)
  end

  def set(key, value)
    @settings[key] = value
  end

  def get(key)
    @settings[key]
  end

  def to_s
    @settings.inspect
  end
end

config = Config.new
config.configure do
  set :app_name, "BeebaneLabs"
  set :version, "1.0"
  set :debug, true
end

puts config.get(:app_name)  # BeebaneLabs
puts config.to_s
# {:app_name=>"BeebaneLabs", :version=>"1.0", :debug=>true}

# ===== class_eval / module_eval — eval dalam context class =====
class MyClass
end

MyClass.class_eval do
  # Bisa mendefinisikan method di dalam class
  def hello
    "hello from class_eval"
  end

  # Bisa mendefinisikan class method
  def self.world
    "world from class_eval"
  end
end

puts MyClass.new.hello   # hello from class_eval
puts MyClass.world       # world from class_eval

# class_eval dengan string (lebih fleksibel tapi kurang aman)
MyClass.class_eval("def foo; 'foo!'; end")
puts MyClass.new.foo     # foo!

# ===== Keamanan — JANGAN PERNAH gunakan eval dengan user input! =====
# BURUK (security vulnerability!):
# user_input = gets.chomp
# eval(user_input)  # JANGAN INI!

# Gunakan alternatif yang lebih aman:
user_input = "42"
# Gunakan send() atau constantize daripada eval
number = Integer(user_input) rescue nil
puts number  # 42

# Whitelist method calls
ALLOWED_METHODS = [:to_i, :to_f, :to_s]
method_name = :to_i
if ALLOWED_METHODS.include?(method_name)
  result = "42".send(method_name)
end
⚠️ Keamanan

JANGAN PERNAH gunakan eval dengan input dari user! Ini adalah security vulnerability serius yang bisa memungkinkan attacker mengeksekusi kode berbahaya. Gunakan send, public_send, atau whitelist method sebagai alternatif yang lebih aman.

10. Contoh Praktis Metaprogramming

Mari lihat contoh-contoh nyata bagaimana metaprogramming digunakan dalam Ruby dan Rails.

1. ActiveRecord-Style Model

Ruby — Simple ORM
# Simple ORM dengan metaprogramming
module SimpleRecord
  def self.included(base)
    base.extend(ClassMethods)
    base.instance_variable_set(:@columns, [])
  end

  module ClassMethods
    def columns
      @columns
    end

    def column(name, type = :string)
      @columns << { name: name, type: type }

      # Buat getter
      define_method(name) do
        instance_variable_get("@#{name}")
      end

      # Buat setter
      define_method("#{name}=") do |value|
        instance_variable_set("@#{name}", value)
      end
    end

    def find(id)
      puts "SELECT * FROM #{table_name} WHERE id = #{id}"
      new(id: id)
    end

    def where(conditions)
      puts "SELECT * FROM #{table_name} WHERE #{conditions.map { |k,v| "#{k} = '#{v}'" }.join(' AND ')}"
      []
    end

    def table_name
      name.downcase + "s"
    end
  end

  def initialize(attrs = {})
    attrs.each do |key, value|
      setter = "#{key}="
      respond_to?(setter) ? send(setter, value) : raise("Unknown attribute: #{key}")
    end
  end

  def save
    puts "INSERT INTO #{self.class.table_name} (#{attributes.keys.join(', ')}) VALUES (#{attributes.values.map { |v| "'#{v}'" }.join(', ')})"
    true
  end

  def attributes
    self.class.columns.each_with_object({}) do |col, hash|
      hash[col[:name]] = send(col[:name])
    end
  end
end

# Menggunakan SimpleRecord
class Product
  include SimpleRecord

  column :id, :integer
  column :name, :string
  column :price, :float
  column :stock, :integer
end

product = Product.new(id: 1, name: "Laptop", price: 15_000_000, stock: 50)
puts product.name    # Laptop
puts product.price   # 15000000
product.save
# INSERT INTO products (id, name, price, stock) VALUES ('1', 'Laptop', '15000000', '50')

Product.find(1)
# SELECT * FROM products WHERE id = 1

Product.where(name: "Laptop", stock: 50)
# SELECT * FROM products WHERE name = 'Laptop' AND stock = '50'

2. Builder DSL

Ruby — Builder DSL
# HTML Builder DSL — metaprogramming untuk membuat HTML
class HtmlBuilder
  def initialize(&block)
    @html = ""
    instance_eval(&block) if block_given?
  end

  def method_missing(tag_name, *args, &block)
    options = args.last.is_a?(Hash) ? args.pop : {}
    content = args.first

    attrs = options.map { |k, v| " #{k}=\"#{v}\"" }.join
    @html << "<#{tag_name}#{attrs}>"

    if block_given?
      old_html = @html
      @html = ""
      instance_eval(&block)
      content = @html
      @html = old_html
    end

    @html << content.to_s
    @html << "</#{tag_name}>"
  end

  def to_s
    @html
  end
end

# Menggunakan builder DSL
html = HtmlBuilder.new do
  div class: "container" do
    h1 "Selamat Datang"
    p class: "intro" do
      "Ini adalah halaman yang dibuat dengan metaprogramming!"
    end
    ul do
      li "Item 1"
      li "Item 2"
      li "Item 3"
    end
    a href: "/about", "Tentang Kami"
  end
end

puts html.to_s
# <div class="container">
#   <h1>Selamat Datang</h1>
#   <p class="intro">Ini adalah halaman yang...</p>
#   <ul><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>
#   <a href="/about">Tentang Kami</a>
# </div>

3. Auto-generating Accessors

Ruby — Memoization & Caching
# Memoization pattern dengan metaprogramming
module Memoizable
  def memoize(method_name)
    original_method = instance_method(method_name)

    define_method(method_name) do |*args|
      cache_key = "@_memo_#{method_name}_#{args.hash}"

      if instance_variable_defined?(cache_key)
        instance_variable_get(cache_key)
      else
        result = original_method.bind(self).call(*args)
        instance_variable_set(cache_key, result)
      end
    end
  end
end

class ApiClient
  extend Memoizable

  def fetch_users
    puts "Fetching users from API..."
    sleep(1)  # Simulate network delay
    ["Budi", "Rina", "Andi"]
  end

  def fetch_posts(user_id)
    puts "Fetching posts for user #{user_id}..."
    sleep(1)
    ["Post 1", "Post 2"]
  end

  memoize :fetch_users
  memoize :fetch_posts
end

client = ApiClient.new

# Panggilan pertama — cache miss
puts client.fetch_users.inspect  # Fetching users from API...
# ["Budi", "Rina", "Andi"]

# Panggilan kedua — cache hit (langsung return)
puts client.fetch_users.inspect  # Tidak ada "Fetching..." lagi!
# ["Budi", "Rina", "Andi"]

11. Quiz: Uji Pemahamanmu!

Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang Ruby Metaprogramming:

Pertanyaan 1: Apa yang dilakukan method_missing di Ruby?

a) Menghapus method yang tidak digunakan dari memory
b) Menangkap pemanggilan method yang belum didefinisikan
c) Mengompilasi method secara otomatis
d) Membuat method menjadi private

Pertanyaan 2: Apa itu eigenclass di Ruby?

a) Class utama dari sebuah objek
b) Class tersembunyi yang dimiliki setiap objek untuk method singleton
c) Parent class dari semua objek
d) Interface yang wajib diimplementasikan

Pertanyaan 3: Apa yang dilakukan define_method?

a) Membuat variabel baru secara dinamis
b) Menghapus method yang sudah ada
c) Membuat method baru secara dinamis di runtime
d) Mengubah tipe data method

Pertanyaan 4: Hook method inherited dipanggil kapan?

a) Saat objek baru dibuat
b) Saat sebuah class di-inherit oleh class lain
c) Saat method baru didefinisikan
d) Saat module di-include

Pertanyaan 5: Mengapa menggunakan eval dengan user input sangat berbahaya?

a) Karena akan menyebabkan syntax error
b) Karena akan memperlambat program secara signifikan
c) Karena memungkinkan attacker mengeksekusi kode berbahaya (code injection)
d) Karena hanya bisa digunakan dalam class method
🔍 Zoom
100%
🎨 Tema