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 #
#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:
Pesanan #1001: subtotal=150000, diskon=10%, pajak=11%, ongkir=15000, total=164850
Status kirim: 1Fungsi proses_pesanan di atas melanggar beberapa prinsip:
- Tujuh parameter—daftar parameter panjang mudah keliru urutannya. Bila pemanggil tertukar antara
pajakdandiskon, output tetap berupa angka, bug tersembunyi. - 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.
- Side effect tersembunyi: pemanggil tidak tahu bahwa fungsi akan mencetak ke
stdoutdan memperbarui*status_kirimtanpa membaca body fungsi.
Contoh Baik: Pemisahan Fungsi #
Versi yang baik memisahkan tugas menjadi fungsi-fungsi kecil, masing-masing dengan satu tanggung jawab:
#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:
Pesanan #1001: subtotal=150000, diskon=10%, pajak=11%, ongkir=15000, total=164850Mari kita telaah baris-baris kode di atas:
- Baris 4 (
double hitung_subtotal(int qty, double harga)): deklarasi fungsi dengan satu tugas—mengalikanqtydenganharga. Return typedoublemenampung hasil perkalian tanpa truncation. - Baris 8 (
double hitung_diskon(double subtotal, double persen_diskon)): fungsi ini menerima subtotal dan persen diskon (misal10untuk 10%). Hasilnya: subtotal setelah dikurangi diskon. - Baris 12 (
double hitung_pajak(double nilai, double persen_pajak)): menerima nilai akhir dan persen pajak. Hasilnya: nilai setelah ditambah pajak. Nama parameternilaigenerik—fungsi ini dapat dipakai untuk nilai post-diskon maupun nilai lain. - Baris 16 (
double hitung_total(double nilai, double ongkir)): menambahkan ongkir ke nilai akhir. Tanggung jawab tunggal: penambahan biaya kirim. - Baris 20 (
void cetak_struk(...)): fungsivoidyang hanya mencetak struk. Tidak melakukan perhitungan—side effect-nya jelas dan terbatas pada outputstdout. - 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, danhitung_totalmasing-masing hanya satu tanggung jawab: melakukan perhitungan tertentu. Setiap fungsi dapat diuji secara terpisah—unit test menjadi mudah. - Fungsi
cetak_strukhanya mencetak—tidak menghitung. Bila format cetak berubah, hanya fungsi ini yang dimodifikasi. - Pemanggil
mainmembaca 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_atauadalah_untuk boolean:adalah_tahun_kabisat,is_valid. Konvensi ini langsung memberi sinyal bahwa return type adalahbool. - Hindari singkatan:
hitunglebih jelas darih;validasi_emaillebih jelas dariv_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.
#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:
Email [email protected] : valid
Email invalid : tidak valid
Email [email protected] : tidak valid
Umur -5: kode -1
Umur 25: kode 0
Umur 200: kode -2Pada contoh di atas:
- Fungsi
validasi_emailmengembalikanbool(valid/tidak). Namavalidasi_emailjelas menyatakan aksi dan input. Parameterconst char *emailmemberi sinyal bahwa string tidak akan dimodifikasi. Kontrak implisit: bilaemail == NULL, kembalikanfalse(defensif terhadap input kosong). - Fungsi
validasi_umurmengembalikanintkode status:0= sukses,-1= umur negatif,-2= umur tidak realistis. Kontrak eksplisit ini memudahkan pemanggil untuk membedakan jenis error—lebih informatif daripada sekadarbool.
Pedoman Tambahan Merancang Fungsi #
Beberapa aturan praktis yang meningkatkan kualitas fungsi:
- Fungsi pendek lebih baik: bila fungsi melebihi 50 baris, pertimbangkan untuk memecahnya. Fungsi pendek lebih mudah dibaca, diuji, dan di-debug.
- Hindari side effect tersembunyi: fungsi
hitung_totalseharusnya hanya menghitung—tidak mencetak, tidak memodifikasi global, tidak mengirim email. Side effect yang tidak diharapkan mempersulit pengujian dan debugging. - 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. - 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”.
- 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.