Aplikasi yang membaca input, mengolah data, dan menulis output sekaligus dalam satu fungsi cenderung sulit diuji, sulit dipakai ulang, dan sulit di-debug. Separation of Concerns (SoC) adalah prinsip yang memecah kode ke lapisan-lapisan dengan tanggung jawab tunggal: parsing, business logic, dan I/O berada di modul terpisah. Bab ini menunjukkan refactor monolitik menjadi tiga lapis bersih, memperkenalkan tipe data struct sebagai contract antar lapisan, dan menjelaskan keuntungan konkret: tiap lapisan dapat diganti (mis. parse dari socket vs dari string) tanpa menyentuh lapisan lain. Setelah membaca bab ini, kita akan punya pola konkret untuk memecah aplikasi C monolitik menjadi arsitektur modular yang dapat berkembang.

Tiga Lapisan Utama #

Sebagian besar aplikasi C dapat dipetakan ke tiga lapisan tanggung jawab:

LapisanTanggung JawabContoh Modul
Input/ParserMembaca raw data, mengkonversi ke struct terstrukturparser.c, input.c
Domain LogicMemproses struct sesuai aturan bisnis, menghasilkan struct hasillogic.c, service.c
Output/PresenterMengkonversi struct hasil ke format eksternal (HTTP, JSON, log)presenter.c, output.c

Penting: setiap lapisan tidak boleh tahu detail lapisan lain. Domain logic tidak peduli apakah input berasal dari socket, file, atau string literal. Presenter tidak peduli apakah hasil dihitung dengan algoritma cepat atau lambat. Kontrak antar lapisan dinyatakan dalam bentuk struct data, bukan implisit dalam parameter fungsi.

Contoh Monolitik yang Bermasalah #

Berikut aplikasi web sederhana yang menangani request HTTP-like. Semua tanggung jawab bercampur di satu fungsi: parsing request, menghitung kuadrat/akar, dan mencetak response HTTP.

c
/* File: test_soc1.c - Aplikasi monolitik: parsing + logic + IO bercampur */
#include <stdio.h>
#include <string.h>
#include <ctype.h>

/* Versi BURUK: semua tanggung jawab bercampur di satu fungsi */
static void proses_request_buruk(const char *request)
{
    /* === PARSING (harusnya modul parser) === */
    char method[8] = {0};
    char path[64]   = {0};
    int  nilai = 0;
    if (sscanf(request, "%7s %63s %d", method, path, &nilai) < 2) {
        fprintf(stderr, "format salah\n");
        return;
    }
    /* === BUSINESS LOGIC (harusnya modul logic) === */
    int hasil = 0;
    if (strcmp(method, "GET") == 0 && strcmp(path, "/kuadrat") == 0) {
        hasil = nilai * nilai;
    } else if (strcmp(method, "GET") == 0 && strcmp(path, "/akar") == 0) {
        if (nilai < 0) {
            fprintf(stderr, "angka negatif\n");
            return;
        }
        /* akar integer aproksimasi */
        int tebakan = nilai / 2;
        for (int i = 0; i < 30 && tebakan > 0; i++) {
            tebakan = (tebakan + nilai / tebakan) / 2;
        }
        hasil = tebakan;
    } else {
        fprintf(stderr, "endpoint tidak dikenal\n");
        return;
    }
    /* === I/O (harusnya modul presenter) === */
    printf("HTTP/1.0 200 OK\r\n");
    printf("Content-Type: text/plain\r\n\r\n");
    printf("Hasil: %d\n", hasil);
}

int main(void)
{
    const char *requests[] = {
        "GET /kuadrat 7",
        "GET /akar 144",
        "GET /unknown 0",
        "GARBAGE input"
    };
    for (int i = 0; i < 4; i++) {
        printf("--- Request %d ---\n", i);
        proses_request_buruk(requests[i]);
    }
    return 0;
}

Output yang Dihasilkan:

text
--- Request 0 ---
HTTP/1.0 200 OK
Content-Type: text/plain

Hasil: 49
--- Request 1 ---
HTTP/1.0 200 OK
Content-Type: text/plain

Hasil: 12
--- Request 2 ---
--- Request 3 ---

endpoint tidak dikenal
endpoint tidak dikenal

