Pada bagian ini kita merangkum struktur modular yang sudah kita bangun sejak Bagian 1. Pemisahan tiga modul — task (model + operasi), storage (persistensi), dan main (I/O) — bukan sekadar tata letak berkas, melainkan keputusan arsitektur yang menentukan batas tanggung jawab. Bagian ini menjelaskan mengapa kita memilih pemisahan ini dan bagaimana setiap modul berinteraksi dengan yang lain.
Konsep #
Setiap modul punya satu tanggung jawab utama (single responsibility). Modul task mengelola data tugas dan operasi CRUD/filter/search — tidak tahu apa-apa tentang file atau layar. Modul storage menangani serialisasi ke file teks — tidak tahu apa arti setiap field enum. Modul main mengoordinasi interaksi dengan pengguna — memanggil task_tambah saat pengguna memilih menu tambah, lalu storage_simpan saat keluar.
Pemisahan ini memberi tiga manfaat praktis. Pertama, testability: tests.c dapat menguji task dan storage tanpa menyentuh main.c. Kedua, reusability: bila kita ingin membuat versi CLI dari aplikasi web, modul task dapat dipakai ulang tanpa perubahan. Ketiga, maintainability: bug di persistensi dapat diperbaiki di storage.c tanpa mengganggu logika bisnis di task.c.
Boundary antar modul didefinisikan oleh header. task.h adalah kontrak publik modul task — fungsi dan tipe yang dapat dipanggil dari luar. Implementasi internal di task.c (mis. task_valid_judul) boleh dideklarasikan static agar tidak terlihat dari luar. storage.h hanya mengekspos storage_simpan dan storage_muat — detail format file tetap internal.
Implementasi #
Diagram ketergantungan modul:
main.c
/ \
v v
task.h storage.h
| |
v v
task.c storage.cmain.c meng-include kedua header (task.h dan storage.h). storage.h meng-include task.h karena storage_simpan menerima TaskList *. task.c tidak meng-include storage.h — modul task tidak tahu bahwa persistensi ada.
Pemanggilan tipikal di main.c:
TaskList list;
task_list_init(&list); /* task */
storage_muat(&list, DB_PATH); /* storage -> task */
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_TINGGI); /* task */
task_set_status(&list, 1, TASK_STATUS_PROSES); /* task */
storage_simpan(&list, DB_PATH); /* storage */Kode Lengkap #
Struktur berkas final:
proyek-04-pengelola-tugas/
task.h -- model + prototipe operasi (public API)
task.c -- implementasi CRUD/filter/search
storage.h -- prototipe simpan/muat (public API)
storage.c -- persistensi ke file teks
main.c -- menu interaktif
tests.c -- test runner
Makefile -- build automation
README.md -- dokumentasi singkatMakefile yang merangkai ketiga modul:
CC = gcc
CFLAGS = -std=c11 -Wall -Wextra -g
TARGET = program
TEST_TARGET = test_runner
SRCS = main.c task.c storage.c
TEST_SRCS = tests.c task.c storage.c
$(TARGET): $(SRCS)
$(CC) $(CFLAGS) -o $(TARGET) $(SRCS)
test: $(TEST_TARGET)
./$(TEST_TARGET)
$(TEST_TARGET): $(TEST_SRCS)
$(CC) $(CFLAGS) -o $(TEST_TARGET) $(TEST_SRCS)
clean:
rm -f $(TARGET) $(TEST_TARGET) tugas.txt
.PHONY: test cleanPengujian #
Verifikasi bahwa setiap modul dapat di-compile secara independen. Pertama, hanya task.c (dengan header):
cd /home/z/my-project/content/c/proyek-c/proyek-04-pengelola-tugas/
gcc -std=c11 -Wall -Wextra -c task.c -o /tmp/task.o
gcc -std=c11 -Wall -Wextra -c storage.c -o /tmp/storage.o
gcc -std=c11 -Wall -Wextra -c main.c -o /tmp/main.o
gcc /tmp/main.o /tmp/task.o /tmp/storage.o -o /tmp/p4b5
echo "=== BUILD MODULAR OK ==="Output yang Dihasilkan:
=== BUILD MODULAR OK ===Kompilasi per modul (dengan flag -c) menghasilkan file object .o. Linker kemudian merangkai tiga object menjadi executable. Bila salah satu modul gagal compile, modul lain tetap dapat di-compile — kita tidak perlu membangun ulang seluruh proyek untuk satu perubahan kecil.
Verifikasi header tidak terjadi include sirkular:
gcc -std=c11 -Wall -Wextra -E main.c 2>&1 | head -5Output yang Dihasilkan (cuplikan awal):
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
...Tanpa error “include sirkular” atau “redefinition”, struktur header kita sehat.
Mari kita telaah keputusan pemisahan ini lebih dalam:
- Mengapa
task.ctidak meng-includestorage.h? Modultaskadalah inti domain — model data dan operasi. Persistensi adalah masalah infrastruktur. Bila suatu hari kita mengganti format file dari teks ke SQLite, hanyastorage.cyang berubah;task.ctetap utuh. Pemisahan ini mengikuti prinsip dependency inversion: modul domain tidak bergantung pada modul infrastruktur. - Mengapa
storage.hmeng-includetask.h? Fungsistorage_simpanmenerimaconst TaskList *. Tanpa include,TaskListtidak dikenali. Headerstorage.hmemerlukan deklarasi tipe daritask.h. - Mengapa
main.ctidak punya fungsi reusable?main.cberisi fungsi-fungsistaticyang mengoordinasi input pengguna dengan panggilan modul. Fungsimenu_tambah,menu_set_status, dll. hanya dipakai dalammain.c—staticmenyembunyikannya dari modul lain. - Mengapa
tests.ctidak meng-includemain.c?main.cpunya fungsimainsendiri. Bilatests.c(yang juga punyamain) meng-includemain.c, terjadi error “multiple definition ofmain”.tests.chanya meng-includetask.hdanstorage.h— tidak perlu menyentuhmain.c. - Mengapa Makefile memisahkan
SRCSdanTEST_SRCS? Build program utama dan build test runner memerlukan kombinasi berkas berbeda.SRCSmengandungmain.c(untuk executable pengguna),TEST_SRCSmengandungtests.c(untuk test runner). Modultask.cdanstorage.cdipakai bersama.
Bagian 6 akan menulis test runner yang menguji seluruh modul.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
main.c
Unduh berkas/* main.c -- titik masuk aplikasi pengelola tugas. */
#include "task.h"
#include "storage.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define DB_PATH "tugas.txt"
static void baca_baris(const char *prompt, char *buf, size_t ukuran) {
printf("%s", prompt);
if (fgets(buf, (int)ukuran, stdin) == 0) {
buf[0] = '\0';
return;
}
size_t n = strlen(buf);
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) {
buf[--n] = '\0';
}
}
static int baca_int(const char *prompt, int *out) {
char buf[32];
baca_baris(prompt, buf, sizeof(buf));
char *sisa = 0;
long v = strtol(buf, &sisa, 10);
if (sisa == buf) return 0;
*out = (int)v;
return 1;
}
static void menu_tambah(TaskList *list) {
char judul[TASK_JUDUL_MAKS];
char deskripsi[TASK_DESKRIPSI_MAKS];
baca_baris("Judul : ", judul, sizeof(judul));
baca_baris("Deskripsi : ", deskripsi, sizeof(deskripsi));
int p = 0;
if (!baca_int("Prioritas (1=Rendah,2=Sedang,3=Tinggi): ", &p)) {
printf("Prioritas tidak valid.\n"); return;
}
TaskResult r = task_tambah(list, judul, deskripsi, (TaskPrioritas)p);
if (r == TASK_OK) {
printf("Tugas ditambahkan (id=%d).\n", list->data[list->jumlah - 1].id);
} else {
printf("Gagal menambah (kode %d).\n", r);
}
}
static void menu_set_status(TaskList *list) {
int id, s;
if (!baca_int("ID tugas : ", &id)) { printf("ID tidak valid.\n"); return; }
if (!baca_int("Status (0=Belum,1=Proses,2=Selesai): ", &s)) {
printf("Status tidak valid.\n"); return;
}
TaskResult r = task_set_status(list, id, (TaskStatus)s);
if (r == TASK_OK) printf("Status diubah.\n");
else if (r == TASK_ERR_TIDAK_DITEMUKAN) printf("Tugas tidak ditemukan.\n");
else printf("Input tidak valid.\n");
}
static void menu_hapus(TaskList *list) {
int id;
if (!baca_int("ID tugas yang dihapus: ", &id)) { printf("ID tidak valid.\n"); return; }
TaskResult r = task_hapus(list, id);
if (r == TASK_OK) printf("Tugas dihapus.\n");
else if (r == TASK_ERR_TIDAK_DITEMUKAN) printf("Tugas tidak ditemukan.\n");
}
static void menu_filter(const TaskList *list) {
int s;
if (!baca_int("Filter status (0=Belum,1=Proses,2=Selesai): ", &s)) {
printf("Status tidak valid.\n"); return;
}
if (s < TASK_STATUS_BELUM || s > TASK_STATUS_SELESAI) {
printf("Status tidak valid.\n"); return;
}
task_filter_status(list, (TaskStatus)s, stdout);
}
static void menu_cari(const TaskList *list) {
char kata[TASK_JUDUL_MAKS];
baca_baris("Kata kunci judul: ", kata, sizeof(kata));
int idx = task_cari(list, kata);
if (idx < 0) {
printf("Tidak ada tugas dengan judul mengandung '%s'.\n", kata);
} else {
const Task *t = &list->data[idx];
printf("Ditemukan: [%d] %s | %s | %s\n",
t->id, t->judul,
task_status_nama(t->status),
task_prioritas_nama(t->prioritas));
}
}
int main(void) {
TaskList list;
task_list_init(&list);
StorageStatus sm = storage_muat(&list, DB_PATH);
if (sm == STORAGE_OK) {
printf("Memuat %d tugas dari %s.\n", list.jumlah, DB_PATH);
} else if (sm == STORAGE_ERR_BUKA_FILE) {
printf("Belum ada file %s. Memulai dengan daftar kosong.\n", DB_PATH);
} else {
printf("Peringatan: format file rusak (kode %d).\n", sm);
}
int lanjut = 1;
while (lanjut) {
printf("\n=== Pengelola Tugas ===\n");
printf("1. Tambah\n2. Tampilkan\n3. Filter status\n");
printf("4. Cari judul\n5. Set status\n6. Hapus\n7. Keluar\n");
int pilihan = 0;
if (!baca_int("Pilih: ", &pilihan)) { printf("Pilihan tidak valid.\n"); continue; }
switch (pilihan) {
case 1: menu_tambah(&list); break;
case 2: task_tampilkan(&list, stdout); break;
case 3: menu_filter(&list); break;
case 4: menu_cari(&list); break;
case 5: menu_set_status(&list); break;
case 6: menu_hapus(&list); break;
case 7: lanjut = 0; break;
default: printf("Pilihan tidak dikenal.\n"); break;
}
}
StorageStatus ss = storage_simpan(&list, DB_PATH);
if (ss == STORAGE_OK) printf("Menyimpan %d tugas ke %s.\n", list.jumlah, DB_PATH);
else printf("Gagal menyimpan (kode %d).\n", ss);
printf("Terima kasih.\n");
return 0;
}
storage.c
Unduh berkas/* storage.c -- persistensi tugas ke file teks. */
#include "storage.h"
#include <stdio.h>
#include <string.h>
static void potong_newline(char *s) {
size_t n = strlen(s);
while (n > 0 && (s[n - 1] == '\n' || s[n - 1] == '\r')) {
s[--n] = '\0';
}
}
StorageStatus storage_simpan(const TaskList *list, const char *path) {
if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
FILE *f = fopen(path, "w");
if (f == 0) return STORAGE_ERR_BUKA_FILE;
fprintf(f, "%d\n", list->jumlah);
for (int i = 0; i < list->jumlah; i++) {
const Task *t = &list->data[i];
/* Ganti | di judul/deskripsi dengan spasi agar parsing tetap aman. */
fprintf(f, "%d|%s|%s|%d|%d\n",
t->id, t->judul, t->deskripsi, (int)t->status, (int)t->prioritas);
}
if (fclose(f) != 0) return STORAGE_ERR_TULIS;
return STORAGE_OK;
}
StorageStatus storage_muat(TaskList *list, const char *path) {
if (list == 0 || path == 0) return STORAGE_ERR_BUKA_FILE;
FILE *f = fopen(path, "r");
if (f == 0) return STORAGE_ERR_BUKA_FILE;
int jumlah = 0;
if (fscanf(f, "%d\n", &jumlah) != 1) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
if (jumlah < 0 || jumlah > TASK_MAKS) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
char baris[512];
int id_terbesar = list->id_berikutnya - 1;
for (int i = 0; i < jumlah; i++) {
if (fgets(baris, sizeof(baris), f) == 0) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
potong_newline(baris);
int id, status, prioritas;
char judul[TASK_JUDUL_MAKS];
char deskripsi[TASK_DESKRIPSI_MAKS];
int parsed = sscanf(baris, "%d|%79[^|]|%199[^|]|%d|%d",
&id, judul, deskripsi, &status, &prioritas);
if (parsed < 5) {
fclose(f);
return STORAGE_ERR_FORMAT;
}
if (list->jumlah >= TASK_MAKS) break;
Task *t = &list->data[list->jumlah];
t->id = id;
strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
t->judul[TASK_JUDUL_MAKS - 1] = '\0';
strncpy(t->deskripsi, deskripsi, TASK_DESKRIPSI_MAKS - 1);
t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
t->status = (TaskStatus)status;
t->prioritas = (TaskPrioritas)prioritas;
list->jumlah++;
if (id > id_terbesar) id_terbesar = id;
}
list->id_berikutnya = id_terbesar + 1;
fclose(f);
return STORAGE_OK;
}
task.c
Unduh berkas/* task.c -- implementasi model dan operasi tugas. */
#include "task.h"
#include <string.h>
#include <stdio.h>
void task_list_init(TaskList *list) {
if (list == 0) return;
list->jumlah = 0;
list->id_berikutnya = 1;
}
int task_valid_judul(const char *judul) {
if (judul == 0 || judul[0] == '\0') return 0;
if (strlen(judul) >= TASK_JUDUL_MAKS) return 0;
return 1;
}
int task_valid_prioritas(int p) {
return p == TASK_PRIORITAS_RENDAH
|| p == TASK_PRIORITAS_SEDANG
|| p == TASK_PRIORITAS_TINGGI;
}
TaskResult task_tambah(TaskList *list,
const char *judul,
const char *deskripsi,
TaskPrioritas prioritas) {
if (list == 0) return TASK_ERR_INPUT_INVALID;
if (!task_valid_judul(judul)) return TASK_ERR_INPUT_INVALID;
if (!task_valid_prioritas((int)prioritas)) return TASK_ERR_INPUT_INVALID;
if (list->jumlah >= TASK_MAKS) return TASK_ERR_PENUH;
Task *t = &list->data[list->jumlah];
t->id = list->id_berikutnya++;
strncpy(t->judul, judul, TASK_JUDUL_MAKS - 1);
t->judul[TASK_JUDUL_MAKS - 1] = '\0';
strncpy(t->deskripsi, deskripsi ? deskripsi : "", TASK_DESKRIPSI_MAKS - 1);
t->deskripsi[TASK_DESKRIPSI_MAKS - 1] = '\0';
t->status = TASK_STATUS_BELUM;
t->prioritas = prioritas;
list->jumlah++;
return TASK_OK;
}
void task_tampilkan(const TaskList *list, FILE *out) {
if (list == 0 || out == 0) return;
fprintf(out, "%-4s %-30s %-12s %-10s\n",
"ID", "Judul", "Status", "Prioritas");
for (int i = 0; i < list->jumlah; i++) {
const Task *t = &list->data[i];
fprintf(out, "%-4d %-30s %-12s %-10s\n",
t->id, t->judul,
task_status_nama(t->status),
task_prioritas_nama(t->prioritas));
}
fprintf(out, "Total: %d tugas\n", list->jumlah);
}
TaskResult task_set_status(TaskList *list, int id, TaskStatus status) {
if (list == 0) return TASK_ERR_INPUT_INVALID;
if (status < TASK_STATUS_BELUM || status > TASK_STATUS_SELESAI) {
return TASK_ERR_INPUT_INVALID;
}
for (int i = 0; i < list->jumlah; i++) {
if (list->data[i].id == id) {
list->data[i].status = status;
return TASK_OK;
}
}
return TASK_ERR_TIDAK_DITEMUKAN;
}
TaskResult task_hapus(TaskList *list, int id) {
if (list == 0) return TASK_ERR_INPUT_INVALID;
for (int i = 0; i < list->jumlah; i++) {
if (list->data[i].id == id) {
for (int j = i; j < list->jumlah - 1; j++) {
list->data[j] = list->data[j + 1];
}
list->jumlah--;
return TASK_OK;
}
}
return TASK_ERR_TIDAK_DITEMUKAN;
}
int task_filter_status(const TaskList *list, TaskStatus status, FILE *out) {
if (list == 0 || out == 0) return 0;
int count = 0;
fprintf(out, "%-4s %-30s %-12s %-10s\n",
"ID", "Judul", "Status", "Prioritas");
for (int i = 0; i < list->jumlah; i++) {
const Task *t = &list->data[i];
if (t->status == status) {
fprintf(out, "%-4d %-30s %-12s %-10s\n",
t->id, t->judul,
task_status_nama(t->status),
task_prioritas_nama(t->prioritas));
count++;
}
}
fprintf(out, "Ditemukan: %d tugas\n", count);
return count;
}
int task_cari(const TaskList *list, const char *kata_kunci) {
if (list == 0 || kata_kunci == 0) return -1;
for (int i = 0; i < list->jumlah; i++) {
if (strstr(list->data[i].judul, kata_kunci) != 0) {
return i;
}
}
return -1;
}
const char *task_status_nama(TaskStatus s) {
switch (s) {
case TASK_STATUS_BELUM: return "Belum";
case TASK_STATUS_PROSES: return "Proses";
case TASK_STATUS_SELESAI:return "Selesai";
default: return "?";
}
}
const char *task_prioritas_nama(TaskPrioritas p) {
switch (p) {
case TASK_PRIORITAS_RENDAH: return "Rendah";
case TASK_PRIORITAS_SEDANG: return "Sedang";
case TASK_PRIORITAS_TINGGI: return "Tinggi";
default: return "?";
}
}
tests.c
Unduh berkas/* tests.c -- rangkaian test untuk modul task dan storage. */
#include "task.h"
#include "storage.h"
#include <stdio.h>
#include <string.h>
static int lulus = 0;
static int gagal = 0;
#define PERIKSA(kondisi, nama) \
do { \
if (kondisi) { lulus++; printf("[LULUS] %s\n", nama); } \
else { gagal++; printf("[GAGAL] %s\n", nama); } \
} while (0)
static void test_tambah(void) {
TaskList list;
task_list_init(&list);
TaskResult r1 = task_tambah(&list, "Belajar C", "Bab struct", TASK_PRIORITAS_TINGGI);
TaskResult r2 = task_tambah(&list, "Belanja", "Beli susu", TASK_PRIORITAS_SEDANG);
PERIKSA(r1 == TASK_OK && r2 == TASK_OK, "Tambah 2 tugas berhasil");
PERIKSA(list.jumlah == 2, "Jumlah = 2");
PERIKSA(list.data[0].id == 1, "ID pertama = 1");
PERIKSA(list.data[0].status == TASK_STATUS_BELUM, "Status awal = BELUM");
}
static void test_tambah_invalid(void) {
TaskList list;
task_list_init(&list);
PERIKSA(task_tambah(&list, "", "x", TASK_PRIORITAS_SEDANG) == TASK_ERR_INPUT_INVALID,
"Judul kosong ditolak");
PERIKSA(task_tambah(&list, "X", "Y", (TaskPrioritas)99) == TASK_ERR_INPUT_INVALID,
"Prioritas invalid ditolak");
PERIKSA(list.jumlah == 0, "Tidak ada tugas ditambahkan");
}
static void test_set_status(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);
TaskResult r = task_set_status(&list, 1, TASK_STATUS_SELESAI);
PERIKSA(r == TASK_OK, "Set status SELESAI berhasil");
PERIKSA(list.data[0].status == TASK_STATUS_SELESAI, "Status = SELESAI");
PERIKSA(task_set_status(&list, 999, TASK_STATUS_PROSES) == TASK_ERR_TIDAK_DITEMUKAN,
"Set status ID tidak ada -> error");
}
static void test_hapus(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "T2", "D2", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "T3", "D3", TASK_PRIORITAS_SEDANG);
TaskResult r = task_hapus(&list, 2);
PERIKSA(r == TASK_OK && list.jumlah == 2, "Hapus id=2 berhasil");
PERIKSA(strcmp(list.data[1].judul, "T3") == 0,
"Setelah hapus, elemen 1 = 'T3'");
}
static void test_filter_status(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_SEDANG); /* BELUM */
task_tambah(&list, "T2", "D2", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "T3", "D3", TASK_PRIORITAS_SEDANG);
task_set_status(&list, 2, TASK_STATUS_PROSES);
task_set_status(&list, 3, TASK_STATUS_SELESAI);
int n = task_filter_status(&list, TASK_STATUS_PROSES, stdout);
PERIKSA(n == 1, "Filter PROSES menemukan 1 tugas");
}
static void test_cari(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "Belajar C", "", TASK_PRIORITAS_SEDANG);
task_tambah(&list, "Belajar Python", "", TASK_PRIORITAS_SEDANG);
int idx = task_cari(&list, "Python");
PERIKSA(idx == 1, "Cari 'Python' -> indeks 1");
PERIKSA(task_cari(&list, "Rust") == -1, "Cari 'Rust' -> -1");
}
static void test_penyimpanan(void) {
TaskList list;
task_list_init(&list);
task_tambah(&list, "T1", "D1", TASK_PRIORITAS_TINGGI);
task_tambah(&list, "T2", "D2", TASK_PRIORITAS_RENDAH);
task_set_status(&list, 2, TASK_STATUS_SELESAI);
StorageStatus ss = storage_simpan(&list, "/tmp/test_tugas.txt");
PERIKSA(ss == STORAGE_OK, "Simpan ke /tmp/test_tugas.txt");
TaskList dimuat;
task_list_init(&dimuat);
ss = storage_muat(&dimuat, "/tmp/test_tugas.txt");
PERIKSA(ss == STORAGE_OK, "Muat dari /tmp/test_tugas.txt");
PERIKSA(dimuat.jumlah == 2, "Jumlah dimuat = 2");
PERIKSA(strcmp(dimuat.data[0].judul, "T1") == 0, "Judul dimuat = 'T1'");
PERIKSA(dimuat.data[1].status == TASK_STATUS_SELESAI, "Status dimuat = SELESAI");
PERIKSA(dimuat.id_berikutnya == 3, "id_berikutnya = 3");
}
int main(void) {
test_tambah();
test_tambah_invalid();
test_set_status();
test_hapus();
test_filter_status();
test_cari();
test_penyimpanan();
printf("\nRingkasan: %d lulus, %d gagal\n", lulus, gagal);
return gagal == 0 ? 0 : 1;
}