Seleksi Kondisi

Seleksi Kondisi #

Seleksi kondisi adalah cara program mengambil keputusan — tanpanya, kode hanya bisa menjalankan satu jalur linear. Dart menyediakan beberapa konstruksi untuk ini: if-else yang fleksibel untuk logika kompleks, switch statement klasik untuk nilai diskrit, dan — mulai Dart 3 — switch expression dan pattern matching yang jauh lebih powerful dari keduanya. Yang membedakan developer berpengalaman dari pemula bukan pada kemampuan menulis if-else, tapi pada kemampuan memilih konstruksi yang tepat, menyusun kondisi agar terbaca dari atas ke bawah, dan mengenali kapan nested condition seharusnya difaktorisasi menjadi sesuatu yang lebih bersih.

if, else if, else #

Konstruksi if-else adalah bentuk percabangan paling dasar. Dart mengharuskan kondisi bertipe bool secara eksplisit — tidak ada truthy/falsy implisit seperti di JavaScript atau Python.

int skor = 82;

if (skor >= 90) {
  print('A — Sangat Baik');
} else if (skor >= 80) {
  print('B — Baik');
} else if (skor >= 70) {
  print('C — Cukup');
} else if (skor >= 60) {
  print('D — Kurang');
} else {
  print('E — Tidak Lulus');
}

Beberapa aturan sintaks yang perlu diperhatikan:

// Kurung kurawal wajib di Dart — berbeda dari C atau JavaScript
// ANTI-PATTERN: tanpa kurung kurawal (mengundang bug)
if (skor >= 90)
  print('A');   // ✗ meski valid secara sintaks, rentan salah baca
  print('B');   // baris ini selalu dieksekusi, bukan bagian dari if!

// BENAR: selalu gunakan kurung kurawal
if (skor >= 90) {
  print('A');   // ✓ jelas mana yang bagian dari if
}
print('selesai'); // jelas di luar blok if

Urutan Kondisi yang Tepat #

Urutan else if menentukan hasil — kondisi yang lebih spesifik harus ditulis lebih awal:

// ANTI-PATTERN: kondisi luas dulu — kondisi spesifik tidak pernah tercapai
int umur = 25;
if (umur >= 18) {
  print('dewasa');       // selalu masuk sini karena 25 >= 18
} else if (umur >= 65) {
  print('lansia');       // ✗ tidak pernah tercapai — syaratnya mustahil terpenuhi
}

// BENAR: kondisi spesifik lebih dulu, lalu yang lebih umum
if (umur >= 65) {
  print('lansia');
} else if (umur >= 18) {
  print('dewasa');
} else {
  print('minor');
}

Guard Clause dan Early Return #

Guard clause adalah teknik menulis kondisi “gagal cepat” di awal fungsi — validasi semua prasyarat terlebih dahulu, baru jalankan logika utama. Ini mengurangi nesting dan membuat jalur sukses terbaca linear dari atas ke bawah.

// ANTI-PATTERN: nesting dalam yang menyembunyikan logika utama
double hitungDiskon(Pengguna? pengguna, Produk? produk, int qty) {
  if (pengguna != null) {
    if (produk != null) {
      if (qty > 0) {
        if (pengguna.isPremium) {
          if (produk.sedangDiskon) {
            return produk.harga * qty * 0.2; // logika utama terkubur di level 6
          } else {
            return produk.harga * qty * 0.1;
          }
        } else {
          return 0;
        }
      } else {
        return 0;
      }
    } else {
      return 0;
    }
  } else {
    return 0;
  }
}

// BENAR: guard clause — validasi dulu, baru logika utama
double hitungDiskon(Pengguna? pengguna, Produk? produk, int qty) {
  // Validasi prasyarat di awal — return segera jika tidak terpenuhi
  if (pengguna == null) return 0;
  if (produk == null) return 0;
  if (qty <= 0) return 0;

  // Logika utama — bersih, tidak bersarang, mudah dibaca
  if (!pengguna.isPremium) return 0;
  return produk.harga * qty * (produk.sedangDiskon ? 0.2 : 0.1);
}

Pola guard clause juga sangat efektif untuk validasi input dan null safety:

void prosesOrder(String? idPengguna, List<ItemKeranjang> keranjang) {
  // Guard: validasi semua input di atas
  if (idPengguna == null || idPengguna.isEmpty) {
    throw ArgumentError('ID pengguna tidak boleh kosong');
  }
  if (keranjang.isEmpty) {
    throw StateError('Keranjang tidak boleh kosong');
  }
  if (keranjang.any((item) => item.qty <= 0)) {
    throw ArgumentError('Semua item harus memiliki qty positif');
  }

  // Dari sini, semua prasyarat sudah terpenuhi — fokus ke logika bisnis
  final total = keranjang.fold<double>(
    0,
    (acc, item) => acc + item.harga * item.qty,
  );
  simpanOrder(idPengguna, keranjang, total);
}
flowchart TD
    A[Masuk fungsi] --> B{Prasyarat 1 terpenuhi?}
    B -- Tidak --> B1[Return / Throw segera]
    B -- Ya --> C{Prasyarat 2 terpenuhi?}
    C -- Tidak --> C1[Return / Throw segera]
    C -- Ya --> D{Prasyarat 3 terpenuhi?}
    D -- Tidak --> D1[Return / Throw segera]
    D -- Ya --> E[Jalankan logika utama\ntanpa nesting]
    E --> F[Return hasil]

switch Statement Klasik #

switch bekerja dengan membandingkan satu ekspresi terhadap beberapa nilai konstanta. Di Dart, switch mendukung String, int, enum, dan tipe yang mengimplementasikan == secara tepat.

String hari = 'Senin';

switch (hari) {
  case 'Senin':
  case 'Selasa':
  case 'Rabu':
  case 'Kamis':
  case 'Jumat':
    print('Hari kerja');
    break;
  case 'Sabtu':
  case 'Minggu':
    print('Akhir pekan');
    break;
  default:
    print('Bukan nama hari yang valid');
}

Fall-through dan break #

Di Dart, setiap case yang memiliki kode harus diakhiri dengan break, return, throw, atau continue. Fall-through (melanjutkan ke case berikutnya tanpa break) hanya diizinkan jika case-nya kosong — seperti pada contoh 'Senin' sampai 'Jumat' di atas.

int kode = 2;

switch (kode) {
  case 1:
    print('Satu');
    // ✗ error: missing break — Dart tidak mengizinkan fall-through berisi kode
  case 2:
    print('Dua');
    break;
}

// Fall-through yang diizinkan: case kosong
switch (kode) {
  case 1:   // ✓ fall-through dari case kosong
  case 2:
    print('Satu atau Dua');
    break;
  case 3:
    print('Tiga');
    break;
}

switch dengan Enum #

switch paling elegan digunakan bersama enum — dan Dart bisa memperingatkan jika ada nilai enum yang tidak ditangani:

enum StatusPesanan { menunggu, diproses, dikirim, selesai, dibatalkan }

void tampilkanStatus(StatusPesanan status) {
  switch (status) {
    case StatusPesanan.menunggu:
      print('Menunggu konfirmasi penjual');
      break;
    case StatusPesanan.diproses:
      print('Sedang disiapkan di gudang');
      break;
    case StatusPesanan.dikirim:
      print('Dalam perjalanan ke alamatmu');
      break;
    case StatusPesanan.selesai:
      print('Pesanan telah diterima');
      break;
    case StatusPesanan.dibatalkan:
      print('Pesanan dibatalkan');
      break;
    // Tanpa default — Dart analyzer akan warning jika ada enum baru yang belum ditangani
  }
}
Saat menggunakan switch dengan enum, hindari default jika memungkinkan. Tanpa default, Dart analyzer akan memberikan peringatan setiap kali ada nilai enum baru yang belum ditangani di switch — ini adalah safety net yang berharga saat enum berkembang.

Switch Expression (Dart 3) #

Dart 3 memperkenalkan switch expression — versi switch yang menghasilkan nilai, bukan menjalankan statement. Ini adalah salah satu fitur terbesar Dart 3 dan mengubah cara kita menulis banyak logika kondisional.

// switch statement klasik — verbose, butuh variabel sementara
String deskripsikan(StatusPesanan status) {
  String hasil;
  switch (status) {
    case StatusPesanan.menunggu:
      hasil = 'Menunggu konfirmasi';
      break;
    case StatusPesanan.diproses:
      hasil = 'Sedang diproses';
      break;
    case StatusPesanan.dikirim:
      hasil = 'Sedang dikirim';
      break;
    case StatusPesanan.selesai:
      hasil = 'Selesai';
      break;
    case StatusPesanan.dibatalkan:
      hasil = 'Dibatalkan';
      break;
  }
  return hasil;
}

// switch expression (Dart 3) — ringkas, langsung menghasilkan nilai
String deskripsikan(StatusPesanan status) => switch (status) {
  StatusPesanan.menunggu   => 'Menunggu konfirmasi',
  StatusPesanan.diproses   => 'Sedang diproses',
  StatusPesanan.dikirim    => 'Sedang dikirim',
  StatusPesanan.selesai    => 'Selesai',
  StatusPesanan.dibatalkan => 'Dibatalkan',
};
// Tidak ada 'default' — switch expression HARUS exhaustive untuk enum

Switch expression bisa digunakan langsung di mana pun sebuah nilai diharapkan:

// Di dalam assignment
double tarif = switch (kategoriPengguna) {
  KategoriPengguna.reguler => 0.0,
  KategoriPengguna.premium => 0.10,
  KategoriPengguna.vip     => 0.20,
};

// Di dalam interpolasi string
print('Diskon kamu: ${switch (level) {
  1 => '5%',
  2 => '10%',
  3 => '15%',
  _ => '0%',  // _ adalah wildcard/default
}}');

// Di dalam return langsung
int hitungPoin(String aksi) => switch (aksi) {
  'login'    => 5,
  'beli'     => 20,
  'review'   => 10,
  'referral' => 50,
  _          => 0,
};

Wildcard _ vs default #

Di switch expression, gunakan _ (wildcard) sebagai fallback — ia menangkap semua nilai yang tidak cocok:

// switch expression dengan wildcard
String kategoriUmur(int umur) => switch (umur) {
  < 13         => 'Anak',
  >= 13 && < 18 => 'Remaja',
  >= 18 && < 60 => 'Dewasa',
  _            => 'Lansia',    // menangkap semua kasus lainnya
};

Pattern Matching (Dart 3) #

Pattern matching adalah ekstensi dari switch yang memungkinkan mencocokkan struktur data, bukan hanya nilai. Ini mengubah switch dari sekadar “pilih berdasarkan nilai” menjadi “destrukturisasi dan tangkap data secara bersamaan”.

Matching Berdasarkan Tipe #

void gambarkan(Object bentuk) {
  switch (bentuk) {
    case Lingkaran(jariJari: var r):
      print('Lingkaran dengan jari-jari $r, luas: ${3.14 * r * r}');
    case Persegi(sisi: var s):
      print('Persegi dengan sisi $s, luas: ${s * s}');
    case PersegiPanjang(panjang: var p, lebar: var l):
      print('Persegi panjang $p×$l, luas: ${p * l}');
  }
}

Relational Pattern #

String kategorikanSuhu(double celsius) => switch (celsius) {
  < 0         => 'Beku',
  >= 0 && < 15 => 'Dingin',
  >= 15 && < 25 => 'Sejuk',
  >= 25 && < 35 => 'Hangat',
  _            => 'Panas',
};

List dan Map Pattern #

Pattern matching bisa mendekonstruksi koleksi secara langsung:

void prosesKoordinat(List<double> coords) {
  switch (coords) {
    case []:
      print('Daftar kosong');
    case [double x]:
      print('Satu dimensi: $x');
    case [double x, double y]:
      print('Dua dimensi: ($x, $y)');
    case [double x, double y, double z]:
      print('Tiga dimensi: ($x, $y, $z)');
    case [_, _, _, ...]:
      print('Lebih dari 3 dimensi');
  }
}

// Map pattern
void bacaKonfigurasi(Map<String, dynamic> config) {
  switch (config) {
    case {'host': String host, 'port': int port}:
      print('Koneksi ke $host:$port');
    case {'host': String host}:
      print('Koneksi ke $host dengan port default');
    default:
      throw FormatException('Format konfigurasi tidak valid');
  }
}

Guard Clause dalam Pattern (when) #

Pattern bisa dilengkapi dengan kondisi tambahan menggunakan kata kunci when:

String evaluasiNilai(int skor, bool sudahRemidi) => switch (skor) {
  >= 90                              => 'A — Sangat Baik',
  >= 80                              => 'B — Baik',
  >= 70                              => 'C — Cukup',
  >= 60 when !sudahRemidi            => 'D — Bisa Remidi',
  >= 60 when sudahRemidi             => 'D — Sudah Remidi, Tetap D',
  _                                  => 'E — Tidak Lulus',
};

// when juga bisa di switch statement biasa
void proses(Object nilai) {
  switch (nilai) {
    case int n when n > 0:
      print('Integer positif: $n');
    case int n when n < 0:
      print('Integer negatif: $n');
    case int _:
      print('Nol');
    case String s when s.isNotEmpty:
      print('String tidak kosong: $s');
    default:
      print('Tipe atau nilai lain');
  }
}

Sealed Class dan Exhaustive Matching #

Sealed class (Dart 3) adalah kelas yang hanya bisa di-extend atau di-implement di dalam file yang sama. Kombinasi sealed class dengan switch expression menghasilkan percabangan yang exhaustive — compiler menjamin semua kemungkinan kasus tertangani.

// Definisi sealed class — semua subtipe harus di file yang sama
sealed class HasilOperasi {}

class Sukses extends HasilOperasi {
  final dynamic data;
  const Sukses(this.data);
}

class GagalValidasi extends HasilOperasi {
  final String pesan;
  const GagalValidasi(this.pesan);
}

class GagalJaringan extends HasilOperasi {
  final int kodeHttp;
  const GagalJaringan(this.kodeHttp);
}

// Switch expression dengan sealed class — exhaustive secara otomatis
String tanganiHasil(HasilOperasi hasil) => switch (hasil) {
  Sukses(data: var d)         => 'Berhasil: $d',
  GagalValidasi(pesan: var p) => 'Validasi gagal: $p',
  GagalJaringan(kodeHttp: var k) => 'Gagal jaringan: HTTP $k',
  // Tidak perlu default — semua subtipe sudah ditangani
  // Jika ada subtipe baru ditambahkan, compiler langsung error di sini
};
// ANTI-PATTERN: menggunakan inheritance terbuka dengan switch — rapuh
abstract class Bentuk {}
class Lingkaran extends Bentuk { ... }
class Persegi extends Bentuk { ... }

String gambar(Bentuk b) => switch (b) {
  Lingkaran()  => 'lingkaran',
  Persegi()    => 'persegi',
  _            => throw UnimplementedError(), // ✗ harus ada default karena tidak exhaustive
};
// Jika ada kelas Segitiga baru, tidak ada peringatan compiler — bug tersembunyi

// BENAR: gunakan sealed class untuk exhaustive matching
sealed class Bentuk {}  // hanya Lingkaran dan Persegi yang bisa ada
// Compiler LANGSUNG error jika switch tidak menangani semua subtipe

Kapan Menggunakan Masing-masing Konstruksi #