Kode menghasilkan output untuk request 0 dan 1, tetapi output untuk request 2 dan 3 tersebar—error message dicetak ke stderr dan request 2 dan 3 tidak menghasilkan response HTTP sama sekali. Inilah konsekuensi langsung dari monolitik: parser dan presenter bercampur, jadi ketika parsing gagal, tidak ada presenter yang dipanggil. Klien HTTP yang menunggu response akan hang.

Refactor ke Tiga Lapisan #

Berikut versi yang sama setelah dipisah ke tiga fungsi dengan tanggung jawab tunggal. Output sekarang konsisten: setiap request mendapat response HTTP, baik sukses (200) maupun gagal (400).

c
/* File: test_soc2.c - Aplikasi dengan separation of concerns */
#include <stdio.h>
#include <string.h>
#include <ctype.h>

/* ===== Layer 1: Parser - tanggung jawab hanya parsing ===== */
typedef struct {
    char method[8];
    char path[64];
    int  nilai;
    int  valid;   /* 1 jika parsing sukses */
} request_t;

static int parse_request(const char *raw, request_t *out)
{
    memset(out, 0, sizeof(*out));
    int n = sscanf(raw, "%7s %63s %d", out->method, out->path, &out->nilai);
    if (n < 2) {
        out->valid = 0;
        return -1;
    }
    out->valid = 1;
    return 0;
}

/* ===== Layer 2: Business logic - tanggung jawab hanya perhitungan ===== */
typedef struct {
    int  ok;
    int  hasil;
    const char *error;
} response_t;

static int hitung_akar(int n)
{
    if (n <= 0) return 0;
    int t = n / 2 + 1;
    for (int i = 0; i < 30; i++) {
        t = (t + n / t) / 2;
    }
    return t;
}

static response_t proses_logika(const request_t *r)
{
    response_t res = {0};
    if (!r->valid) {
        res.ok = 0;
        res.error = "request tidak valid";
        return res;
    }
    if (strcmp(r->method, "GET") != 0) {
        res.ok = 0;
        res.error = "method tidak didukung";
        return res;
    }
    if (strcmp(r->path, "/kuadrat") == 0) {
        res.ok = 1;
        res.hasil = r->nilai * r->nilai;
    } else if (strcmp(r->path, "/akar") == 0) {
        if (r->nilai < 0) {
            res.ok = 0;
            res.error = "angka negatif tidak punya akar real";
            return res;
        }
        res.ok = 1;
        res.hasil = hitung_akar(r->nilai);
    } else {
        res.ok = 0;
        res.error = "endpoint tidak dikenal";
    }
    return res;
}

/* ===== Layer 3: Presenter - tanggung jawab hanya output ===== */
static void cetak_response(const response_t *r)
{
    if (r->ok) {
        printf("HTTP/1.0 200 OK\r\n");
        printf("Content-Type: text/plain\r\n\r\n");
        printf("Hasil: %d\n", r->hasil);
    } else {
        printf("HTTP/1.0 400 Bad Request\r\n");
        printf("Content-Type: text/plain\r\n\r\n");
        printf("Error: %s\n", r->error);
    }
}

int main(void)
{
    const char *requests[] = {
        "GET /kuadrat 7",
        "GET /akar 144",
        "GET /unknown 0",
        "GARBAGE input"
    };
    for (int i = 0; i < 4; i++) {
        printf("--- Request %d ---\n", i);
        request_t req;
        response_t res;

        /* Alur 3 lapis: parse -> logic -> present */
        parse_request(requests[i], &req);
        res = proses_logika(&req);
        cetak_response(&res);
    }
    return 0;
}

Output yang Dihasilkan:

text
--- Request 0 ---
HTTP/1.0 200 OK
Content-Type: text/plain

Hasil: 49
--- Request 1 ---
HTTP/1.0 200 OK
Content-Type: text/plain

Hasil: 12
--- Request 2 ---
HTTP/1.0 400 Bad Request
Content-Type: text/plain

Error: endpoint tidak dikenal
--- Request 3 ---
HTTP/1.0 400 Bad Request
Content-Type: text/plain

Error: method tidak didukung

