Fungsi adalah unit terkecil yang dapat diuji, di-reuse, dan di-reason secara independen di C. Ketika sebuah fungsi tumbuh menjadi 100 baris atau lebih, ia mulai mengorbankan ketiga properti tersebut: sulit dites karena memerlukan banyak input, sulit dipakai ulang karena logikanya terikat konteks, dan sulit dimengerti karena membawa terlalu banyak tanggung jawab. Bab ini membahas prinsip Single Responsibility Principle dalam konteks C, menunjukkan refactor sebuah fungsi panjang menjadi beberapa fungsi kecil yang masing-masing dapat diuji secara independen, dan memperkenalkan pola pure function yang menghilangkan side effect agar assertion-based testing menjadi mudah. Pada akhir bab ini kita akan mampu mengenali kapan sebuah fungsi terlalu panjang dan tahu strategi memecahnya tanpa mengubah perilaku.

Mengapa Fungsi Kecil Bernilai #

Robert C. Martin dalam Clean Code merangkum riset dekade 1990-an: fungsi idealnya berada di bawah 20 baris (beberapa tim menetapkan 30 atau 50 sebagai batas atas), memiliki satu tanggung jawab, dan menerima paling sedikit parameter. Alasannya bukan estetika—fungsi kecil memberikan tiga manfaat konkret:

  1. Dapat diuji unit. Fungsi hitung_total(const pesanan_t*) yang menerima struct dan mengembalikan integer dapat diuji dengan satu panggilan assert. Bandingkan dengan fungsi proses_pesanan(const char *input) yang melakukan parse, validasi, hitung, dan cetak—menguji satu jalur memerlukan setup input string, redirect stdout, dan membandingkan output teks.
  2. Dapat dipakai ulang. Fungsi hitung_total dapat dipanggil dari CLI, GUI, dan library tanpa modifikasi. Fungsi proses_pesanan yang langsung mem-print ke stdout tidak bisa dipakai di server yang tidak punya stdout (mis. daemon).
  3. Mudah dipahami. Pembaca yang melihat empat fungsi bernama parse_pesanan, validasi_pesanan, hitung_total, cetak_struk langsung paham alurnya tanpa membuka isi fungsi. Fungsi monolitik memaksa pembaca untuk menelusuri 40+ baris sebelum paham.

Contoh Fungsi yang Terlalu Panjang #

Berikut contoh nyata fungsi monolitik yang melanggar SRP—fungsi ini melakukan empat tugas sekaligus dalam satu tubuh:

c
/* File: fungsi1.c - Fungsi panjang yang melanggar SRP */
#include <stdio.h>

/* Versi BURUK: satu fungsi melakukan parse + validasi + hitung + cetak */
static int proses_pesanan_buruk(const char *input)
{
    /* parse input "QTY,HARGA,DISKON" */
    int qty, harga, diskon;
    if (sscanf(input, "%d,%d,%d", &qty, &harga, &diskon) != 3) {
        fprintf(stderr, "format input salah\n");
        return -1;
    }
    /* validasi */
    if (qty <= 0 || harga <= 0 || diskon < 0 || diskon > 100) {
        fprintf(stderr, "nilai tidak valid\n");
        return -2;
    }
    /* hitung subtotal */
    int subtotal = qty * harga;
    int potongan = subtotal * diskon / 100;
    int total = subtotal - potongan;
    /* cetak struk */
    printf("=== Struk ===\n");
    printf("Qty         : %d\n", qty);
    printf("Harga satuan: %d\n", harga);
    printf("Subtotal    : %d\n", subtotal);
    printf("Diskon %d%%  : %d\n", diskon, potongan);
    printf("Total       : %d\n", total);
    return total;
}

int main(void)
{
    int total = proses_pesanan_buruk("3,50000,10");
    printf("Balik ke main: %d\n", total);
    return 0;
}

Output yang Dihasilkan:

text
=== Struk ===
Qty         : 3
Harga satuan: 50000
Subtotal    : 150000
Diskon 10%  : 15000
Total       : 135000
Balik ke main: 135000

