Hingga bagian ini, operasi CRUD dan persistensi sudah berfungsi. Namun program belum siap dipakai bila pengguna bisa memasukkan harga negatif, nama kosong, atau id diluar rentang tanpa feedback yang jelas. Pada bagian ini, kita perdalam strategi validasi dan error handling agar program tetap stabil saat input tidak valid, dan pengguna tahu persis apa yang salah.
Konsep #
Strategi validasi yang kita pegang adalah tiga lapis:
- Lapis model —
product_is_valid()memeriksa field satu per satu. Fungsi ini dipanggil setelahproduct_createdan sebelum menyimpan ke file. - Lapis koleksi —
inventory_add/update/deletememvalidasi argumen sebelum menyentuh array. Bila validasi gagal, fungsi mengembalikan kode error dan tidak mengubah state. - Lapis UI —
main.cmenangkap kode error dari koleksi dan mencetak pesan yang dapat dimengerti pengguna.
Mekanisme kerja kode error: kita gunakan konvensi “0 = sukses, negatif = gagal”. Setiap kode negatif bisa berarti jenis error berbeda. Misalnya product_create mengembalikan:
0bila sukses-1bila pointer NULL atau nama kosong-2bila harga invalid-3bila stok negatif-4bila id invalid
Pemanggil bisa memeriksa kode spesifik untuk memberikan pesan error yang akurat.
Implementasi #
Definisi kode error di product.c:
/* Kode error yang dipakai product_create */
#define PRODUCT_ERR_NAME (-1)
#define PRODUCT_ERR_PRICE (-2)
#define PRODUCT_ERR_STOCK (-3)
#define PRODUCT_ERR_ID (-4)Validasi di product_create dengan feedback kode error:
int product_create(Product *p, int id, const char *name,
double price, int stock) {
if (p == NULL || name == NULL) {
return PRODUCT_ERR_NAME;
}
if (id <= 0 || id > PRODUCT_ID_MAX) {
return PRODUCT_ERR_ID;
}
if (price < 0.0 || isnan(price) || isinf(price)) {
return PRODUCT_ERR_PRICE;
}
if (stock < 0) {
return PRODUCT_ERR_STOCK;
}
/* ... isi field ... */
return 0;
}Di main.c, kita tangkap kode error dan cetak pesan yang berguna:
static void do_add(Inventory *inv) {
char name[PRODUCT_NAME_MAX];
char price_buf[32];
char stock_buf[16];
printf("Nama produk: ");
if (read_line(name, sizeof(name)) != 0) {
return;
}
printf("Harga (Rp): ");
read_line(price_buf, sizeof(price_buf));
printf("Stok: ");
read_line(stock_buf, sizeof(stock_buf));
double price = strtod(price_buf, NULL);
int stock = (int)strtol(stock_buf, NULL, 10);
int id = inventory_add(inv, name, price, stock);
if (id < 0) {
printf("Gagal menambah produk. Periksa kembali input Anda.\n");
return;
}
printf("Produk ditambahkan dengan id=%d\n", id);
}Pengamanan tambahan: parsing input pengguna dengan strtod/strtol tidak crash pada input non-angka (cuma mengembalikan 0). Tetapi kita harus memeriksa apakah konversi benar-benar terjadi dengan endptr:
static int parse_double(const char *s, double *out) {
if (s == NULL || out == NULL || s[0] == '\0') {
return -1;
}
char *endptr = NULL;
double v = strtod(s, &endptr);
if (endptr == s || *endptr != '\0') {
return -1; /* ada karakter yang bukan digit */
}
*out = v;
return 0;
}Verifikasi error handling dengan kasus berbagai input invalid:
/* demo_error.c - Uji coba error handling */
#include <stdio.h>
#include "inventory.h"
int main(void) {
Inventory inv;
inventory_init(&inv);
/* Kasus 1: harga negatif */
int id = inventory_add(&inv, "Produk A", -100.0, 5);
printf("harga negatif -> id=%d (harus -1)\n", id);
/* Kasus 2: nama kosong */
id = inventory_add(&inv, "", 1000.0, 5);
printf("nama kosong -> id=%d (harus -1)\n", id);
/* Kasus 3: stok negatif */
id = inventory_add(&inv, "Produk B", 1000.0, -3);
printf("stok negatif -> id=%d (harus -1)\n", id);
/* Kasus 4: data valid */
id = inventory_add(&inv, "Produk Valid", 1000.0, 3);
printf("valid -> id=%d (harus 1)\n", id);
/* Kasus 5: update id yang tidak ada */
int rc = inventory_update(&inv, 999, 2000.0, 5);
printf("update id tidak ada -> rc=%d (harus -1)\n", rc);
/* Kasus 6: delete id yang tidak ada */
rc = inventory_delete(&inv, 999);
printf("delete id tidak ada -> rc=%d (harus -1)\n", rc);
return 0;
}Output yang Dihasilkan:
harga negatif -> id=-1 (harus -1)
nama kosong -> id=-1 (harus -1)
stok negatif -> id=-1 (harus -1)
valid -> id=1 (harus 1)
update id tidak ada -> rc=-1 (harus -1)
delete id tidak ada -> rc=-1 (harus -1)Mari kita telaah baris-baris kode di atas:
- Baris 1-5 (konstanta kode error): Macro didefinisikan dengan nilai negatif yang berbeda untuk setiap jenis kesalahan. Pemanggil bisa membandingkan dengan kode spesifik untuk memberi pesan yang akurat.
- Baris 8-13 (NULL check pertama): Pengecekan pointer NULL dilakukan paling awal. Bila tidak ada,
name[0]bisa segfault. - Baris 14-16 (id check): Validasi rentang id. Nilai
id <= 0menolak id 0 dan negatif. Nilaiid > PRODUCT_ID_MAXmencegah id berlebihan yang bisa overflow di%06dformat. - Baris 17-19 (price check):
isnandanisinfdari<math.h>mendeteksi hasil0.0/0.0dan1.0/0.0yang valid di IEEE-754 tetapi tidak boleh jadi harga. - Baris 29-30 (
parse_doubleendptr check):strtodmengembalikanendptryang menunjuk ke karakter pertama yang tidak diproses. Bilaendptr == s, tidak ada karakter yang diproses (input bukan angka). Bila*endptr != '\0', ada karakter sisa (mis."12.5abc"). - Baris 47-49 (UI feedback):
printfdengan kode error spesifik membantu pengguna mengoreksi input. Hindari pesan generik “input salah” yang tidak informatif.
Kode Lengkap #
Kode lengkap dengan error handling terdapat pada product.c, inventory.c, dan main.c. Fungsi parse_double dan parse_int dengan endptr check bisa ditempatkan di file helper terpisah bila dipakai banyak modul.
Pengujian #
Pengujian error handling memerlukan edge case berlimpah. Daftar test yang harus dijalankan:
- Input
""(string kosong) untuk nama → ditolak. - Input
"abc"untuk harga → ditolak. - Input
"100.5.5"untuk harga → ditolak. - Input sangat besar
99999999999999999999→ overflow, ditolak. - Input dengan whitespace awal
" 100"→ diterima (strtol handle leading whitespace). - File CSV dengan baris rusak → dilewati tanpa crash.
gcc -std=c11 -Wall -Wextra -o /tmp/demo_error demo_error.c \
inventory.c product.c -lm
/tmp/demo_error