DRY — Don’t Repeat Yourself #

Ada satu kebiasaan yang hampir semua engineer pernah lakukan: menyalin blok kode dari satu tempat ke tempat lain karena “logikanya sama, tinggal sedikit diubah”. Pada sprint pertama terasa produktif. Pada sprint kesepuluh, ketika aturan bisnis berubah dan kamu harus mencari semua salinan itu satu per satu — barulah biayanya terasa. DRY (Don’t Repeat Yourself) adalah prinsip yang menyerang akar masalah ini: setiap knowledge dalam sistem harus memiliki satu representasi yang tunggal, jelas, dan authoritative. Artikel ini membahas apa yang benar-benar dimaksud DRY, bagaimana membedakan duplikasi yang berbahaya dari kesamaan yang kebetulan, bentuk-bentuk pelanggaran yang paling sering muncul di codebase nyata, teknik refactor dari level fungsi hingga arsitektur, dan — sama pentingnya — kapan DRY justru bisa menjadi jebakan.

Apa yang Sebenarnya Dimaksud “Repeat”? #

DRY sering disalahartikan sebagai larangan memiliki dua baris kode yang identik. Definisi yang lebih tepat justru lebih dalam dari itu: yang dilarang adalah duplikasi knowledge — aturan bisnis, logika, atau keputusan desain yang sama direpresentasikan di lebih dari satu tempat.

Perbedaan ini penting karena dua blok kode yang terlihat identik tidak selalu mewakili knowledge yang sama. Dan sebaliknya, duplikasi knowledge bisa tersembunyi di balik kode yang terlihat berbeda.

// Dua fungsi ini terlihat sama secara sintaksis
func calculateOrderTax(price float64) float64 {
    return price * 0.11 // aturan: PPN 11% untuk order
}

func calculateShippingTax(price float64) float64 {
    return price * 0.11 // aturan: PPN 11% untuk ongkos kirim
}

// Apakah ini pelanggaran DRY?
// Tergantung: apakah aturan PPN order dan PPN ongkos kirim SELALU bergerak bersama?
// Jika ya → ini duplikasi knowledge, perlu digabung
// Jika tidak → ini dua aturan berbeda yang kebetulan bernilai sama sekarang

Pertanyaan yang harus dijawab sebelum melakukan abstraksi: “Jika aturan ini berubah di satu tempat, apakah pasti berubah di tempat lain juga?” Jika jawabannya ya — itu duplikasi knowledge yang harus diselesaikan dengan DRY.

flowchart TD
    Q1{"Dua blok kode\nterlihat sama?"}
    Q2{"Mewakili knowledge\nyang sama?"}
    Q3{"Jika satu berubah,\napakah yang lain\nharus ikut berubah?"}
    AB["Abstraksi ke\nsatu sumber tunggal\n✓ DRY"]
    LEAVE["Biarkan terpisah\n✓ Juga benar"]

    Q1 -->|Ya| Q2
    Q1 -->|Tidak| LEAVE
    Q2 -->|Ya| AB
    Q2 -->|Tidak yakin| Q3
    Q3 -->|Ya| AB
    Q3 -->|Tidak| LEAVE

    style AB fill:#5CB85C,color:#fff
    style LEAVE fill:#4C9BE8,color:#fff

Bentuk Pelanggaran DRY yang Umum #

Pelanggaran DRY muncul dalam banyak bentuk. Beberapa mudah terlihat saat code review, yang lain tersembunyi di balik layer arsitektur.

Duplikasi Logika Bisnis #

Bentuk yang paling berbahaya karena dampaknya paling luas. Aturan bisnis yang tersebar di banyak tempat hampir pasti akan tidak sinkron seiring waktu.

// ANTI-PATTERN: aturan "user minimal 18 tahun" ada di dua tempat
// dengan error message yang berbeda pula

func RegisterUser(req RegisterRequest) error {
    if req.Age < 18 {
        return errors.New("user must be at least 18 years old")
    }
    // ... proses registrasi
    return nil
}

func UpdateProfile(req UpdateProfileRequest) error {
    if req.Age < 18 {
        return errors.New("age is not valid") // ← message berbeda!
    }
    // ... proses update
    return nil
}