Fungsi proses_pesanan_buruk di atas berfungsi benar—outputnya benar. Tetapi fungsi tersebut sulit dipelihara karena:

  • Untuk mengubah format output, kita harus mengedit fungsi yang juga berisi parsing dan perhitungan.
  • Untuk menguji kasus diskon 0%, kita harus memformat input string dan membaca output stdout.
  • Untuk memakai logika qty * harga * (100 - diskon) / 100 di tempat lain, kita tidak bisa—kode tersebut terkubur di dalam proses_pesanan_buruk.

Refactor ke Fungsi-Fungsi Kecil #

Berikut versi yang sama setelah dipecah menjadi empat fungsi dengan tanggung jawab tunggal. Output program tetap identik—perilaku tidak berubah, hanya strukturnya.

c
/* File: fungsi2.c - Refactor jadi fungsi-fungsi kecil dengan SRP */
#include <stdio.h>

typedef struct {
    int qty;
    int harga;
    int diskon;
} pesanan_t;

/* 1. Parser: tanggung jawab hanya mengubah string ke struct */
static int parse_pesanan(const char *input, pesanan_t *out)
{
    if (sscanf(input, "%d,%d,%d", &out->qty, &out->harga, &out->diskon) != 3) {
        return -1;
    }
    return 0;
}

/* 2. Validator: tanggung jawab hanya mengecek aturan bisnis */
static int validasi_pesanan(const pesanan_t *p)
{
    if (p->qty <= 0 || p->harga <= 0) {
        return -1;
    }
    if (p->diskon < 0 || p->diskon > 100) {
        return -2;
    }
    return 0;
}

/* 3. Kalkulator: tanggung jawab hanya menghitung total */
static int hitung_total(const pesanan_t *p)
{
    int subtotal = p->qty * p->harga;
    int potongan = subtotal * p->diskon / 100;
    return subtotal - potongan;
}

/* 4. Presenter: tanggung jawab hanya mencetak output */
static void cetak_struk(const pesanan_t *p, int total)
{
    int subtotal = p->qty * p->harga;
    int potongan = subtotal * p->diskon / 100;
    printf("=== Struk ===\n");
    printf("Qty         : %d\n", p->qty);
    printf("Harga satuan: %d\n", p->harga);
    printf("Subtotal    : %d\n", subtotal);
    printf("Diskon %d%%  : %d\n", p->diskon, potongan);
    printf("Total       : %d\n", total);
}

/* Orchestrator: menggabungkan langkah-langkah */
static int proses_pesanan(const char *input)
{
    pesanan_t p;
    if (parse_pesanan(input, &p) != 0) {
        fprintf(stderr, "format input salah\n");
        return -1;
    }
    int rc = validasi_pesanan(&p);
    if (rc != 0) {
        fprintf(stderr, "nilai tidak valid (rc=%d)\n", rc);
        return rc;
    }
    int total = hitung_total(&p);
    cetak_struk(&p, total);
    return total;
}

int main(void)
{
    int total = proses_pesanan("3,50000,10");
    printf("Balik ke main: %d\n", total);
    return 0;
}

Output yang Dihasilkan:

text
=== Struk ===
Qty         : 3
Harga satuan: 50000
Subtotal    : 150000
Diskon 10%  : 15000
Total       : 135000
Balik ke main: 135000

