Konstanta #
Konstanta adalah cara Ruby untuk menyimpan nilai yang tidak seharusnya berubah selama program berjalan — konfigurasi aplikasi, nilai matematika, daftar status yang valid, batas ukuran file, dan sejenisnya. Tapi ada hal menarik sekaligus mengejutkan tentang konstanta di Ruby: interpreter tidak secara keras melarang perubahannya. Ruby hanya memberi peringatan, lalu melanjutkan eksekusi seolah tidak terjadi apa-apa. Ini berarti “konstanta” di Ruby lebih tepat dipahami sebagai sinyal intensi kepada programmer lain — bukan jaminan teknis bahwa nilainya tidak akan pernah berubah. Memahami nuansa ini, serta cara membuat konstanta yang benar-benar tidak bisa diubah, adalah topik utama artikel ini.
Mendefinisikan Konstanta #
Konstanta di Ruby dimulai dengan huruf kapital. Konvensi komunitas menggunakan SCREAMING_SNAKE_CASE — semua huruf besar dengan underscore sebagai pemisah — untuk membedakannya secara visual dari variabel dan nama kelas.
# Konstanta sederhana di level top-level
PI = 3.14159265358979
GRAVITASI = 9.80665 # m/s², standar gravitasi Bumi
KECEPATAN_CAHAYA = 299_792_458 # meter per detik
# Konstanta konfigurasi aplikasi
VERSI_APP = "2.4.1"
NAMA_APP = "Inventori Pro"
BATAS_UPLOAD = 10 * 1024 * 1024 # 10 MB dalam bytes
TIMEOUT_DETIK = 30
# Konstanta yang menyimpan koleksi
STATUS_VALID = [:aktif, :nonaktif, :pending, :diblokir].freeze
FORMAT_GAMBAR = %w[jpg jpeg png webp gif].freeze
Nama kelas dan modul juga adalah konstanta — hanya saja menggunakan PascalCase bukan SCREAMING_SNAKE_CASE.
# Ini juga konstanta — kelas dan modul selalu konstanta di Ruby
class ProsesanPembayaran # PascalCase — konstanta bertipe kelas
end
module AuthHelper # PascalCase — konstanta bertipe modul
end
# Cek dengan constants — menampilkan semua konstanta yang terdefinisi
puts Object.constants.grep(/^[A-Z_]+$/).first(5).inspect
# => [:PI, :GRAVITASI, :VERSI_APP, ...]
Perilaku Reassignment — Peringatan, Bukan Error #
Ini adalah bagian yang paling penting untuk dipahami: Ruby tidak melarang reassignment konstanta. Yang dilakukannya hanya mencetak peringatan ke stderr, lalu melanjutkan eksekusi dengan nilai baru.
BATAS = 100
puts BATAS # => 100
BATAS = 200 # warning: already initialized constant BATAS
# warning: previous definition of BATAS was here
puts BATAS # => 200 (tetap berhasil diubah!)
Ini berarti “konstanta” di Ruby bukan jaminan immutability — ia adalah konvensi komunikasi. Ketika kamu menulis BATAS_MAKS = 50, kamu sedang memberi tahu developer lain: “nilai ini tidak seharusnya diubah, pikir dua kali sebelum mengubahnya.”
# ANTI-PATTERN: mengubah konstanta di tengah program
DISKON_LEBARAN = 0.3
def hitung_diskon(harga)
harga * DISKON_LEBARAN
end
# Di tempat lain dalam kode:
DISKON_LEBARAN = 0.5 # ← warning, tapi tetap jalan — sangat berbahaya!
# Sekarang semua kalkulasi yang sudah berjalan menggunakan nilai berbeda
# BENAR: jika nilai perlu berubah, gunakan variabel atau method, bukan konstanta
def diskon_aktif
ENV["DISKON_PERSEN"]&.to_f || 0.3
end
Jika kamu berjalan dengan opsi-W2atau mengaktifkan verbose warnings, Ruby akan menampilkan lokasi persis dari definisi awal dan reassignment konstanta. Dalam environment production, beberapa framework seperti Rails mengkonfigurasi Ruby untuk memperlakukan warning ini sebagai sinyal serius. Jangan abaikan warningalready initialized constant— ia hampir selalu menandakan ada bug atau desain yang perlu diperbaiki.
Mutasi vs Reassignment #
Ada perbedaan penting yang sering membingungkan: reassignment (mengganti objek yang dirujuk konstanta) menghasilkan warning, tapi mutasi (mengubah isi objek yang dirujuk) tidak menghasilkan warning sama sekali.
KOTA_TUJUAN = ["Jakarta", "Surabaya", "Bandung"]
# Reassignment — warning
KOTA_TUJUAN = ["Medan", "Makassar"] # ← warning: already initialized constant
# Mutasi — TIDAK ada warning, tapi nilai konstanta berubah!
KOTA_TUJUAN << "Yogyakarta" # ← tidak ada warning
KOTA_TUJUAN.push("Semarang") # ← tidak ada warning
puts KOTA_TUJUAN.inspect
# => ["Jakarta", "Surabaya", "Bandung", "Yogyakarta", "Semarang"]
Inilah mengapa freeze sangat penting untuk konstanta yang berupa koleksi (Array, Hash, String):
# ANTI-PATTERN: konstanta koleksi tanpa freeze — bisa dimutasi diam-diam
STATUS_ORDER = ["pending", "diproses", "dikirim", "selesai"]
STATUS_ORDER << "dibatalkan" # tidak ada warning, tapi ini mengubah "konstanta"!
# BENAR: freeze mencegah mutasi
STATUS_ORDER = ["pending", "diproses", "dikirim", "selesai"].freeze
STATUS_ORDER << "dibatalkan" # => FrozenError: can't modify frozen Array
# Hash juga perlu di-freeze
KODE_HTTP = {
ok: 200,
not_found: 404,
server_error: 500
}.freeze
KODE_HTTP[:created] = 201 # => FrozenError: can't modify frozen Hash
flowchart TD
A[Konstanta dirujuk ke objek] --> B{Apa yang dilakukan?}
B --> C["Reassignment\nKONSTAN = nilai_baru"]
B --> D["Mutasi\nKONSTAN << elemen"]
C --> E["Warning dari Ruby\n(tapi tetap jalan)"]
D --> F{Apakah objek frozen?}
F --> G["Ya — freeze aktif\n→ FrozenError"]
F --> H["Tidak — tanpa freeze\n→ Berhasil tanpa warning!"]
G --> I["✓ Konstanta terlindungi"]
H --> J["✗ Nilai berubah diam-diam"]Deep Freeze untuk Koleksi Bersarang #
freeze hanya membekukan objek di permukaan — elemen-elemen di dalamnya tetap bisa dimutasi jika berupa objek yang bisa diubah.
# freeze dangkal — elemen string di dalam array masih bisa dimutasi
NAMA_HARI = ["Senin", "Selasa", "Rabu"].freeze
NAMA_HARI << "Kamis" # => FrozenError: array-nya frozen
NAMA_HARI[0] << " Pagi" # BERHASIL! String di dalam tidak ikut frozen
puts NAMA_HARI[0] # => "Senin Pagi" ← nilai berubah!
# BENAR: freeze setiap elemen juga
NAMA_HARI = ["Senin".freeze, "Selasa".freeze, "Rabu".freeze].freeze
NAMA_HARI[0] << " Pagi" # => FrozenError: string-nya juga frozen
# Cara lebih ringkas menggunakan map:
NAMA_HARI = ["Senin", "Selasa", "Rabu"].map(&:freeze).freeze
# Atau dengan magic comment — membekukan semua string literal di file:
# frozen_string_literal: true
NAMA_HARI = ["Senin", "Selasa", "Rabu"].freeze
NAMA_HARI[0] << " Pagi" # => FrozenError karena frozen_string_literal
Konstanta dalam Kelas dan Modul #
Mendefinisikan konstanta di dalam kelas atau modul adalah praktik yang sangat umum dan dianjurkan. Ini memberikan namespace — konstanta terikat pada konteks yang relevan, bukan mengambang di scope global.
class KonfigurasiDatabase
HOST = ENV.fetch("DB_HOST", "localhost")
PORT = ENV.fetch("DB_PORT", "5432").to_i
NAMA_DB = ENV.fetch("DB_NAME", "app_development")
POOL_SIZE = 10
TIMEOUT = 5_000 # milliseconds
def self.connection_string
"postgresql://#{HOST}:#{PORT}/#{NAMA_DB}"
end
end
puts KonfigurasiDatabase::HOST # => "localhost"
puts KonfigurasiDatabase::PORT # => 5432
puts KonfigurasiDatabase.connection_string
# => "postgresql://localhost:5432/app_development"
module ValidasiInput
PANJANG_MIN_PASSWORD = 8
PANJANG_MAKS_PASSWORD = 128
POLA_EMAIL = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
POLA_TELEPON = /\A(\+62|0)[0-9]{9,12}\z/.freeze
KARAKTER_KHUSUS = %w[! @ # $ % ^ & * ( )].freeze
def self.email_valid?(email)
email.match?(POLA_EMAIL)
end
def self.password_valid?(password)
password.length.between?(PANJANG_MIN_PASSWORD, PANJANG_MAKS_PASSWORD)
end
end
puts ValidasiInput.email_valid?("[email protected]") # => true
puts ValidasiInput.email_valid?("bukan-email") # => false
puts ValidasiInput::PANJANG_MIN_PASSWORD # => 8
Konstanta dalam Subkelas #
Konstanta mewarisi ke subkelas — subkelas bisa mengakses konstanta dari kelas induknya. Tapi subkelas juga bisa mendefinisikan ulang konstanta dengan nilainya sendiri tanpa mempengaruhi kelas induk.
class Hewan
KELAS_BIOLOGIS = "Animalia"
SUHU_NORMAL = 37.0 # Celsius, rata-rata mamalia
def info_dasar
"Kingdom: #{KELAS_BIOLOGIS}, Suhu normal: #{SUHU_NORMAL}°C"
end
end
class Reptil < Hewan
SUHU_NORMAL = nil # reptil berdarah dingin — override konstanta induk
def info_dasar
# KELAS_BIOLOGIS diwarisi dari Hewan
"Kingdom: #{KELAS_BIOLOGIS}, Berdarah dingin"
end
end
puts Hewan.new.info_dasar # => Kingdom: Animalia, Suhu normal: 37.0°C
puts Reptil.new.info_dasar # => Kingdom: Animalia, Berdarah dingin
puts Hewan::SUHU_NORMAL # => 37.0 (tidak berubah)
puts Reptil::SUHU_NORMAL # => nil
Constant Lookup Path #
Saat Ruby menemukan nama yang diawali huruf kapital dalam kode, ia mencari konstanta tersebut melalui constant lookup path — urutan scope yang diperiksa satu per satu.
LEVEL = "global"
module Luar
LEVEL = "modul luar"
module Dalam
LEVEL = "modul dalam"
def self.cetak_level
puts LEVEL # => "modul dalam" — ditemukan di scope terdekat
end
end
def self.cetak_level
puts LEVEL # => "modul luar"
puts Dalam::LEVEL # => "modul dalam" — akses eksplisit
puts ::LEVEL # => "global" — :: memaksa pencarian dari top-level
end
end
Luar.cetak_level
Luar::Dalam.cetak_level
flowchart TD
A["Ruby menemukan nama KONSTANTA"] --> B["Cari di scope\nkelas/modul saat ini"]
B --> C{Ditemukan?}
C --> |Ya| Z["Gunakan nilai ini"]
C --> |Tidak| D["Cari di kelas/modul\nyang meng-include/extend"]
D --> E{Ditemukan?}
E --> |Ya| Z
E --> |Tidak| F["Cari di kelas/modul\ninduk (ancestor chain)"]
F --> G{Ditemukan?}
G --> |Ya| Z
G --> |Tidak| H["Cari di top-level\n(Object)"]
H --> I{Ditemukan?}
I --> |Ya| Z
I --> |Tidak| J["NameError: uninitialized\nconstant KONSTANTA"]Operator :: digunakan untuk navigasi namespace secara eksplisit:
# Akses konstanta dari namespace tertentu
puts Math::PI # => 3.141592653589793
puts Math::E # => 2.718281828459045
# :: di awal berarti top-level
module App
VERSION = "1.0"
class Config
VERSION = "config-1.0"
def versi_app
::App::VERSION # akses App::VERSION dari dalam Config
end
def versi_config
VERSION # konstanta lokal — Config::VERSION
end
end
end
c = App::Config.new
puts c.versi_app # => "1.0"
puts c.versi_config # => "config-1.0"
Pola Penggunaan Konstanta di Aplikasi Nyata #
Berikut beberapa pola umum penggunaan konstanta yang sering ditemui di codebase Ruby dan Rails profesional.
Enum Manual dengan Hash #
Sebelum Ruby punya enum bawaan, developer menggunakan Hash konstanta untuk mendefinisikan nilai-nilai terbatas.
module StatusPesanan
PENDING = "pending"
DIPROSES = "diproses"
DIKIRIM = "dikirim"
SELESAI = "selesai"
DIBATAL = "dibatalkan"
SEMUA = [PENDING, DIPROSES, DIKIRIM, SELESAI, DIBATAL].freeze
BISA_DIBATAL = [PENDING, DIPROSES].freeze
SUDAH_FINAL = [SELESAI, DIBATAL].freeze
def self.valid?(status)
SEMUA.include?(status)
end
def self.bisa_dibatalkan?(status)
BISA_DIBATAL.include?(status)
end
end
pesanan_status = "dikirim"
puts StatusPesanan.valid?(pesanan_status) # => true
puts StatusPesanan.bisa_dibatalkan?(pesanan_status) # => false
# Penggunaan di kode lain — lebih aman dari string literal
if pesanan.status == StatusPesanan::SELESAI
kirim_email_konfirmasi(pesanan)
end
Konfigurasi Bertingkat dengan Modul #
module Config
module Database
ADAPTER = "postgresql"
POOL = Integer(ENV.fetch("DB_POOL", 5))
TIMEOUT = 5_000
end
module Cache
DRIVER = :redis
TTL = 3_600 # 1 jam dalam detik
MAX_SIZE = 256 * 1024 * 1024 # 256 MB
end
module Upload
MAKS_UKURAN = 10 * 1024 * 1024 # 10 MB
FORMAT_IZIN = %w[jpg jpeg png pdf docx].freeze
PATH_SEMENTARA = "/tmp/uploads".freeze
end
end
puts Config::Database::POOL # => 5
puts Config::Upload::MAKS_UKURAN # => 10485760
puts Config::Upload::FORMAT_IZIN.inspect
# => ["jpg", "jpeg", "png", "pdf", "docx"]
Konstanta Berbasis Environment #
Konstanta yang nilainya dibaca dari environment variable adalah pola umum untuk konfigurasi yang berbeda antar environment (development, staging, production).
module AppConfig
# ENV.fetch akan raise KeyError jika variabel tidak ada (lebih aman dari ENV[])
SECRET_KEY = ENV.fetch("SECRET_KEY_BASE") { raise "SECRET_KEY_BASE harus diset!" }
DATABASE_URL = ENV.fetch("DATABASE_URL", "postgresql://localhost/app_dev")
# Nilai boolean dari environment
DEBUG_MODE = ENV.fetch("DEBUG", "false") == "true"
MAINTENANCE = ENV.fetch("MAINTENANCE_MODE", "false") == "true"
# Nilai numerik — selalu konversi tipe secara eksplisit
MAX_WORKERS = Integer(ENV.fetch("MAX_WORKERS", 4))
REQUEST_TIMEOUT = Float(ENV.fetch("REQUEST_TIMEOUT", 30.0))
end
# ANTI-PATTERN: menggunakan ENV[] langsung di seluruh kode
# (tidak ada satu titik kontrol, tipe tidak terjamin)
def kirim_email
smtp_host = ENV["SMTP_HOST"] # bisa nil kapan saja, tipe String atau nil
# ...
end
# BENAR: sentralisasi di konstanta dengan tipe yang sudah dijamin
module Mailer
SMTP_HOST = ENV.fetch("SMTP_HOST", "localhost").freeze
SMTP_PORT = Integer(ENV.fetch("SMTP_PORT", 587))
FROM_ADDR = ENV.fetch("MAIL_FROM", "[email protected]").freeze
end
def kirim_email
smtp_host = Mailer::SMTP_HOST # selalu String, tidak pernah nil
# ...
end
Konvensi Penamaan Konstanta #
Ruby menggunakan dua gaya untuk konstanta, dan keduanya punya tempat yang tepat masing-masing.
| Gaya | Kapan digunakan | Contoh |
|---|---|---|
SCREAMING_SNAKE_CASE | Nilai konfigurasi, angka, string, koleksi | MAX_SIZE, DEFAULT_TIMEOUT, STATUS_AKTIF |
PascalCase | Nama kelas, modul, dan tipe | UserService, AuthHelper, HttpClient |
# SCREAMING_SNAKE_CASE — untuk nilai konfigurasi dan literal
MAKS_PERCOBAAN = 3
BATAS_HALAMAN = 25
URL_API_EKSTERNAL = "https://api.example.com/v2".freeze
# PascalCase — untuk kelas dan modul (juga konstanta!)
class GestorPembayaran
PROVIDER_AKTIF = :midtrans.freeze # konstanta konfigurasi di dalam kelas
end
module UtilsFormatting
DEFAULT_LOCALE = :id.freeze
end
# ANTI-PATTERN: gaya yang tidak konsisten
maxSize = 10 # ← ini bukan konstanta, ini variabel lokal!
Max_Size = 10 # ← valid tapi tidak idiomatik
MAXSIZE = 10 # ← valid tapi susah dibaca
Introspeksi Konstanta #
Ruby menyediakan beberapa method untuk mengeksplorasi konstanta yang terdefinisi di runtime — berguna untuk debugging dan metaprogramming.
module Konfigurasi
VERSI = "1.0"
DEBUG = false
TIMEOUT = 30
end
# Daftar semua konstanta dalam modul
puts Konfigurasi.constants.inspect
# => [:VERSI, :DEBUG, :TIMEOUT]
# Ambil nilai konstanta secara dinamis
nama_konstanta = :VERSI
puts Konfigurasi.const_get(nama_konstanta) # => "1.0"
# Cek apakah konstanta ada
puts Konfigurasi.const_defined?(:VERSI) # => true
puts Konfigurasi.const_defined?(:TIDAK_ADA) # => false
# Set konstanta secara dinamis (gunakan dengan hati-hati)
Konfigurasi.const_set(:ENVIRONMENT, "production")
puts Konfigurasi::ENVIRONMENT # => "production"
Kapan menggunakan introspeksi konstanta:
✓ Memuat konfigurasi secara dinamis dari file atau database
✓ Metaprogramming — generate kelas atau konstanta secara otomatis
✓ Testing — memverifikasi konstanta yang diharapkan ada
✓ Debugging — melihat semua konstanta yang terdefinisi dalam modul
✗ Jangan gunakan const_set untuk mengubah konstanta yang sudah ada
✗ Hindari const_get dengan input dari user — potensi security issue
Ringkasan #
- Konstanta dimulai huruf kapital —
SCREAMING_SNAKE_CASEuntuk nilai konfigurasi,PascalCaseuntuk kelas dan modul.- Ruby tidak melarang reassignment — hanya memperingatkan — konstanta adalah sinyal intensi, bukan jaminan teknis. Jangan abaikan warning
already initialized constant.- Mutasi berbeda dari reassignment — mengubah isi koleksi konstanta tidak menghasilkan warning, sehingga bisa terjadi diam-diam tanpa terdeteksi.
- Selalu
freezekonstanta koleksi — Array, Hash, dan String yang menjadi nilai konstanta harus di-freezeuntuk mencegah mutasi tidak sengaja.freezehanya dangkal — untuk koleksi bersarang,freezejuga setiap elemen di dalamnya, atau aktifkan# frozen_string_literal: true.- Definisikan konstanta dalam kelas atau modul — memberikan namespace yang jelas dan mencegah polusi scope global.
::untuk navigasi namespace eksplisit —App::Config::TIMEOUTlebih jelas dari akses implisit yang bergantung pada lookup path.- Sentralisasi konfigurasi environment — baca
ENVdi satu tempat dan simpan sebagai konstanta bertipe jelas, bukan memanggilENV[]langsung di mana-mana.const_getdanconst_defined?untuk introspeksi konstanta secara dinamis saat diperlukan dalam metaprogramming atau testing.