// Masalah: product memutuskan usia minimum naik jadi 21.
// Siapa yang ingat ada validasi yang sama di UpdateProfile?
// Dan kenapa messagenya berbeda — bug laten yang sudah ada?

// BENAR: satu sumber kebenaran untuk aturan usia
const MinimumUserAge = 18

func validateAge(age int) error {
    if age < MinimumUserAge {
        return fmt.Errorf("minimum age is %d years old", MinimumUserAge)
    }
    return nil
}

func RegisterUser(req RegisterRequest) error {
    if err := validateAge(req.Age); err != nil {
        return err
    }
    return nil
}

func UpdateProfile(req UpdateProfileRequest) error {
    if err := validateAge(req.Age); err != nil {
        return err
    }
    return nil
}
// Aturan berubah? Ubah MinimumUserAge di satu tempat. Selesai.

Duplikasi Konstanta dan Magic Number #

Magic number yang tersebar adalah bentuk duplikasi yang paling mudah terlewat saat code review tapi paling mahal saat maintenance.

// ANTI-PATTERN: angka 3, 5, dan 30 tersebar tanpa penjelasan

func processOrder(order Order) {
    if order.RetryCount > 3 {  // ← 3 ini apa?
        markAsFailed(order)
    }
}

func processPayment(payment Payment) {
    if payment.Attempts > 3 {  // ← 3 yang sama? atau beda aturan?
        refund(payment)
    }
}

func cleanupSessions() {
    threshold := time.Now().Add(-30 * 24 * time.Hour) // ← 30 hari hardcoded
    db.Where("created_at < ?", threshold).Delete(&Session{})
}

func archiveOldLogs() {
    cutoff := time.Now().AddDate(0, 0, -30) // ← 30 hari lagi di sini
    // Ini 30 hari yang sama dengan cleanup session, atau berbeda?
}

// BENAR: konstanta yang terdokumentasi dengan baik
const (
    MaxOrderRetryCount    = 3
    MaxPaymentAttempts    = 3
    SessionRetentionDays  = 30
    LogArchiveDays        = 30
)

func processOrder(order Order) {
    if order.RetryCount > MaxOrderRetryCount {
        markAsFailed(order)
    }
}

func processPayment(payment Payment) {
    if payment.Attempts > MaxPaymentAttempts {
        refund(payment)
    }
}

func cleanupSessions() {
    threshold := time.Now().AddDate(0, 0, -SessionRetentionDays)
    db.Where("created_at < ?", threshold).Delete(&Session{})
}

Duplikasi Validasi Lintas Layer #

Ini adalah pelanggaran DRY yang paling banyak diperdebatkan. Sering terlihat di aplikasi web: validasi yang sama dijalankan di handler HTTP, di service, dan di repository.

// ANTI-PATTERN: validasi email dijalankan tiga kali di tiga layer
// dengan implementasi yang berbeda-beda

// Layer HTTP Handler
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)

    if req.Email == "" || !strings.Contains(req.Email, "@") {
        http.Error(w, "invalid email", 400)
        return
    }
    userService.Create(req)
}

// Layer Service
func (s *UserService) Create(req CreateUserRequest) error {
    if req.Email == "" {
        return errors.New("email required") // validasi lagi, tapi lebih lemah
    }
    return s.repo.Save(req)
}

// Layer Repository
func (r *UserRepository) Save(req CreateUserRequest) error {
    if req.Email == "" { // validasi lagi di layer paling bawah
        return errors.New("cannot save user without email")
    }
    // ...
}
// BENAR: validasi ada di satu tempat yang tepat
// Handler: validasi format input (parsing, tipe data)
// Service: validasi business rule (email sudah terdaftar, domain diblokir)
// Repository: tidak melakukan validasi — itu bukan tanggung jawabnya

// Satu package validator yang bisa digunakan di mana saja
package validator

import (
    "errors"
    "regexp"
)

var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

func ValidateEmail(email string) error {
    if email == "" {
        return errors.New("email is required")
    }
    if !emailRegex.MatchString(email) {
        return errors.New("invalid email format")
    }
    return nil
}

