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:
| Lapisan | Tanggung Jawab | Contoh Modul |
|---|---|---|
| Input/Parser | Membaca raw data, mengkonversi ke struct terstruktur | parser.c, input.c |
| Domain Logic | Memproses struct sesuai aturan bisnis, menghasilkan struct hasil | logic.c, service.c |
| Output/Presenter | Mengkonversi 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.
/* 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:
--- 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 dikenalKode 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).
/* 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:
--- 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 didukungMari kita telaah baris-baris kode di atas:
- Baris 9-19 (
request_t+parse_request): Lapisan parser. Input string, output structrequest_t. Fungsi ini tidak tahu apa yang akan dilakukan dengan struct—ia hanya mengisi field dan menandaivalid. Jika parsing gagal,valid = 0; pemanggil yang menangani. - Baris 22-26 (
response_t): Tipe kontrak antara logic dan presenter. Memuatok(sukses/gagal),hasil(nilai jika sukses), danerror(pesan jika gagal). Presenter hanya melihatresponse_tdan tidak tahu cara hasil dihitung. - Baris 36-67 (
proses_logika): Lapisan logic. Menerimarequest_t(sudah diparse), mengembalikanresponse_t. Tidak ada I/O langsung—tidak adaprintfataufprintf. Fungsi ini dapat diuji dengan assertion murni tanpa mock stdout. - Baris 70-83 (
cetak_response): Lapisan presenter. Menerimaresponse_t, mengkonversi ke format HTTP. Hanya lapisan ini yang tahu sintaks HTTP; jika kita ingin mengubah ke JSON, hanya file ini yang diubah. - 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.
/* 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:
[MONITOR] suhu=28C kelembapan=75% -> Normal
[MONITOR] suhu=30C kelembapan=80% -> Waspada: cuaca panas/lembap (alert dikirim)
[MONITOR] suhu=27C kelembapan=70% -> NormalPola data flow ini memberi tiga manfaat praktis:
- Lapisan input dapat diganti tanpa mengubah logic.
baca_sensorsaat ini membaca array statis; untuk produksi, kita bisa menggantinya dengan baca dari socket UDP, file CSV, atau driver sensor I2C.analisis_cuacatetap tidak berubah karena ia hanya menerimasensor_t. - Lapisan output dapat diganti tanpa mengubah logic.
kirim_alertsaat ini memakaiprintf; 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_cuacadengan suhu 40, kita panggilanalisis_cuaca(&(sensor_t){.suhu=40, .kelembapan=50})dan ceka.aman == 0. Tidak perlu memulai koneksi socket atau membaca file—test berjalan dalam microsecond.