KISS #

Ada sebuah paradoks yang sering terjadi dalam software engineering: semakin berpengalaman seorang engineer, semakin ia tergoda untuk menambahkan lapisan abstraksi, pattern, dan framework. Kode yang “canggih” terasa lebih profesional. Padahal, kompleksitas yang tidak perlu adalah salah satu penyebab terbesar bug, keterlambatan delivery, dan sistem yang sulit dirawat. KISS — Keep It Simple, Stupid — adalah pengingat bahwa kesederhanaan bukan kelemahan, melainkan keahlian tersendiri. Buat solusi sesederhana mungkin yang masih bisa bekerja dengan benar, dan tambahkan kompleksitas hanya ketika ada kebutuhan nyata. Panduan ini membahas KISS dari definisi yang sering disalahpahami, tanda-tanda over-engineering yang perlu diwaspadai, lima contoh konkret di Go dan Dart, hingga kapan kompleksitas memang diperlukan dan tidak boleh disederhanakan paksa.

Apa yang Dimaksud KISS? #

KISS adalah singkatan dari Keep It Simple, Stupid — kadang juga diinterpretasikan sebagai Keep It Simple and Straightforward. Prinsipnya: pilih solusi paling sederhana yang benar-benar menyelesaikan masalah, tanpa kompleksitas tambahan yang tidak memberi nilai nyata.

Kesederhanaan di sini bukan berarti ceroboh atau naif. Ada perbedaan yang penting:

Sederhana (KISS):                  Naif (bukan KISS):
────────────────────────────────   ────────────────────────────────
Alur logika mudah diikuti          Tidak ada error handling
Struktur jelas dan eksplisit       Abaikan edge case
Kode yang diperlukan saja          Kode yang belum selesai
Mudah dipahami engineer lain       Mudah bagi penulisnya saat ini

Kompleks tidak perlu (bukan KISS): Kompleks yang memang perlu:
────────────────────────────────   ────────────────────────────────
Interface untuk sesuatu yang       Interface karena ada beberapa
tidak akan pernah di-swap          implementasi berbeda
Abstraksi untuk satu use case      Abstraksi untuk pola yang
yang tidak akan berkembang         benar-benar berulang
Generic parameter untuk            Generic karena memang ada
tipe yang sudah diketahui          multiple types yang valid

Kata kunci yang harus selalu ditanya sebelum menambahkan kompleksitas: “Apakah ini menyelesaikan masalah nyata yang ada sekarang?” Jika jawabannya tidak, atau jawabannya “mungkin suatu hari nanti” — itu tanda untuk tetap sederhana.


Tujuh Tanda Over-Engineering yang Harus Diwaspadai #

Over-engineering adalah pelanggaran KISS yang paling umum. Ini adalah tanda-tandanya:

1. Lebih banyak kode untuk mendukung framework internal
   daripada untuk business logic yang sebenarnya

2. Butuh waktu >5 menit untuk menjelaskan alur sederhana
   kepada engineer baru

3. Ada interface yang hanya punya satu implementasi
   dan tidak ada rencana konkret untuk implementasi lain

4. Ada layer abstraksi yang hanya meneruskan panggilan
   tanpa menambahkan behavior apapun

5. Ada generic/template yang hanya digunakan dengan satu tipe

6. Ada konfigurasi extensibility untuk fitur yang
   belum pernah diminta dan tidak ada di roadmap

7. Kode yang "fleksibel untuk masa depan" tapi tidak
   ada yang bisa menjelaskan masa depan yang mana

Kompleksitas memiliki biaya nyata yang tidak selalu terlihat: waktu onboarding yang lebih lama, bug yang lebih sulit dilacak, refactoring yang lebih mahal, dan kode yang lebih sulit ditest.


Contoh 1 — Fungsi yang Terlalu Pintar vs Jelas #

Salah satu pelanggaran KISS yang paling umum adalah kode yang “clever” — kode yang terasa elegan bagi penulisnya tapi membingungkan bagi pembaca.

// ANTI-PATTERN: terlalu banyak nested condition, tipe tidak jelas
func IsAdult(user map[string]interface{}) bool {
    if age, ok := user["age"]; ok {
        if ageInt, ok := age.(int); ok {
            if ageInt >= 18 {
                return true
            } else {
                return false
            }
        }
    }
    return false
}

// BENAR: tipe eksplisit, logika satu baris
type User struct {
    Age int
}

func IsAdult(user User) bool {
    return user.Age >= 18
}

Versi KISS lebih pendek, lebih aman (tipe compile-time), lebih mudah ditest, dan lebih jelas dibaca. Penggunaan map[string]interface{} untuk struct yang sudah diketahui bentuknya adalah over-generalization yang tidak memberikan manfaat apapun.

// ANTI-PATTERN: chaining yang membuat debug hampir mustahil
func ReadConfig() (*Config, error) {
    return parse(validate(load(readFile("config.yaml"))))
    // Jika ada error, dari layer mana? Tidak bisa tahu tanpa debugger
}

// BENAR: setiap langkah eksplisit, error jelas asal-usulnya
func ReadConfig(path string) (*Config, error) {
    raw, err := readFile(path)
    if err != nil {
        return nil, fmt.Errorf("readConfig: read file: %w", err)
    }

    data, err := load(raw)
    if err != nil {
        return nil, fmt.Errorf("readConfig: load: %w", err)
    }

    cfg, err := parse(data)
    if err != nil {
        return nil, fmt.Errorf("readConfig: parse: %w", err)
    }

    if err := validate(cfg); err != nil {
        return nil, fmt.Errorf("readConfig: validate: %w", err)
    }

    return cfg, nil
}

Versi kedua lebih panjang, tapi jauh lebih mudah di-debug, mudah dipahami, dan setiap error langsung menunjuk ke langkah mana yang gagal.


Contoh 2 — Abstraksi yang Prematur #

KISS dan YAGNI berjalan berdampingan: jangan buat abstraksi untuk kebutuhan yang belum ada.

// ANTI-PATTERN: abstraksi berlapis untuk sesuatu yang sederhana
// Interface ini baru berguna jika ada implementasi lain — yang belum ada
type PaymentProcessorFactory interface {
    Create(config PaymentConfig) PaymentProcessor
}

type PaymentProcessor interface {
    Process(ctx context.Context, req PaymentRequest) (*PaymentResult, error)
    Validate(req PaymentRequest) error
    Rollback(txID string) error
}

type StripePaymentProcessorFactory struct{}
func (f *StripePaymentProcessorFactory) Create(cfg PaymentConfig) PaymentProcessor {
    return &StripeProcessor{apiKey: cfg.APIKey}
}

// Service butuh factory hanya untuk mendapat processor
type PaymentService struct {
    factory PaymentProcessorFactory
}

// BENAR: langsung dan eksplisit — tambahkan abstraksi saat ada kebutuhan nyata
type PaymentService struct {
    stripe *stripe.Client // langsung pakai, tidak perlu factory+interface
}

func (s *PaymentService) CreateCharge(ctx context.Context, amount int64) error {
    _, err := s.stripe.Charges.New(&stripe.ChargeParams{Amount: stripe.Int64(amount)})
    return err
}

// Nanti, jika benar-benar butuh support banyak payment provider:
type PaymentGateway interface {
    Charge(ctx context.Context, amount int64) error
}
// Baru saat itu abstraksi ini memiliki nilai nyata

Contoh 3 — API Endpoint yang Terlalu Generic #

KISS sangat relevan di desain API. Endpoint yang terlalu generic terlihat fleksibel tapi sebenarnya lebih sulit digunakan dan didokumentasikan.

// ANTI-PATTERN: satu endpoint generic untuk semua operasi
POST /api/action
Content-Type: application/json

{
  "type": "create_user",
  "payload": {
    "name": "Budi",
    "email": "[email protected]"
  }
}

POST /api/action
{
  "type": "update_user",
  "id": "123",
  "payload": { ... }
}

// Masalah:
// - Tidak bisa pakai HTTP method yang semantically benar (PUT, DELETE)
// - Tidak ada schema yang jelas untuk setiap action
// - Swagger/OpenAPI tidak bisa mendokumentasikan dengan benar
// - Authorization per resource type sulit diimplementasikan

// BENAR: endpoint yang eksplisit sesuai resource
POST   /api/users          → buat user baru
GET    /api/users/{id}     → ambil user
PUT    /api/users/{id}     → update user
DELETE /api/users/{id}     → hapus user

Endpoint yang eksplisit lebih mudah dipahami, lebih mudah didokumentasikan dengan OpenAPI, lebih mudah di-secure dengan middleware authorization per route, dan lebih mudah di-test.


Contoh 4 — Conditional yang Bersih vs Nested #

Nested condition adalah salah satu bentuk kompleksitas tersembunyi yang paling sering ditemukan.

// ANTI-PATTERN: deeply nested condition — mental model sulit dibangun
func ProcessOrder(order Order) error {
    if order != nil {
        if order.UserID != "" {
            if order.Total > 0 {
                if order.Status == "pending" {
                    // logic utama terkubur di level 4
                    return saveOrder(order)
                } else {
                    return errors.New("order not pending")
                }
            } else {
                return errors.New("invalid total")
            }
        } else {
            return errors.New("missing user id")
        }
    } else {
        return errors.New("order is nil")
    }
    return nil
}

// BENAR: early return / guard clause — logic utama di level 1
func ProcessOrder(order Order) error {
    if order == nil {
        return errors.New("order is nil")
    }
    if order.UserID == "" {
        return errors.New("missing user id")
    }
    if order.Total <= 0 {
        return errors.New("invalid total")
    }
    if order.Status != "pending" {
        return errors.New("order not pending")
    }

    // Logic utama mudah ditemukan dan dibaca
    return saveOrder(order)
}

Early return pattern (guard clause) adalah salah satu teknik KISS paling efektif: validasi semua precondition di awal, dan biarkan logic utama berjalan di level teratas tanpa nesting.


Contoh 5 — KISS di Dart/Flutter #

Di Flutter, KISS sering dilanggar dalam pengelolaan state yang terlalu kompleks untuk kebutuhan yang sederhana.

// ANTI-PATTERN: BLoC/Cubit untuk state yang bisa ditangani dengan setState
// Untuk sebuah counter sederhana:
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}

class CounterState {
  final int count;
  CounterState({required this.count});
}

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(CounterState(count: 0)) {
    on<IncrementEvent>((event, emit) => emit(CounterState(count: state.count + 1)));
    on<DecrementEvent>((event, emit) => emit(CounterState(count: state.count - 1)));
  }
}

// Untuk counter yang hanya dipakai di satu widget:
// → 5 class, 30+ baris untuk sesuatu yang butuh 5 baris

// BENAR: setState untuk local state yang tidak perlu dibagi
class CounterWidget extends StatefulWidget {
  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(children: [
      Text('$_count'),
      ElevatedButton(
        onPressed: () => setState(() => _count++),
        child: Text('+'),
      ),
    ]);
  }
}
BLoC, Riverpod, Provider, dan state management lainnya sangat berguna untuk state yang perlu dibagi antar widget atau perlu persist. Tapi untuk state lokal yang hanya relevan dalam satu widget, setState adalah pilihan yang paling KISS — dan tidak ada yang salah dengan itu.

KISS dan Error Handling #

Go secara filosofis menerapkan KISS dalam error handling — error adalah nilai biasa yang dikembalikan, bukan exception tersembunyi. Tapi pola yang salah bisa membuat error handling tetap kompleks.

// ANTI-PATTERN: error handling yang berlebihan dan tidak informative
func getUserData(id string) (*UserData, error) {
    user, err := repo.FindUser(id)
    if err != nil {
        return nil, fmt.Errorf("error: %v", err) // "error: error" tidak berguna
    }
    profile, err := repo.FindProfile(user.ProfileID)
    if err != nil {
        wrapped := fmt.Errorf("getUserData failed: profile fetch error: inner error: %v", err)
        return nil, wrapped // terlalu verbose, informasi terduplikasi
    }
    return buildUserData(user, profile), nil
}

// BENAR: wrap error dengan konteks yang jelas dan tidak redundan
func getUserData(ctx context.Context, id string) (*UserData, error) {
    user, err := repo.FindUser(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("getUserData %s: find user: %w", id, err)
    }

    profile, err := repo.FindProfile(ctx, user.ProfileID)
    if err != nil {
        return nil, fmt.Errorf("getUserData %s: find profile: %w", id, err)
    }

    return buildUserData(user, profile), nil
}
// Error message: "getUserData usr-123: find profile: record not found"
// Jelas, tidak redundan, mudah dilacak

Hubungan KISS dengan Prinsip Lain #

KISS tidak berdiri sendiri — ia berinteraksi erat dengan prinsip engineering lainnya.

KISS + YAGNI = "Jangan tambahkan yang tidak dibutuhkan sekarang"
  → Bersama-sama mencegah premature optimization dan premature abstraction

