DRY #

Ada sebuah keputusan kecil yang dilakukan ribuan kali dalam karier seorang engineer: menemukan kode yang mirip dengan apa yang baru saja ditulis, lalu memilih antara copy-paste atau membuat abstraksi. Copy-paste terasa lebih cepat — dan memang benar di awal. Masalah muncul enam bulan kemudian ketika aturan bisnis berubah dan kamu harus mengingat ada berapa tempat kode itu disalin. DRY — Don’t Repeat Yourself — adalah prinsip yang mengadvokasi pilihan kedua: setiap pengetahuan dalam sistem harus memiliki satu representasi yang jelas dan authoritative. Perlu dicatat kata kuncinya: pengetahuan, bukan sekadar kode. Dua fungsi yang berbaris kodenya identik belum tentu melanggar DRY jika keduanya merepresentasikan aturan yang berbeda. Panduan ini membahas DRY dari definisinya yang sering disalahpahami, empat bentuk pelanggaran yang paling umum, refactoring konkret di Go dan Dart, hingga batas yang penting: kapan duplikasi justru lebih baik daripada abstraksi yang salah.

Apa yang Dimaksud dengan DRY? #

DRY adalah prinsip yang pertama kali dirumuskan oleh Andrew Hunt dan David Thomas dalam buku The Pragmatic Programmer:

“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system.”

Kata kunci yang sering dilewatkan adalah knowledge — bukan code. Ini perbedaan yang krusial.

Duplikasi kode (mungkin bukan pelanggaran DRY):

  // Service A
  for _, item := range items {
      total += item.Price
  }

  // Service B (domain berbeda, konteks berbeda)
  for _, line := range lines {
      sum += line.Amount
  }

  → Dua loop yang secara visual mirip, tapi merepresentasikan
    konsep yang berbeda di domain yang berbeda.
    Menggabungkannya bisa menciptakan coupling yang tidak perlu.


Duplikasi knowledge (pelanggaran DRY):

  // UserService
  if user.Age < 18 {
      return errors.New("user must be at least 18 years old")
  }

  // ProfileService
  if profile.Age < 18 {
      return errors.New("age is not valid")
  }

  → Dua representasi dari SATU aturan bisnis yang sama:
    "usia minimum adalah 18 tahun".
    Jika aturan berubah ke 21 tahun, harus update dua tempat
    — dan keduanya punya pesan error yang berbeda.

DRY tentang single source of truth untuk sebuah aturan atau pengetahuan, bukan tentang menghilangkan semua kode yang kebetulan terlihat serupa.


Empat Bentuk Pelanggaran DRY yang Paling Umum #

1. Duplikasi Logika Bisnis #

Aturan bisnis yang sama ditulis ulang di beberapa tempat — sering dengan sedikit variasi yang tidak disengaja.

// ANTI-PATTERN: aturan "minimum order amount" tersebar di tiga tempat
// dengan threshold yang berbeda-beda — mana yang benar?

// OrderHandler
func validateOrder(order Order) error {
    if order.Total < 10000 {
        return errors.New("minimum order is Rp 10.000")
    }
    return nil
}

// CheckoutService
func processCheckout(cart Cart) error {
    if cart.Total < 10000 { // sama, tapi terpisah
        return errors.New("cart total too small")
    }
    // ...
}

// PaymentService
func initiatePayment(amount int64) error {
    if amount < 10000 { // sama, tapi threshold hardcoded lagi
        return errors.New("amount below minimum")
    }
    // ...
}

// BENAR: satu sumber kebenaran untuk aturan bisnis
const MinimumOrderAmount int64 = 10_000

var ErrBelowMinimumOrder = fmt.Errorf("minimum order is Rp %d", MinimumOrderAmount)

func ValidateOrderAmount(amount int64) error {
    if amount < MinimumOrderAmount {
        return ErrBelowMinimumOrder
    }
    return nil
}

// Semua service menggunakan fungsi yang sama
func validateOrder(order Order) error  { return ValidateOrderAmount(order.Total) }
func processCheckout(cart Cart) error  { return ValidateOrderAmount(cart.Total) }
func initiatePayment(amount int64) error { return ValidateOrderAmount(amount) }