// Handler: gunakan validator untuk cek format
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)

    if err := validator.ValidateEmail(req.Email); err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    userService.Create(req)
}

// Service: fokus pada business rule, bukan validasi format
func (s *UserService) Create(req CreateUserRequest) error {
    existing, _ := s.repo.FindByEmail(req.Email)
    if existing != nil {
        return errors.New("email already registered")
    }
    return s.repo.Save(req)
}

Duplikasi di Level Arsitektur #

Bentuk paling halus: dua service yang berbeda mengimplementasikan logika yang identik secara independen karena tidak ada shared library atau shared domain model.

// ANTI-PATTERN: OrderService dan InvoiceService sama-sama
// mengimplementasikan kalkulasi pajak secara terpisah

// order/service.go
func (s *OrderService) calculateTotal(items []OrderItem) float64 {
    subtotal := 0.0
    for _, item := range items {
        subtotal += item.Price * float64(item.Quantity)
    }
    tax := subtotal * 0.11
    return subtotal + tax
}

// invoice/service.go
func (s *InvoiceService) computeAmount(lines []InvoiceLine) float64 {
    base := 0.0
    for _, line := range lines {
        base += line.UnitPrice * float64(line.Qty)
    }
    vat := base * 0.11 // ← sama persis, tapi tidak connected
    return base + vat
}

// BENAR: logika pajak ada di satu domain model yang digunakan bersama

// pricing/tax.go — satu sumber kebenaran untuk kalkulasi PPN
package pricing

const VATRate = 0.11

type TaxCalculator struct{}

func (tc TaxCalculator) Apply(subtotal float64) TaxResult {
    tax := subtotal * VATRate
    return TaxResult{
        Subtotal: subtotal,
        Tax:      tax,
        Total:    subtotal + tax,
    }
}

type TaxResult struct {
    Subtotal float64
    Tax      float64
    Total    float64
}

// order/service.go menggunakan TaxCalculator
func (s *OrderService) calculateTotal(items []OrderItem) pricing.TaxResult {
    subtotal := 0.0
    for _, item := range items {
        subtotal += item.Price * float64(item.Quantity)
    }
    return s.taxCalc.Apply(subtotal)
}

// invoice/service.go menggunakan TaxCalculator yang sama
func (s *InvoiceService) computeAmount(lines []InvoiceLine) pricing.TaxResult {
    base := 0.0
    for _, line := range lines {
        base += line.UnitPrice * float64(line.Qty)
    }
    return s.taxCalc.Apply(base)
}

DRY di Level Fungsi dan Method #

Tingkat paling dasar penerapan DRY adalah mengekstrak logika yang berulang ke fungsi atau method tersendiri. Ini bukan hanya soal menghindari copy-paste — tapi tentang memberi nama pada sebuah konsep domain.

// ANTI-PATTERN: logika paginasi diimplementasikan ulang di setiap handler

func ListOrdersHandler(w http.ResponseWriter, r *http.Request) {
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    if page <= 0 {
        page = 1
    }
    limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
    if limit <= 0 || limit > 100 {
        limit = 10
    }
    offset := (page - 1) * limit
    orders := orderRepo.Find(offset, limit)
    json.NewEncoder(w).Encode(orders)
}

func ListProductsHandler(w http.ResponseWriter, r *http.Request) {
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    if page <= 0 {
        page = 1 // ← copy-paste dari handler sebelumnya
    }
    limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
    if limit <= 0 || limit > 100 { // ← dan ini
        limit = 10
    }
    offset := (page - 1) * limit
    products := productRepo.Find(offset, limit)
    json.NewEncoder(w).Encode(products)
}

// BENAR: ekstrak ke fungsi dengan nama yang mencerminkan konsep domain
type Pagination struct {
    Page   int
    Limit  int
    Offset int
}

const (
    defaultPage  = 1
    defaultLimit = 10
    maxLimit      = 100
)

func parsePagination(r *http.Request) Pagination {
    page, _ := strconv.Atoi(r.URL.Query().Get("page"))
    if page <= 0 {
        page = defaultPage
    }

    limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
    switch {
    case limit <= 0:
        limit = defaultLimit
    case limit > maxLimit:
        limit = maxLimit
    }

    return Pagination{
        Page:   page,
        Limit:  limit,
        Offset: (page - 1) * limit,
    }
}

func ListOrdersHandler(w http.ResponseWriter, r *http.Request) {
    p := parsePagination(r) // satu baris, jelas maksudnya
    orders := orderRepo.Find(p.Offset, p.Limit)
    json.NewEncoder(w).Encode(orders)
}

func ListProductsHandler(w http.ResponseWriter, r *http.Request) {
    p := parsePagination(r)
    products := productRepo.Find(p.Offset, p.Limit)
    json.NewEncoder(w).Encode(products)
}

Perhatikan: dengan memberi nama parsePagination, kamu tidak hanya menghilangkan duplikasi — kamu juga mendokumentasikan bahwa “ini adalah logika paginasi”, bukan hanya “ini adalah beberapa baris yang melakukan sesuatu dengan page dan limit”.


DRY di Level Struct dan Encapsulation #

Cara Go idiomatis untuk menerapkan DRY di level domain adalah dengan melekatkan knowledge pada struct yang paling relevan melalui method.

// ANTI-PATTERN: knowledge tentang Order tersebar di luar struct Order itu sendiri
type Order struct {
    Items    []OrderItem
    Discount float64
    Status   string
}

// Di service:
func processOrder(order Order) {
    subtotal := 0.0
    for _, item := range order.Items {
        subtotal += item.Price * float64(item.Quantity)
    }
    total := subtotal - order.Discount // ← logika Order di luar Order

    if order.Status != "pending" && order.Status != "draft" { // ← juga di luar Order
        return errors.New("order cannot be processed")
    }
    // ...
}

// Di handler lain (duplikasi):
func cancelOrder(order Order) error {
    if order.Status != "pending" { // ← validasi yang mirip, tapi tidak identik
        return errors.New("only pending orders can be cancelled")
    }
    // ...
}

// BENAR: knowledge tentang Order melekat pada Order
type Order struct {
    Items    []OrderItem
    Discount float64
    Status   string
}

func (o Order) Subtotal() float64 {
    total := 0.0
    for _, item := range o.Items {
        total += item.Price * float64(item.Quantity)
    }
    return total
}

func (o Order) Total() float64 {
    return o.Subtotal() - o.Discount
}

func (o Order) IsProcessable() bool {
    return o.Status == "pending" || o.Status == "draft"
}

func (o Order) IsCancellable() bool {
    return o.Status == "pending"
}

// Service sekarang berbicara dalam bahasa domain, bukan mengulang logika
func processOrder(order Order) error {
    if !order.IsProcessable() {
        return errors.New("order cannot be processed in current status")
    }
    total := order.Total() // satu pemanggilan, knowledge ada di Order
    chargePayment(total)
    return nil
}

func cancelOrder(order Order) error {
    if !order.IsCancellable() {
        return errors.New("only pending orders can be cancelled")
    }
    return nil
}

DRY di Level Konfigurasi #

Konfigurasi yang tersebar adalah salah satu bentuk DRY violation yang paling umum di aplikasi production — dan sering jadi penyebab environment-specific bug yang sulit direproduksi.

// ANTI-PATTERN: nilai konfigurasi hardcoded dan tersebar
func connectDB() *sql.DB {
    db, _ := sql.Open("postgres", "host=localhost port=5432 ...") // ← hardcoded
    db.SetMaxOpenConns(10)   // ← magic number
    db.SetMaxIdleConns(5)    // ← magic number
    return db
}

func connectRedis() *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // ← hardcoded lagi
        PoolSize: 10,               // ← sama dengan DB? beda aturan?
    })
}

func main() {
    if os.Getenv("ENV") == "production" { // ← pengecekan env tersebar
        // setup production logger
    }
}

// BENAR: konfigurasi dibaca sekali dari satu sumber, digunakan bersama
type Config struct {
    Database DatabaseConfig
    Redis    RedisConfig
    App      AppConfig
}

type DatabaseConfig struct {
    DSN          string `env:"DB_DSN"`
    MaxOpenConns int    `env:"DB_MAX_OPEN_CONNS" envDefault:"10"`
    MaxIdleConns int    `env:"DB_MAX_IDLE_CONNS" envDefault:"5"`
}

type RedisConfig struct {
    Addr     string `env:"REDIS_ADDR" envDefault:"localhost:6379"`
    PoolSize int    `env:"REDIS_POOL_SIZE" envDefault:"10"`
}

type AppConfig struct {
    Env  string `env:"APP_ENV" envDefault:"development"`
    Port int    `env:"APP_PORT" envDefault:"8080"`
}

func LoadConfig() (*Config, error) {
    cfg := &Config{}
    if err := env.Parse(cfg); err != nil {
        return nil, fmt.Errorf("failed to load config: %w", err)
    }
    return cfg, nil
}

// main.go: config dibaca sekali, dioper ke semua komponen
func main() {
    cfg, err := LoadConfig()
    if err != nil {
        log.Fatal(err)
    }

    db := connectDB(cfg.Database)
    redisClient := connectRedis(cfg.Redis)
    server := NewServer(cfg.App, db, redisClient)
    server.Run()
}
flowchart TD
    ENV["Environment Variables\n/ Config File"]
    CFG["Config Struct\n(satu sumber kebenaran)"]
    DB["Database\nConnection"]
    REDIS["Redis\nConnection"]
    SERVER["HTTP Server"]
    WORKER["Background\nWorker"]

    ENV -->|dibaca sekali| CFG
    CFG -->|cfg.Database| DB
    CFG -->|cfg.Redis| REDIS
    CFG -->|cfg.App| SERVER
    CFG -->|cfg.App| WORKER

    style CFG fill:#4C9BE8,color:#fff
    style ENV fill:#5CB85C,color:#fff

DRY di Level Arsitektur — Shared Domain Model #

Di sistem dengan multiple service atau multiple handler, DRY di level arsitektur berarti meletakkan shared logic di shared layer — bukan menduplikasinya di setiap service.

internal/
  ├── domain/
  │   ├── pricing/
  │   │   ├── tax.go          ← kalkulasi PPN — digunakan semua service
  │   │   └── discount.go     ← aturan diskon — satu sumber kebenaran
  │   └── user/
  │       ├── age.go          ← validasi usia — digunakan semua service
  │       └── status.go       ← aturan status user
  ├── validator/
  │   ├── email.go            ← validasi email format
  │   └── phone.go            ← validasi nomor telepon
  ├── order/
  │   └── service.go          ← gunakan domain/pricing, bukan implementasi sendiri
  └── invoice/
      └── service.go          ← gunakan domain/pricing yang sama

Struktur ini memastikan setiap aturan bisnis hanya ada di satu tempat. Ketika PPN berubah dari 11% ke 12%, kamu ubah satu baris di domain/pricing/tax.go — dan semua service otomatis ikut.

flowchart TD
    DOMAIN["domain/pricing\n(shared knowledge)"]
    ORDER["order/service\n(uses domain)"]
    INVOICE["invoice/service\n(uses domain)"]
    REPORT["report/service\n(uses domain)"]
    TAX["VATRate = 0.11"]

    DOMAIN -->|export| TAX
    ORDER -->|import| DOMAIN
    INVOICE -->|import| DOMAIN
    REPORT -->|import| DOMAIN

    style DOMAIN fill:#4C9BE8,color:#fff
    style TAX fill:#5CB85C,color:#fff

DRY vs Over-Engineering — Batas yang Paling Sering Dilanggar #

Paradoks DRY: upaya menghilangkan duplikasi yang terlalu agresif bisa menghasilkan abstraksi yang salah — dan abstraksi yang salah jauh lebih mahal dari duplikasi sederhana.

// ANTI-PATTERN: abstraksi prematur karena dua fungsi "terlihat mirip"

// Dua fungsi ini kebetulan mirip sekarang...
func validateOrderAmount(amount float64) error {
    if amount <= 0 {
        return errors.New("amount must be positive")
    }
    return nil
}

func validateShippingCost(cost float64) error {
    if cost <= 0 {
        return errors.New("cost must be positive")
    }
    return nil
}

// Lalu seseorang "DRY-kan" mereka:
func validatePositiveFloat(value float64, fieldName string) error {
    if value <= 0 {
        return fmt.Errorf("%s must be positive", fieldName)
    }
    return nil
}

// Setahun kemudian: aturan berubah
// - Order amount: boleh negatif untuk retur/refund
// - Shipping cost: minimal 5000, bukan sekadar > 0
// Sekarang abstraksi itu menghalangi perubahan yang seharusnya independen
// BENAR: abstraksi hanya ketika knowledge benar-benar sama
// dan akan berubah bersama

// Aturan "harga item tidak boleh negatif" berlaku untuk semua item di katalog
// → ini knowledge yang sama, perlu diabstraksi
func validateCatalogItemPrice(price float64) error {
    if price < 0 {
        return errors.New("catalog item price cannot be negative")
    }
    return nil
}

// Aturan order amount dan shipping cost punya life cycle berbeda
// → biarkan terpisah meski sekarang mirip
func validateOrderAmount(amount float64) error {
    if amount <= 0 {
        return errors.New("order amount must be positive")
    }
    return nil
}

func validateShippingCost(cost float64) error {
    if cost < 5000 {
        return errors.New("minimum shipping cost is Rp5.000")
    }
    return nil
}

Rule of thumb yang berguna untuk memutuskan kapan mengabstraksi:

ABSTRAKSI AMAN ketika:
  ✓ Logika yang sama muncul tiga kali atau lebih (Rule of Three)
  ✓ Jika satu berubah, yang lain PASTI harus ikut berubah
  ✓ Kamu bisa memberi nama yang jelas untuk abstraksi tersebut
  ✓ Abstraksi tidak memerlukan banyak parameter opsional atau flag

TAHAN DIRI ketika:
  ✗ Baru dua occurrences — mungkin kebetulan mirip
  ✗ Tidak yakin apakah ini aturan yang sama atau berbeda
  ✗ Abstraksi memerlukan banyak parameter untuk handle berbagai kasus
  ✗ Abstraksi membuat kode lebih sulit dibaca dan diikuti alurnya
“Duplication is far cheaper than the wrong abstraction.” — Sandi Metz. Abstraksi yang salah terlalu mahal untuk di-undo karena sudah tersebar di mana-mana dan semua caller bergantung padanya. Duplikasi bisa dihilangkan kapan saja. Abstraksi yang salah memerlukan refactor besar-besaran.

Hubungan DRY dengan Prinsip Lain #

DRY tidak berdiri sendiri. Ia berkaitan erat dengan beberapa prinsip lain yang saling memperkuat:

PrinsipHubungan dengan DRY
SSOT (Single Source of Truth)DRY adalah penerapan SSOT di level kode. Keduanya menyatakan bahwa setiap fakta/knowledge hanya boleh ada di satu tempat.
SRP (Single Responsibility)SRP mendorong pemisahan tanggung jawab, yang secara natural mengurangi duplikasi — setiap tanggung jawab hanya ada di satu komponen.
YAGNI (You Aren’t Gonna Need It)YAGNI mencegah over-abstraction. Bersama DRY, mereka membentuk keseimbangan: abstraksi ketika ada duplikasi nyata, tapi tidak mengabstraksi sesuatu yang “mungkin nanti berulang”.
Clean ArchitectureDRY di level arsitektur berarti shared logic ada di domain layer, bukan diduplikasi di setiap layer atau service.
OCP (Open/Closed)Abstraksi yang dihasilkan DRY sering berbentuk interface atau fungsi yang bisa di-extend tanpa dimodifikasi.
flowchart LR
    DRY["DRY\n(Don't Repeat Yourself)"]
    SSOT["SSOT\nSatu sumber kebenaran"]
    SRP["SRP\nSatu tanggung jawab"]
    YAGNI["YAGNI\nJangan abstraksi\nsebelum waktunya"]
    CA["Clean Architecture\nShared domain model"]

    DRY <-->|ekuivalen di level kode| SSOT
    DRY -->|diperkuat oleh| SRP
    DRY <-->|diseimbangkan oleh| YAGNI
    DRY -->|mengarah ke| CA

    style DRY fill:#4C9BE8,color:#fff

Anti-Pattern dalam Satu Pandangan #

// ✗ Duplikasi logika bisnis — aturan yang sama di dua tempat
func registerUser(age int) error  { if age < 18 { return err } ... }
func updateProfile(age int) error { if age < 18 { return err } ... }

// ✗ Magic number — 3 ini berarti apa? retry order atau payment?
if retryCount > 3 { markFailed() }
if attempts > 3 { refund() }

// ✗ Validasi lintas layer — handler, service, dan repo semua validasi email
func handler()     { if email == "" { return error } }
func service()     { if email == "" { return error } }
func repository()  { if email == "" { return error } }

// ✗ Config hardcoded dan tersebar
func connectDB()    { sql.Open("postgres", "localhost:5432") }
func connectRedis() { redis.NewClient(&Options{Addr: "localhost:6379"}) }

// ✗ Knowledge domain di luar struct yang bersangkutan
subtotal := order.Items[0].Price + order.Items[1].Price // knowledge Order di handler
if order.Status != "pending" && order.Status != "draft" { ... } // juga di handler

Checklist Review DRY #

LOGIKA BISNIS:
  □ Setiap aturan bisnis hanya ada di satu fungsi/method/package
  □ Error message untuk aturan yang sama konsisten di semua tempat
  □ Perubahan aturan bisnis hanya memerlukan perubahan di satu tempat

KONSTANTA DAN KONFIGURASI:
  □ Tidak ada magic number yang tersebar — semua ada konstanta bernama
  □ Konfigurasi dibaca dari satu sumber dan dioper ke komponen
  □ Tidak ada string literal yang sama muncul di lebih dari satu file

VALIDASI:
  □ Validasi format input ada di satu package validator
  □ Validasi business rule ada di service/domain, bukan di handler dan repository
  □ Tidak ada logika validasi yang sama dengan implementasi berbeda di dua layer

STRUCT DAN DOMAIN:
  □ Knowledge tentang sebuah domain melekat pada struct domain tersebut
  □ Kalkulasi (tax, discount, total) ada di method struct, bukan di service
  □ Business state check (IsProcessable, IsCancellable) ada di method struct

ARSITEKTUR:
  □ Shared logic ada di shared domain package, bukan duplikat di setiap service
  □ Tidak ada dua service yang mengimplementasikan kalkulasi yang identik secara terpisah
  □ Perubahan di satu tempat tidak memerlukan grep untuk mencari salinan lainnya

Ringkasan #

  • DRY bukan larangan copy-paste kode — yang dilarang adalah duplikasi knowledge: aturan bisnis, logika, atau keputusan desain yang sama direpresentasikan di lebih dari satu tempat.
  • Tes yang tepat: “Jika aturan ini berubah di satu tempat, apakah yang lain pasti harus ikut berubah?” — jika ya, itu duplikasi knowledge yang harus diselesaikan.
  • Bentuk pelanggaran umum: duplikasi logika bisnis, magic number tersebar, validasi dijalankan ulang di setiap layer, konfigurasi hardcoded, dan knowledge domain berada di luar struct yang bersangkutan.
  • Penerapan di level fungsi: ekstrak logika berulang ke fungsi bernama yang mencerminkan konsep domain — memberi nama adalah bagian dari nilai DRY.
  • Penerapan di level struct: lekatkan knowledge pada struct yang paling relevan melalui method (order.Total(), order.IsProcessable()) agar service berbicara dalam bahasa domain.
  • Penerapan di level arsitektur: tempatkan shared logic di shared domain package — semua service mengimpor dari sana, bukan mengimplementasikan sendiri.
  • Bahaya abstraksi prematur: dua kode yang terlihat sama belum tentu mewakili knowledge yang sama. Gunakan Rule of Three: abstraksi aman setelah tiga occurrences, bukan dua.
  • Hubungan dengan YAGNI: DRY dan YAGNI bekerja bersama — abstraksi ketika ada duplikasi nyata, tapi jangan abstraksi sesuatu yang “mungkin nanti berulang”.
  • Kutipan kunci: “Duplication is far cheaper than the wrong abstraction.” Abstraksi yang salah lebih mahal dari duplikasi karena lebih sulit di-undo.

← Sebelumnya: SOLID   Berikutnya: KISS →

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