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 Code | Hindari duplikasi kode dengan generate method secara dinamis |
| Elegant DSL | Buat sintaks yang mirip bahasa manusia seperti di Rails |
| Flexibility | Modifikasi class dan method di runtime sesuai kebutuhan |
| Less Boilerplate | Kurangi kode repetitif dengan pattern yang dinamis |
| Library Design | Buat library/framework yang lebih user-friendly |
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!
┌───────────────────────────────────────────────────────┐ │ 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
# 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
# .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
# 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
# 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..."
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
# 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
# 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
# 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 = ?
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
# 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
# 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
# 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
# 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
# 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.
# 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.
# ===== 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.
# ===== 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
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
# 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
# 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
# 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: