Merancang fungsi yang baik adalah keterampilan yang membedakan kode yang mudah dirawat dari kode yang cepat berubah legacy spaghetti. Banyak pemula fokus pada “bisa jalan” tetapi mengabaikan “mudah dibaca, diuji, dan diubah”. Fungsi yang baik memiliki satu tanggung jawab tunggal, nama yang jelas, jumlah parameter minimal, dan kontrak yang eksplisit. Pada chapter penutup bagian fungsi ini kita akan menelusuri prinsip single responsibility, perbandingan kode buruk vs kode baik, serta pedoman penamaan dan validasi input yang membuat fungsi lebih kokoh.

Single Responsibility Principle #

Setiap fungsi sebaiknya melakukan satu hal dengan baik. Bila sebuah fungsi mulai melakukan dua atau tiga hal sekaligus (misalnya: menghitung pajak, mencetak struk, dan mengirim email konfirmasi dalam satu fungsi), fungsi tersebut melanggar Single Responsibility Principle. Akibatnya, fungsi sulit diuji secara terpisah, sulit digunakan ulang, dan rentan bug saat salah satu bagian berubah.

Contoh Buruk: Satu Fungsi Mengerjakan Banyak Hal #

c
#include <stdio.h>

/* CONTOH BURUK: satu fungsi mengerjakan terlalu banyak hal */
void proses_pesanan(int id, int qty, double harga, double pajak,
                    double diskon, double ongkir, int *status_kirim) {
    /* Hitung subtotal */
    double subtotal = qty * harga;
    /* Terapkan diskon */
    double setelah_diskon = subtotal - (subtotal * diskon / 100);
    /* Hitung pajak */
    double setelah_pajak = setelah_diskon + (setelah_diskon * pajak / 100);
    /* Tambah ongkir */
    double total = setelah_pajak + ongkir;
    /* Cetak struk + update status kirim */
    printf("Pesanan #%d: subtotal=%.0f, diskon=%.0f%%, pajak=%.0f%%, ongkir=%.0f, total=%.0f\n",
           id, subtotal, diskon, pajak, ongkir, total);
    *status_kirim = 1;
}

int main(void) {
    int status = 0;
    proses_pesanan(1001, 3, 50000, 11, 10, 15000, &status);
    printf("Status kirim: %d\n", status);
    return 0;
}

Output yang Dihasilkan:

text
Pesanan #1001: subtotal=150000, diskon=10%, pajak=11%, ongkir=15000, total=164850
Status kirim: 1

Fungsi proses_pesanan di atas melanggar beberapa prinsip:

  1. Tujuh parameter—daftar parameter panjang mudah keliru urutannya. Bila pemanggil tertukar antara pajak dan diskon, output tetap berupa angka, bug tersembunyi.
  2. Mengerjakan tiga hal sekaligus: menghitung subtotal, menerapkan diskon/pajak/ongkir, mencetak struk, dan memperbarui status kirim. Tidak satu pun dari tugas ini dapat dipakai ulang terpisah.
  3. Side effect tersembunyi: pemanggil tidak tahu bahwa fungsi akan mencetak ke stdout dan memperbarui *status_kirim tanpa membaca body fungsi.

Contoh Baik: Pemisahan Fungsi #

Versi yang baik memisahkan tugas menjadi fungsi-fungsi kecil, masing-masing dengan satu tanggung jawab:

c
#include <stdio.h>

/* VERSI BAIK: dipisah menjadi fungsi-fungsi kecil, masing-masing satu tanggung jawab */

double hitung_subtotal(int qty, double harga) {
    return qty * harga;
}

double hitung_diskon(double subtotal, double persen_diskon) {
    return subtotal - (subtotal * persen_diskon / 100);
}

double hitung_pajak(double nilai, double persen_pajak) {
    return nilai + (nilai * persen_pajak / 100);
}

double hitung_total(double nilai, double ongkir) {
    return nilai + ongkir;
}

void cetak_struk(int id, double subtotal, double diskon,
                 double pajak, double ongkir, double total) {
    printf("Pesanan #%d: subtotal=%.0f, diskon=%.0f%%, pajak=%.0f%%, ongkir=%.0f, total=%.0f\n",
           id, subtotal, diskon, pajak, ongkir, total);
}

int main(void) {
    /* Tiap langkah dipanggil eksplisit, urutan terbaca jelas */
    double subtotal = hitung_subtotal(3, 50000);
    double setelah_diskon = hitung_diskon(subtotal, 10);
    double setelah_pajak = hitung_pajak(setelah_diskon, 11);
    double total = hitung_total(setelah_pajak, 15000);

    cetak_struk(1001, subtotal, 10, 11, 15000, total);
    return 0;
}

Output yang Dihasilkan:

text
Pesanan #1001: subtotal=150000, diskon=10%, pajak=11%, ongkir=15000, total=164850

Mari kita telaah baris-baris kode di atas:

  1. Baris 4 (double hitung_subtotal(int qty, double harga)): deklarasi fungsi dengan satu tugas—mengalikan qty dengan harga. Return type double menampung hasil perkalian tanpa truncation.
  2. Baris 8 (double hitung_diskon(double subtotal, double persen_diskon)): fungsi ini menerima subtotal dan persen diskon (misal 10 untuk 10%). Hasilnya: subtotal setelah dikurangi diskon.
  3. Baris 12 (double hitung_pajak(double nilai, double persen_pajak)): menerima nilai akhir dan persen pajak. Hasilnya: nilai setelah ditambah pajak. Nama parameter nilai generik—fungsi ini dapat dipakai untuk nilai post-diskon maupun nilai lain.
  4. Baris 16 (double hitung_total(double nilai, double ongkir)): menambahkan ongkir ke nilai akhir. Tanggung jawab tunggal: penambahan biaya kirim.
  5. Baris 20 (void cetak_struk(...)): fungsi void yang hanya mencetak struk. Tidak melakukan perhitungan—side effect-nya jelas dan terbatas pada output stdout.
  6. Baris 26-30 (pemanggilan bertahap di main): urutan pemanggilan membaca seperti langkah bisnis: subtotal → diskon → pajak → total → cetak. Setiap baris mewakili satu langkah logika bisnis.

Pada contoh baik di atas:

  • Fungsi hitung_subtotal, hitung_diskon, hitung_pajak, dan hitung_total masing-masing hanya satu tanggung jawab: melakukan perhitungan tertentu. Setiap fungsi dapat diuji secara terpisah—unit test menjadi mudah.
  • Fungsi cetak_struk hanya mencetak—tidak menghitung. Bila format cetak berubah, hanya fungsi ini yang dimodifikasi.
  • Pemanggil main membaca seperti urutan langkah yang jelas: hitung subtotal → terapkan diskon → hitung pajak → tambah ongkir → cetak. Logika bisnis menjadi self-documenting.

Penamaan dan Kontrak Fungsi yang Jelas #

Nama fungsi adalah dokumentasi terbaik—lebih sering dibaca daripada komentar. Pedoman penamaan:

  • Gunakan kata kerja untuk aksi: hitung_total, validasi_email, cetak_struk, proses_pesanan. Hindari kata benda untuk fungsi (total, email) yang dapat membingungkan dengan variabel.
  • Awalan is_ atau adalah_ untuk boolean: adalah_tahun_kabisat, is_valid. Konvensi ini langsung memberi sinyal bahwa return type adalah bool.
  • Hindari singkatan: hitung lebih jelas dari h; validasi_email lebih jelas dari v_email. Kompilasi tidak melambat karena nama panjang.
  • Konsisten: bila satu fungsi memakai hitung_* untuk fungsi matematika, pertahankan awalan yang sama untuk fungsi serupa.

Kontrak fungsi juga harus eksplisit: return type yang jelas, parameter yang diberi nama deskriptif, dan dokumentasi singkat tentang apa yang dikembalikan saat error.

c
#include <stdio.h>
#include <stdbool.h>

/* Kontrak fungsi: jelas dari nama + return type + parameter */
bool validasi_email(const char *email) {
    /* Implementasi minimal: cek ada karakter '@' dan minimal panjang 6 */
    if (email == NULL) return false;
    int i = 0;
    int ada_at = 0;
    while (email[i] != '\0') {
        if (email[i] == '@') ada_at = 1;
        i++;
    }
    return ada_at && i >= 6;
}

/* Fungsi yang mengembalikan error code (jelas) */
int validasi_umur(int umur) {
    if (umur < 0)   return -1;   /* Error: umur negatif */
    if (umur > 150) return -2;   /* Error: umur tidak realistis */
    return 0;                    /* Sukses */
}

int main(void) {
    const char *emails[] = {"[email protected]", "invalid", "[email protected]"};
    for (int i = 0; i < 3; i++) {
        bool ok = validasi_email(emails[i]);
        printf("Email %-20s : %s\n", emails[i], ok ? "valid" : "tidak valid");
    }

    int umur_test[] = {-5, 25, 200};
    for (int i = 0; i < 3; i++) {
        int rc = validasi_umur(umur_test[i]);
        printf("Umur %d: kode %d\n", umur_test[i], rc);
    }
    return 0;
}

Output yang Dihasilkan:

text
Email [email protected]     : valid
Email invalid              : tidak valid
Email [email protected]                : tidak valid
Umur -5: kode -1
Umur 25: kode 0
Umur 200: kode -2

Pada contoh di atas:

  • Fungsi validasi_email mengembalikan bool (valid/tidak). Nama validasi_email jelas menyatakan aksi dan input. Parameter const char *email memberi sinyal bahwa string tidak akan dimodifikasi. Kontrak implisit: bila email == NULL, kembalikan false (defensif terhadap input kosong).
  • Fungsi validasi_umur mengembalikan int kode status: 0 = sukses, -1 = umur negatif, -2 = umur tidak realistis. Kontrak eksplisit ini memudahkan pemanggil untuk membedakan jenis error—lebih informatif daripada sekadar bool.

Pedoman Tambahan Merancang Fungsi #

Beberapa aturan praktis yang meningkatkan kualitas fungsi:

  1. Fungsi pendek lebih baik: bila fungsi melebihi 50 baris, pertimbangkan untuk memecahnya. Fungsi pendek lebih mudah dibaca, diuji, dan di-debug.
  2. Hindari side effect tersembunyi: fungsi hitung_total seharusnya hanya menghitung—tidak mencetak, tidak memodifikasi global, tidak mengirim email. Side effect yang tidak diharapkan mempersulit pengujian dan debugging.
  3. Validasi input di awal: gunakan guard clause (if (input_invalid) return error;) di awal fungsi. Pola ini membuat sisa body fokus pada logika utama tanpa indentasi berlebihan.
  4. Dokumentasikan kontrak: bila fungsi mengembalikan kode error, dokumentasikan arti setiap kode di komentar atau dokumentasi API. Pemanggil perlu tahu cara membedakan “sukses dengan hasil kosong” dan “gagal”.
  5. Pertimbangkan pengujian: tulis fungsi dengan pemikiran “bagaimana saya menguji ini?”. Fungsi yang bergantung pada global state atau input tak terduga sulit diuji; fungsi yang menerima input melalui parameter dan mengembalikan output melalui return value mudah diuji.

Penutup Bagian Fungsi #

Kita telah menelusuri konsep fungsi dari pondasi hingga pola lanjutan: deklarasi vs definisi, prototype, parameter dan argument, return value, fungsi void, pass by value, pointer parameter, scope variabel, lokal vs global, static local, recursion, dan function pointer. Konsep-konsep ini adalah building block yang akan kita pakai di seluruh sisa kursus C—mulai dari array, pointer, struct, hingga program yang lebih kompleks. Pertahankan prinsip single responsibility, penamaan jelas, dan kontrak eksplisit di setiap fungsi yang kita tulis; kode yang baik adalah kode yang dapat dipahami pembaca lain (atau diri kita sendiri enam bulan ke depan) tanpa harus membaca seluruh implementasi.