KISS + DRY = Keseimbangan yang penting
  → DRY mendorong abstraksi untuk menghilangkan duplikasi
  → KISS mengingatkan bahwa abstraksi yang salah lebih buruk dari duplikasi
  → Solusinya: abstrak hanya yang benar-benar DRY (knowledge yang sama)

KISS + SRP = "Satu tanggung jawab, tapi jangan over-decompose"
  → SRP mendorong pemisahan concern
  → KISS mengingatkan bahwa terlalu banyak pemisahan bisa membuat
    alur sulit diikuti (terlalu banyak indirection)

KISS + SOLID = "Gunakan pattern saat ada kebutuhan nyata"
  → SOLID memberikan panduan desain yang baik
  → KISS mengingatkan untuk tidak menerapkan semua pattern di semua situasi

Kapan Kompleksitas Memang Diperlukan #

KISS bukan berarti selalu pilih solusi paling sederhana tanpa pertimbangan. Ada situasi di mana kompleksitas adalah harga yang pantas dibayar.

Kompleksitas yang justified:
  ✓ Framework atau library yang akan digunakan banyak tim
  ✓ Core platform dengan banyak consumer dan kebutuhan extensibility nyata
  ✓ Domain yang memang kompleks (rules engine, workflow engine, compiler)
  ✓ Performance-critical code yang butuh optimasi di luar yang obvious
  ✓ Security-critical code yang butuh defense-in-depth

Pendekatan yang tetap mengikuti KISS:
  Mulai sederhana → tambahkan kompleksitas hanya saat kebutuhan nyata muncul
  Bukan: antisipasi semua kemungkinan di awal, lalu sederhanakan belakangan

Anti-Pattern KISS dalam Satu Pandangan #

// ✗ Nested ternary yang sulit dibaca
result := cond1 ? (cond2 ? val1 : val2) : (cond3 ? val3 : val4)
// ✓ If-else yang eksplisit, atau early return

// ✗ Method chaining panjang yang sulit di-debug
result := service.Load().Filter().Transform().Validate().Save()
// ✓ Simpan intermediate result agar bisa di-inspect dan di-debug

// ✗ Interface untuk sesuatu yang tidak akan pernah berbeda implementasinya
type Logger interface { Log(msg string) }
type ConsoleLogger struct {}
// (dan tidak ada implementasi lain)
// ✓ Langsung pakai logger konkret kecuali testing memerlukan mock

// ✗ Config struct dengan 30 field untuk use case sederhana
type ServerConfig struct {
    Host, Port, ReadTimeout, WriteTimeout, MaxConns, ... // 30 field
}
// ✓ Mulai dengan yang dibutuhkan, tambahkan saat ada kebutuhan nyata

// ✗ Nama yang terlalu abstrak: Manager, Handler, Processor, Service
type UserManager struct{} // manager of what, exactly?
// ✓ Nama yang menjelaskan apa yang dilakukan: UserRegistrar, ProfileUpdater

Ringkasan #

  • KISS berarti pilih solusi paling sederhana yang benar-benar menyelesaikan masalah — bukan naif atau ceroboh, tapi jelas dan eksplisit.
  • Tujuh tanda over-engineering: lebih banyak kode untuk framework internal, sulit dijelaskan ke engineer baru, interface tanpa implementasi lain, layer yang hanya meneruskan panggilan, generic untuk satu tipe, dan “fleksibilitas masa depan” yang tidak konkret.
  • Early return / guard clause adalah teknik KISS yang sangat efektif — validasi di awal, logic utama di level teratas tanpa nesting.
  • Abstraksi prematur adalah pelanggaran KISS yang paling umum — tambahkan interface dan layer hanya saat kebutuhan nyata muncul, bukan antisipasi.
  • API yang eksplisit lebih KISS dari endpoint generic — mudah dipahami, didokumentasikan, di-secure, dan di-test.
  • State management di Flutter: setState untuk local state, BLoC/Riverpod untuk shared/global state — pilih berdasarkan kebutuhan nyata.
  • KISS + YAGNI mencegah premature optimization; KISS + DRY menghasilkan keseimbangan antara menghilangkan duplikasi dan menghindari abstraksi yang salah.
  • Kompleksitas yang justified memang ada — framework publik, domain yang memang kompleks, performance-critical code. Tapi mulailah sederhana, evolusikan saat kebutuhan nyata muncul.

← Sebelumnya: DRY   Berikutnya: YAGNI →

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