Program hingga bagian ini bisa saja ditulis sebagai satu file main.c sepanjang 1000 baris. Tapi kode seperti itu sulit dipelihara: setiap perubahan di salah satu fungsi berisiko merusak yang lain, dan test menjadi berat karena seluruh kode harus di-compile bersama. Pada bagian ini, kita pecah program menjadi modul .h/.c yang terpisah dengan tanggung jawab tunggal.

Konsep #

Pemisahan modul di C mengikuti prinsip Single Responsibility. Setiap modul memiliki:

  1. Header file (.h) — berisi deklarasi publik: tipe, konstanta, dan prototype fungsi yang dipakai modul lain.
  2. Implementasi (.c) — berisi definisi fungsi dan static helpers yang tidak keluar dari modul.
  3. Header guard — #ifndef PRODUCT_H / #define PRODUCT_H / #endif mencegah include ganda yang menyebabkan redefinition error.

Untuk pengelola inventaris, kita kelompokkan menjadi tiga modul:

ModulTanggung Jawab
productModel data Product + validasi field
inventoryKoleksi Inventory + operasi CRUD
storagePersistensi CSV (load/save)

Plus main.c sebagai entry point dan tests.c sebagai unit test runner.

Implementasi #

Struktur file setelah pemisahan:

text
proyek-07-pengelola-inventaris/
├── product.h          # Model Product
├── product.c          # Implementasi model
├── inventory.h        # Koleksi + CRUD
├── inventory.c        # Implementasi koleksi
├── storage.h          # Load/save CSV
├── storage.c          # Implementasi storage
├── main.c             # Entry point + menu interaktif
├── tests.c            # Unit test runner
├── Makefile           # Build automation
└── README.md          # Dokumentasi

Setiap header punya header guard yang unik. Contoh product.h:

c
#ifndef PRODUCT_H
#define PRODUCT_H

#include <stdint.h>

/* ... deklarasi tipe dan fungsi ... */

#endif /* PRODUCT_H */

Pattern #ifndef/#define/#endif bekerja sebagai berikut: pada include pertama, PRODUCT_H belum terdefinisi, jadi #ifndef lolos dan #define mendefinisikannya. Pada include kedua (mis. bila dua header berbeda sama-sama meng-include product.h), PRODUCT_H sudah ada, jadi seluruh blok di-skip.

Hubungan dependency antar modul:

text
main.c ──> inventory.h ──> product.h
  │           │
  │           └──> storage.h ──> inventory.h
  │
  └──> storage.h

storage.h butuh tipe Inventory (untuk load/save signature), jadi storage.h meng-include inventory.h yang pada gilirannya meng-include product.h. Tidak ada circular dependency.

Setiap file .c hanya meng-include header yang dibutuhkannya. product.c hanya meng-include product.h. inventory.c meng-include inventory.h (yang otomatis dapat product.h). storage.c meng-include storage.h (yang otomatis dapat inventory.h dan product.h). main.c meng-include inventory.h dan storage.h karena memanggil fungsi dari kedua modul itu.

Verifikasi struktur modular:

c
/* demo_modular.c - Buktikan setiap modul bisa dipakai terpisah */
#include <stdio.h>
#include "product.h"   /* Bisa dipakai tanpa inventory.h */

int main(void) {
    Product p;
    int rc = product_create(&p, 1, "Standalone Product",
                            25000.0, 4);
    printf("rc=%d\n", rc);
    product_print(&p);
    return 0;
}

Build dengan satu modul saja:

bash
gcc -std=c11 -Wall -Wextra -o /tmp/demo_modular \
    demo_modular.c product.c -lm
/tmp/demo_modular

Output yang Dihasilkan:

text
rc=0
[000001] Standalone Product                 Rp  25000.00  stok=   4

Mari kita telaah pola modular di atas:

  1. Header guard di setiap .h mencegah redefinition bila header di-include lebih dari sekali dalam satu unit kompilasi. Tanpa guard, compiler error “redefinition of struct Product”.
  2. #include "product.h" (tanda kutip): tanda kutip memberi tahu preprocessor untuk mencari header di direktori yang sama dengan file .c lebih dulu, baru kemudian ke path sistem. #include <stdio.h> (kurung siku) mencari header di path sistem (/usr/include/ dan sejenisnya) tanpa mencari di direktori lokal.
  3. Static helper di .c: Fungsi static seperti contains_ci() di inventory.c tidak terlihat dari modul lain. Ini menyembunyikan detail implementasi dan mencegah tabrakan nama dengan modul lain yang mungkin punya fungsi serupa.
  4. Separation of concerns: product.c tidak tahu tentang Inventory, inventory.c tidak tahu tentang CSV, storage.c tidak tahu tentang menu interaktif. Setiap modul bisa diganti tanpa menyentuh yang lain, selama signature fungsi publik tetap sama.
  5. Dependency direction: dependency mengalir satu arah. storage.c butuh inventory.h (untuk tipe Inventory), tetapi inventory.c tidak tahu tentang storage.h. Bila kelak kita mengganti format CSV ke JSON, hanya storage.c yang berubah.

Kode Lengkap #

Kode lengkap modul-modul terpisah ada pada direktori proyek: product.h/.c, inventory.h/.c, storage.h/.c, main.c, tests.c. Setiap file memiliki tanggung jawab tunggal dan dokumentasi singkat di komentar header.

Pengujian #

Verifikasi pemisahan modul dengan dua cara:

  1. Compile parsial — pastikan setiap modul bisa di-compile sendiri (tidak ada include yang missing). Contoh: gcc -std=c11 -Wall -Wextra -c product.c harus menghasilkan product.o tanpa error.
  2. Link multi-file — bangun executable akhir dengan menyertakan semua file .c yang relevan. Pastikan tidak ada multiple definition error.
bash
# Compile parsial (object files)
gcc -std=c11 -Wall -Wextra -c product.c -o /tmp/product.o
gcc -std=c11 -Wall -Wextra -c inventory.c -o /tmp/inventory.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

# Link bersama-sama
gcc /tmp/main.o /tmp/inventory.o /tmp/product.o /tmp/storage.o \
    -o /tmp/inventory_app -lm
/tmp/inventory_app