Mari kita telaah baris-baris kode di atas:

  1. Baris 9-19 (request_t + parse_request): Lapisan parser. Input string, output struct request_t. Fungsi ini tidak tahu apa yang akan dilakukan dengan struct—ia hanya mengisi field dan menandai valid. Jika parsing gagal, valid = 0; pemanggil yang menangani.
  2. Baris 22-26 (response_t): Tipe kontrak antara logic dan presenter. Memuat ok (sukses/gagal), hasil (nilai jika sukses), dan error (pesan jika gagal). Presenter hanya melihat response_t dan tidak tahu cara hasil dihitung.
  3. Baris 36-67 (proses_logika): Lapisan logic. Menerima request_t (sudah diparse), mengembalikan response_t. Tidak ada I/O langsung—tidak ada printf atau fprintf. Fungsi ini dapat diuji dengan assertion murni tanpa mock stdout.
  4. Baris 70-83 (cetak_response): Lapisan presenter. Menerima response_t, mengkonversi ke format HTTP. Hanya lapisan ini yang tahu sintaks HTTP; jika kita ingin mengubah ke JSON, hanya file ini yang diubah.
  5. Baris 88-95 (alur main): Tiga baris alur—parse, logic, present. Pembaca yang melihat main langsung paham struktur aplikasi tanpa harus membaca isi tiap fungsi.

Data Flow Antar Lapisan #

Berikut contoh kedua yang menunjukkan bagaimana data mengalir antar lapisan dalam aplikasi monitoring sensor. Tiga lapisan (input, logic, output) terhubung melalui dua struct (sensor_t dan analisis_t) yang berperan sebagai data transfer object.

c
/* File: test_soc3.c - SoC: data flow antar layer dengan tipe eksplisit */
#include <stdio.h>

/* Layer 1: input adapter - baca dari array dummy (bisa dari socket/file) */
typedef struct { int suhu; int kelembapan; } sensor_t;

static int baca_sensor(sensor_t *out)
{
    /* Simulasi: bisa diganti dengan baca dari socket atau hardware */
    static const sensor_t data[] = {{28, 75}, {30, 80}, {27, 70}};
    static int i = 0;
    if (i >= 3) return -1;
    *out = data[i++];
    return 0;
}

/* Layer 2: domain logic - tidak tahu dari mana data berasal */
typedef struct {
    int  aman;
    int  peringatan;
    const char *status;
} analisis_t;

static analisis_t analisis_cuaca(const sensor_t *s)
{
    analisis_t a = {0};
    if (s->suhu > 35 || s->kelembapan > 85) {
        a.aman = 0;
        a.peringatan = 1;
        a.status = "Bahaya: cuaca ekstrem";
    } else if (s->suhu > 30 || s->kelembapan > 75) {
        a.aman = 1;
        a.peringatan = 1;
        a.status = "Waspada: cuaca panas/lembap";
    } else {
        a.aman = 1;
        a.peringatan = 0;
        a.status = "Normal";
    }
    return a;
}

/* Layer 3: output adapter - cetak (bisa diganti kirim ke API/log) */
static void kirim_alert(const sensor_t *s, const analisis_t *a)
{
    printf("[MONITOR] suhu=%dC kelembapan=%d%% -> %s%s\n",
           s->suhu, s->kelembapan, a->status,
           a->peringatan ? " (alert dikirim)" : "");
}

int main(void)
{
    sensor_t s;
    while (baca_sensor(&s) == 0) {
        analisis_t a = analisis_cuaca(&s);
        kirim_alert(&s, &a);
    }
    return 0;
}

Output yang Dihasilkan:

text
[MONITOR] suhu=28C kelembapan=75% -> Normal
[MONITOR] suhu=30C kelembapan=80% -> Waspada: cuaca panas/lembap (alert dikirim)
[MONITOR] suhu=27C kelembapan=70% -> Normal

Pola data flow ini memberi tiga manfaat praktis:

  • Lapisan input dapat diganti tanpa mengubah logic. baca_sensor saat ini membaca array statis; untuk produksi, kita bisa menggantinya dengan baca dari socket UDP, file CSV, atau driver sensor I2C. analisis_cuaca tetap tidak berubah karena ia hanya menerima sensor_t.
  • Lapisan output dapat diganti tanpa mengubah logic. kirim_alert saat ini memakai printf; untuk produksi, kita bisa mengganti dengan kirim ke Kafka topic, syslog, atau HTTP webhook. Lapisan logic tidak tahu perbedaannya.
  • Logic dapat diuji secara independen. Untuk menguji analisis_cuaca dengan suhu 40, kita panggil analisis_cuaca(&(sensor_t){.suhu=40, .kelembapan=50}) dan cek a.aman == 0. Tidak perlu memulai koneksi socket atau membaca file—test berjalan dalam microsecond.