Memilih konstruksi yang tepat bukan sekadar selera — setiap konstruksi memiliki kekuatan yang berbeda:

flowchart TD
    A{Apa yang perlu diputuskan?} --> B{Nilai tunggal vs\nbeberapa kondisi?}
    B -- Satu kondisi sederhana --> C[if / ternary ?:]
    B -- Banyak nilai diskrit --> D{Perlu menghasilkan\nnilai?}
    D -- Ya --> E{Dart 3+?}
    E -- Ya --> F[switch expression]
    E -- Tidak --> G[switch statement\ndengan variabel sementara]
    D -- Tidak --> H[switch statement]
    B -- Struktur data kompleks --> I{Dart 3+?}
    I -- Ya --> J[Pattern matching\nswitch]
    I -- Tidak --> K[if + is + cast]
    A --> L{Validasi prasyarat\ndi awal fungsi?}
    L -- Ya --> M[Guard clause\nearly return]
GUNAKAN if/else ketika:
  ✓ Kondisi melibatkan rentang nilai (>= 18, < 60)
  ✓ Kondisi melibatkan beberapa variabel berbeda
  ✓ Hanya ada 2-3 cabang
  ✓ Kondisi boolean kompleks dengan && dan ||

GUNAKAN switch statement ketika:
  ✓ Membandingkan satu variabel terhadap banyak nilai diskrit
  ✓ Bekerja dengan enum dan butuh exhaustiveness warning
  ✓ Ada kasus fall-through (case kosong yang berurutan)

GUNAKAN switch expression (Dart 3) ketika:
  ✓ Setiap cabang menghasilkan nilai (bukan menjalankan side effect)
  ✓ Bekerja dengan enum atau sealed class
  ✓ Ingin exhaustiveness dijamin compiler

GUNAKAN pattern matching (Dart 3) ketika:
  ✓ Perlu mendekonstruksi objek sekaligus memeriksa tipenya
  ✓ Bekerja dengan sealed class hierarchy
  ✓ Pattern melibatkan List, Map, atau relational condition
  ✓ Perlu guard clause (when) di dalam pola

GUNAKAN guard clause / early return ketika:
  ✓ Fungsi memiliki beberapa prasyarat yang harus dipenuhi
  ✓ Kode utama terkubur di dalam banyak level nesting
  ✓ Ingin jalur "gagal" jelas terpisah dari jalur "sukses"

Anti-Pattern Seleksi Kondisi #

Kondisi Boolean Redundan #

// ANTI-PATTERN: membandingkan bool dengan true/false secara eksplisit
bool aktif = cekStatus();
if (aktif == true) { ... }   // ✗ redundan
if (aktif == false) { ... }  // ✗ redundan
if (aktif != true) { ... }   // ✗ redundan

// BENAR: gunakan bool secara langsung
if (aktif) { ... }           // ✓
if (!aktif) { ... }          // ✓

Return Boolean dari If-Else #

// ANTI-PATTERN: return true/false dari if-else padahal kondisinya sudah bool
bool cekEligibel(int umur, double saldo) {
  if (umur >= 18 && saldo >= 500000) {
    return true;   // ✗ tidak perlu — kondisinya sendiri sudah bool
  } else {
    return false;
  }
}

// BENAR: return ekspresi boolean langsung
bool cekEligibel(int umur, double saldo) {
  return umur >= 18 && saldo >= 500000;  // ✓
}

// Atau dengan arrow function
bool cekEligibel(int umur, double saldo) =>
    umur >= 18 && saldo >= 500000;

Nesting Berlebihan #

// ANTI-PATTERN: nesting if di dalam if yang bisa di-flatten
void validasiForm(String nama, String email, int umur) {
  if (nama.isNotEmpty) {
    if (email.contains('@')) {
      if (umur >= 18) {
        simpan(nama, email, umur);
      } else {
        print('Umur minimal 18 tahun');
      }
    } else {
      print('Email tidak valid');
    }
  } else {
    print('Nama tidak boleh kosong');
  }
}

// BENAR: guard clause membalik kondisi dan keluar lebih awal
void validasiForm(String nama, String email, int umur) {
  if (nama.isEmpty) { print('Nama tidak boleh kosong'); return; }
  if (!email.contains('@')) { print('Email tidak valid'); return; }
  if (umur < 18) { print('Umur minimal 18 tahun'); return; }

  simpan(nama, email, umur);
}

Switch Tanpa Default untuk Non-Enum #

// ANTI-PATTERN: switch String tanpa default — kasus tidak tertangani diam-diam
void prosesKomando(String komando) {
  switch (komando) {
    case 'start':
      mulai();
      break;
    case 'stop':
      berhenti();
      break;
    // ✗ tidak ada default — komando 'restart' tidak dilakukan apa-apa
  }
}

// BENAR: selalu sediakan default untuk switch non-enum
void prosesKomando(String komando) {
  switch (komando) {
    case 'start':
      mulai();
      break;
    case 'stop':
      berhenti();
      break;
    default:
      throw ArgumentError('Komando tidak dikenal: $komando'); // ✓ gagal dengan jelas
  }
}

Ternary Bersarang Terlalu Dalam #

// ANTI-PATTERN: ternary lebih dari dua level — sangat sulit dibaca
String hasil = a > b
    ? (a > c ? 'a' : (c > d ? 'c' : 'd'))
    : (b > c ? 'b' : (c > d ? 'c' : 'd'));

// BENAR: switch expression lebih jelas untuk banyak cabang
String terbesar(int a, int b, int c, int d) {
  int maks = [a, b, c, d].reduce((curr, next) => curr > next ? curr : next);
  return switch (maks) {
    _ when maks == a => 'a',
    _ when maks == b => 'b',
    _ when maks == c => 'c',
    _                => 'd',
  };
}

// Atau lebih sederhana dengan logika yang berbeda:
String terbesar(int a, int b, int c, int d) {
  final nilai = {'a': a, 'b': b, 'c': c, 'd': d};
  return nilai.entries.reduce((e1, e2) => e1.value > e2.value ? e1 : e2).key;
}

Ringkasan #

  • if-else adalah pilihan utama untuk kondisi yang melibatkan rentang nilai, beberapa variabel, atau logika boolean kompleks. Selalu gunakan kurung kurawal meski hanya satu statement.
  • Guard clause dan early return mengurangi nesting secara dramatis — validasi semua prasyarat di atas, baru jalankan logika utama di bawah. Kode jadi terbaca dari atas ke bawah tanpa memerlukan nesting dalam.
  • switch statement paling cocok untuk nilai diskrit dan enum. Hindari default saat menggunakan enum agar compiler bisa memperingatkan nilai yang belum ditangani.
  • Switch expression (Dart 3) adalah switch yang menghasilkan nilai — ringkas, exhaustive untuk enum, dan bisa digunakan langsung di dalam ekspresi manapun. Gunakan _ sebagai wildcard pengganti default.
  • Pattern matching (Dart 3) memungkinkan mendekonstruksi dan mencocokkan struktur data sekaligus — tipe, List, Map, relational pattern, semua bisa dikombinasikan dengan when untuk guard tambahan.
  • Sealed class + switch expression menghasilkan percabangan yang dijamin exhaustive oleh compiler — jika subtipe baru ditambahkan, semua switch yang tidak menanganinya langsung error.
  • Hindari membandingkan bool dengan == true atau == false, mengembalikan true/false dari if-else padahal ekspresi kondisinya sendiri sudah bool, dan nesting ternary lebih dari dua level.
  • Urutan kondisi penting — kondisi yang lebih spesifik harus selalu ditulis lebih awal; kondisi yang lebih umum di bawah, agar tidak “menutupi” kasus yang seharusnya ditangani lebih dulu.

← Sebelumnya: Operator   Berikutnya: Perulangan →

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