2. Duplikasi Konstanta dan Magic Number #

Angka atau string yang sama tersebar di codebase tanpa nama yang bermakna.

// ANTI-PATTERN: magic number yang sama muncul berkali-kali
func processRetry(attempt int) {
    if attempt > 3 { sendToDeadLetterQueue() }  // ← 3 ini apa?
}

func rateLimiter(count int) bool {
    return count <= 3  // ← 3 lagi, sama atau beda?
}

func validateOTPAttempt(tries int) error {
    if tries >= 3 {  // ← 3 yang ketiga, mungkin beda konteks
        return ErrTooManyAttempts
    }
    return nil
}

// BENAR: setiap konstanta punya nama yang menjelaskan maknanya
const (
    MaxRetryAttempts  = 5  // retry untuk network calls
    MaxOTPAttempts    = 3  // maksimal salah OTP sebelum locked
    APIRateLimit      = 100 // request per menit per user
)

func processRetry(attempt int) {
    if attempt > MaxRetryAttempts { sendToDeadLetterQueue() }
}

func validateOTPAttempt(tries int) error {
    if tries >= MaxOTPAttempts {
        return ErrTooManyAttempts
    }
    return nil
}

3. Duplikasi Query dan Data Mapping #

Query database atau mapping struct yang sama ditulis ulang di beberapa layer.

// ANTI-PATTERN: query aktif user ditulis ulang di handler dan service
// Handler
func getUserList(db *sql.DB) []User {
    rows, _ := db.Query("SELECT id, name, email FROM users WHERE is_active = true")
    // ... scan rows
}

// Service
func getActiveUsersForReport(db *sql.DB) []User {
    rows, _ := db.Query("SELECT id, name, email FROM users WHERE is_active = true")
    // ... scan rows — persis sama
}

// Jika nama kolom berubah, harus update dua tempat

// BENAR: query dikapsulasi di repository layer
type UserRepository struct{ db *sql.DB }

func (r *UserRepository) FindActive(ctx context.Context) ([]User, error) {
    rows, err := r.db.QueryContext(ctx,
        "SELECT id, name, email FROM users WHERE is_active = true")
    if err != nil { return nil, err }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        rows.Scan(&u.ID, &u.Name, &u.Email)
        users = append(users, u)
    }
    return users, nil
}

// Handler dan Service keduanya pakai repo yang sama — satu sumber kebenaran

4. Duplikasi di Level Arsitektur #

Logika yang sama diimplementasikan di beberapa layer tanpa satu pun yang menjadi “sumber utama”.

// ANTI-PATTERN: validasi email di HTTP handler DAN di service
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    req := parseRequest(r)
    // Validasi di layer HTTP
    if !strings.Contains(req.Email, "@") {
        http.Error(w, "invalid email", 400)
        return
    }
    userService.Create(req)
}

func (s *UserService) Create(req CreateUserRequest) error {
    // Validasi yang sama di service layer
    if !strings.Contains(req.Email, "@") {
        return errors.New("invalid email")
    }
    return s.repo.Save(req)
}

// BENAR: validasi di satu layer yang appropriate (service/domain)
// Handler hanya mendelegasikan, tidak validasi sendiri
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    req := parseRequest(r)
    if err := userService.Create(req); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    w.WriteHeader(http.StatusCreated)
}

func (s *UserService) Create(req CreateUserRequest) error {
    if err := validateEmail(req.Email); err != nil { // satu tempat validasi
        return err
    }
    return s.repo.Save(req)
}

DRY pada Business Rules — Contoh Lengkap #

Berikut refactoring yang lebih komprehensif: sistem diskon yang awalnya tersebar menjadi terpusat.

// ANTI-PATTERN: logika diskon tersebar di checkout, invoice, dan reporting
// Masing-masing punya implementasi sendiri yang sedikit berbeda

// checkout_service.go
func applyDiscount(price float64, userType string) float64 {
    if userType == "vip" { return price * 0.8 }
    if userType == "member" { return price * 0.9 }
    return price
}

// invoice_service.go
func calculateFinalPrice(basePrice float64, membership string) float64 {
    switch membership {
    case "vip": return basePrice * 0.80      // sama tapi syntax berbeda
    case "member": return basePrice * 0.9    // presisi berbeda: 0.80 vs 0.9
    }
    return basePrice
}