Mari kita telaah baris-baris kode di atas:

  1. Baris 6-10 (pesanan_t): Struct data dipisah dari logika. Empat fungsi di bawah menerima const pesanan_t *—ini adalah pola data-oriented design yang dipopulerkan oleh Mike Acton di GDC 2014. Data mengalir antar fungsi, fungsi melakukan transformasi murni.
  2. Baris 13-18 (parse_pesanan): Hanya tanggung jawab parsing. Input string, output struct dan kode error. Tidak ada validasi, tidak ada perhitungan—fungsi ini hanya mengubah format.
  3. Baris 21-29 (validasi_pesanan): Hanya tanggung jawab validasi. Menerima struct yang sudah terisi, mengembalikan kode error spesifik (-1 untuk nilai negatif, -2 untuk diskon di luar 0-100). Kode error spesifik membantu debugging.
  4. Baris 32-36 (hitung_total): Pure function—tidak ada I/O, tidak ada global state, tidak ada side effect. Output hanya bergantung pada input. Fungsi seperti ini dapat diuji dengan assertion tanpa setup apapun.
  5. Baris 39-47 (cetak_struk): Fungsi I/O terisolasi di sini. Untuk mengubah format output (mis. ke JSON), kita hanya mengedit fungsi ini—tiga fungsi lain tidak tersentuh.
  6. Baris 50-65 (proses_pesanan): Orchestrator yang menggabungkan empat fungsi. Fungsi ini hanya berisi 16 baris dan membaca seperti pseudocode: parse, validasi, hitung, cetak.

Pure Function Memudahkan Testing #

Manfaat paling besar dari fungsi kecil yang terfokus adalah kemudahan testing. Kita dapat menambahkan fungsi test yang memakai assert untuk verifikasi—tanpa framework testing, tanpa mocking stdout, tanpa setup kompleks.

c
/* File: fungsi3.c - Single Responsibility untuk fungsi yang dapat diuji */
#include <stdio.h>
#include <string.h>
#include <assert.h>

/* Mendapat input terstruktur, menghasilkan output terstruktur.
   Tidak ada I/O, tidak ada global state - sangat mudah diuji. */

typedef enum {
    TINGKAT_A,
    TINGKAT_B,
    TINGKAT_C,
    TINGKAT_D,
    TINGKAT_E
} tingkat_t;

static const char *tingkat_str(tingkat_t t)
{
    static const char *nama[] = {"A", "B", "C", "D", "E"};
    return nama[t];
}

/* Pure function: tidak ada side effect, output hanya bergantung input */
static tingkat_t nilai_ke_tingkat(int nilai)
{
    if (nilai >= 90) return TINGKAT_A;
    if (nilai >= 80) return TINGKAT_B;
    if (nilai >= 70) return TINGKAT_C;
    if (nilai >= 60) return TINGKAT_D;
    return TINGKAT_E;
}

static void test_nilai_ke_tingkat(void)
{
    assert(nilai_ke_tingkat(100) == TINGKAT_A);
    assert(nilai_ke_tingkat(90)  == TINGKAT_A);
    assert(nilai_ke_tingkat(89)  == TINGKAT_B);
    assert(nilai_ke_tingkat(80)  == TINGKAT_B);
    assert(nilai_ke_tingkat(79)  == TINGKAT_C);
    assert(nilai_ke_tingkat(70)  == TINGKAT_C);
    assert(nilai_ke_tingkat(69)  == TINGKAT_D);
    assert(nilai_ke_tingkat(60)  == TINGKAT_D);
    assert(nilai_ke_tingkat(59)  == TINGKAT_E);
    assert(nilai_ke_tingkat(0)   == TINGKAT_E);
    printf("Semua assertion lulus\n");
}

int main(void)
{
    test_nilai_ke_tingkat();
    int daftar[] = {95, 85, 75, 65, 55};
    for (int i = 0; i < 5; i++) {
        printf("Nilai %d -> tingkat %s\n",
               daftar[i], tingkat_str(nilai_ke_tingkat(daftar[i])));
    }
    return 0;
}

Output yang Dihasilkan:

text
Semua assertion lulus
Nilai 95 -> tingkat A
Nilai 85 -> tingkat B
Nilai 75 -> tingkat C
Nilai 65 -> tingkat D
Nilai 55 -> tingkat E

Fungsi nilai_ke_tingkat adalah pure function: tidak ada I/O, tidak ada global state, output sepenuhnya ditentukan oleh input. Fungsi test_nilai_ke_tingkat memverifikasi sepuluh boundary condition—tepat di batas ambang (90, 80, 70, 60) dan satu unit di bawahnya (89, 79, 69, 59). Jika kelak ada yang mengubah ambang dari 80 menjadi 85, assertion akan gagal dan memberitahu developer bahwa kontrak API berubah.