// reporting.go
func getEffectivePrice(price float64, tier string) float64 {
    discounts := map[string]float64{"vip": 0.8, "member": 0.9}  // berbeda lagi
    if d, ok := discounts[tier]; ok { return price * d }
    return price
}

// BENAR: satu domain object yang merepresentasikan aturan diskon
type MembershipTier string

const (
    TierVIP    MembershipTier = "vip"
    TierMember MembershipTier = "member"
    TierRegular MembershipTier = ""
)

type DiscountPolicy struct {
    tier       MembershipTier
    multiplier float64
}

var discountPolicies = map[MembershipTier]DiscountPolicy{
    TierVIP:    {TierVIP, 0.80},
    TierMember: {TierMember, 0.90},
    TierRegular: {TierRegular, 1.00},
}

func ApplyMembershipDiscount(price float64, tier MembershipTier) float64 {
    policy, ok := discountPolicies[tier]
    if !ok {
        return price // no discount for unknown tier
    }
    return price * policy.multiplier
}

// Semua service menggunakan fungsi yang sama — aturan ada di satu tempat
// Jika diskon VIP berubah ke 0.75, hanya ubah satu baris di discountPolicies

DRY dalam Dart/Flutter #

Di Flutter, DRY sering dilanggar dalam UI layer — widget yang sama dibangun ulang di beberapa tempat.

// ANTI-PATTERN: card UI yang sama dibuat ulang di tiga screen
// ProductScreen
Widget _buildProductCard(Product p) => Card(
  child: Column(children: [
    Image.network(p.imageUrl),
    Text(p.name, style: TextStyle(fontWeight: FontWeight.bold)),
    Text('Rp ${p.price}'),
    ElevatedButton(onPressed: () => addToCart(p), child: Text('Add to Cart')),
  ]),
);

// SearchScreen — hampir identik
Widget _buildProductCard(Product p) => Card(
  child: Column(children: [
    Image.network(p.imageUrl),
    Text(p.name, style: TextStyle(fontWeight: FontWeight.bold, fontSize: 14)),
    Text('Rp ${p.price}'),
    ElevatedButton(onPressed: () => addToCart(p), child: Text('Tambah')), // teks berbeda!
  ]),
);

// BENAR: satu widget yang reusable dengan parameter untuk variasi
class ProductCard extends StatelessWidget {
  final Product product;
  final VoidCallback onAddToCart;
  final String addButtonLabel;

  const ProductCard({
    required this.product,
    required this.onAddToCart,
    this.addButtonLabel = 'Add to Cart', // default value
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      child: Column(children: [
        Image.network(product.imageUrl),
        Text(product.name, style: Theme.of(context).textTheme.titleMedium),
        Text('Rp ${product.price}'),
        ElevatedButton(
          onPressed: onAddToCart,
          child: Text(addButtonLabel),
        ),
      ]),
    );
  }
}

// Digunakan di mana saja dengan satu definisi
ProductCard(product: p, onAddToCart: () => addToCart(p))
ProductCard(product: p, onAddToCart: () => addToCart(p), addButtonLabel: 'Tambah')

DRY vs Over-Abstraction — Batas yang Penting #

Ada kutipan dari Sandi Metz yang sangat relevan:

“Duplication is far cheaper than the wrong abstraction.”

Tidak semua kode yang terlihat sama harus digabungkan. Ini adalah judgment call yang penting.

// Kasus di mana duplikasi lebih baik dari abstraksi yang salah

// User validation
func validateUserAge(age int) error {
    if age < 18 { return errors.New("user must be at least 18") }
    return nil
}

// Driver validation  
func validateDriverAge(age int) error {
    if age < 18 { return errors.New("driver must be at least 18") }
    return nil
}

// Terlihat duplikasi — tapi apakah harus digabung?
// Jika aturan berubah: user tetap 18, tapi driver harus 21
// Abstraksi prematur akan mempersulit divergensi ini

// Sebaliknya, jika keduanya BENAR-BENAR satu aturan bisnis:
const MinimumAgeForService = 18

func ValidateMinimumAge(age int, role string) error {
    if age < MinimumAgeForService {
        return fmt.Errorf("%s must be at least %d years old", role, MinimumAgeForService)
    }
    return nil
}

Pertanyaan yang membantu memutuskan:

Abstraksi LAYAK jika:
  ✓ Keduanya benar-benar merepresentasikan aturan yang sama
  ✓ Jika satu berubah, yang lain PASTI berubah juga
  ✓ Abstraksi tidak mengurangi kejelasan kode
  ✓ Kamu sudah melihat duplikasi ini setidaknya 2-3 kali (Rule of Three)

Duplikasi LEBIH BAIK jika:
  ✗ Keduanya hanya kebetulan serupa saat ini tapi bisa diverge
  ✗ Abstraksi memerlukan banyak parameter untuk handle semua kasus
  ✗ Nama abstraksi sulit ditemukan karena konsepnya tidak jelas
  ✗ Kamu baru melihat duplikasi ini pertama kali (jangan prematur)

Rule of Three: abstraksi sebaiknya baru dibuat ketika kamu menemukan duplikasi yang sama untuk ketiga kalinya, bukan pertama atau kedua. Kali pertama tulis langsung, kali kedua buat catatan, kali ketiga buat abstraksi.


DRY pada Konfigurasi #

Konfigurasi yang tersebar adalah bentuk DRY violation yang paling mudah terjadi.

// ANTI-PATTERN: nilai konfigurasi hardcoded di banyak tempat
func connectDB() *sql.DB {
    db, _ := sql.Open("postgres", "host=localhost port=5432 dbname=myapp") // hardcoded
    db.SetMaxOpenConns(25) // magic number
    return db
}

func connectRedis() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr: "localhost:6379", // hardcoded lagi
    })
}

// BENAR: konfigurasi terpusat, dibaca sekali dari environment
type AppConfig struct {
    Database DatabaseConfig
    Redis    RedisConfig
    App      ServerConfig
}

type DatabaseConfig struct {
    DSN         string
    MaxOpenConn int
    MaxIdleConn int
}

func LoadConfig() AppConfig {
    return AppConfig{
        Database: DatabaseConfig{
            DSN:         os.Getenv("DATABASE_URL"),
            MaxOpenConn: getEnvInt("DB_MAX_OPEN_CONN", 25),
            MaxIdleConn: getEnvInt("DB_MAX_IDLE_CONN", 5),
        },
        Redis: RedisConfig{
            Addr: os.Getenv("REDIS_URL"),
        },
    }
}

// Semua service menerima config yang sama — satu sumber kebenaran
func NewServer(cfg AppConfig) *Server {
    db := connectDB(cfg.Database)
    redis := connectRedis(cfg.Redis)
    return &Server{db: db, redis: redis}
}

Ringkasan #

  • DRY adalah tentang satu representasi authoritative untuk setiap pengetahuan dalam sistem — bukan sekadar menghilangkan kode yang terlihat sama.
  • Duplikasi knowledge (aturan bisnis, konstanta, query) jauh lebih berbahaya dari duplikasi kode biasa — satu berubah, yang lain terlupa.
  • Empat bentuk pelanggaran: duplikasi logika bisnis, magic number/konstanta, query dan mapping data, serta validasi di banyak layer arsitektur.
  • Konstanta bernama adalah langkah DRY paling sederhana dan paling sering diabaikan — ganti semua magic number dengan konstanta yang menjelaskan maknanya.
  • Repository pattern adalah cara alami untuk menerapkan DRY pada query database — satu method di satu tempat, bukan query yang sama di berbagai layer.
  • DRY di Flutter: widget yang dipakai di banyak tempat harus diekstrak menjadi reusable widget dengan parameter untuk variasi.
  • Batas DRY: duplikasi lebih murah dari abstraksi yang salah — jangan abstrakt sesuatu yang hanya kebetulan terlihat sama saat ini.
  • Rule of Three: buat abstraksi ketika menemukan duplikasi untuk ketiga kalinya, bukan pertama atau kedua.
  • Konfigurasi terpusat adalah bentuk DRY yang sering dilewatkan — baca config sekali, sebarkan melalui dependency injection.

← Sebelumnya: SOLID   Berikutnya: KISS →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact