Dua kata yang sering muncul saat membahas arsitektur perangkat lunak adalah coupling dan cohesion. Coupling mengukur seberapa banyak satu modul tahu tentang modul lain; cohesion mengukur seberapa erat fungsi-fungsi dalam satu modul saling berkaitan. Aturan praktisnya: low coupling, high cohesion. Bab ini menunjukkan tiga anti-pola yang menaikkan coupling (global state, dependency tersembunyi, API terlalu kaya), bagaimana pola dependency injection menurunkan coupling di C, dan bagaimana memetakan arah dependency antar modul agar grafik tetap asiklik (tidak ada circular dependency). Setelah membaca bab ini kita akan punya kriteria konkret untuk menilai apakah sebuah modul terlalu terikat ke modul lain.
Coupling dan Cohesion: Definisi #
Coupling adalah derajat ketergantungan antar modul. Modul A dikatakan tightly coupled ke modul B jika perubahan di B memaksa perubahan di A. Cohesion adalah derajat keterkaitan fungsi-fungsi di dalam satu modul. Modul dengan cohesion tinggi hanya berisi fungsi-fungsi yang bekerja sama untuk satu tujuan; modul dengan cohesion rendah adalah kumpulan fungsi acak yang kebetulan ditaruh di file yang sama.
Empat level coupling dari yang tertinggi (terburuk) ke terendah (terbaik):
- Content coupling: modul A langsung mengakses data internal modul B (mis. langsung baca
extern int g_counter). Perubahan internal B dapat merusak A tanpa compiler memperingatkan. - Common coupling: beberapa modul saling berbagi global variabel. Perubahan format global memaksa semua modul diperiksa.
- Control coupling: modul A mengirim flag ke modul B yang menentukan jalur eksekusi internal B. A harus tahu logika internal B.
- Data coupling: modul A hanya mengirim data yang dibutuhkan modul B melalui parameter. B tidak tahu apa yang dilakukan A.
Tujuan desain adalah menurunkan coupling ke level data coupling dan menaikkan cohesion ke level functional cohesion.
Anti-Pola 1: Global State #
Berikut contoh modul yang berbagi global variabel—ini adalah common coupling yang menaikkan biaya perawatan karena setiap modul diam-diam bergantung pada state global.
/* File: test_dep1.c - Coupling tinggi: konfigurasi global di-share */
#include <stdio.h>
#include <string.h>
/* Global state yang dibaca banyak modul - coupling tinggi */
static int g_debug_mode = 0;
static char g_log_path[256] = "app.log";
static int g_max_retry = 3;
/* Modul A: logger langsung membaca global */
static void log_pesan(const char *msg)
{
if (g_debug_mode) {
printf("[DEBUG] %s -> %s\n", msg, g_log_path);
}
}
/* Modul B: retrier langsung membaca global */
static int coba_lagi(int (*aksi)(void))
{
for (int i = 0; i < g_max_retry; i++) {
if (aksi() == 0) return 0;
}
return -1;
}
static int aksi_demo(void) { return 1; } /* selalu gagal */
int main(void)
{
g_debug_mode = 1;
g_max_retry = 2;
log_pesan("memulai aplikasi");
int rc = coba_lagi(aksi_demo);
log_pesan(rc == 0 ? "aksi sukses" : "aksi gagal");
return 0;
}Output yang Dihasilkan:
[DEBUG] memulai aplikasi -> app.log
[DEBUG] aksi gagal -> app.logKode di atas menghasilkan output yang benar, tetapi memiliki tiga masalah:
log_pesandancoba_lagidiam-diam membacag_debug_modedang_max_retry. Pembaca kode yang membuka filelogger.ctidak tahu bahwa ia bergantung padag_debug_modeyang didefinisikan di file lain.- Menguji
log_pesandalam isolasi mustahil—kita harus mengubahg_debug_modeglobal yang mungkin sedang dipakai juga oleh modul lain dalam thread yang sama. - Jika kelak ada kebutuhan dua konfigurasi paralel (mis. satu untuk log aplikasi, satu untuk log audit), global state hanya menyimpan satu nilai—harus dipecah menjadi parameter.
Anti-Pola 2: Cohesion Rendah #
Modul cohesion rendah adalah kumpulan fungsi tanpa tema bersama. Berikut contoh util.c yang menggabungkan matematika, string, validasi email, dan parsing path—empat topik berbeda yang seharusnya berada di empat modul berbeda.
/* File: test_dep2.c - Cohesion rendah: kumpulan fungsi tanpa tema */
#include <stdio.h>
#include <string.h>
/* Utilitas amburadul: kumpulan fungsi tanpa tema bersama */
static int util_tambah(int a, int b) { return a + b; }
static int util_strlen(const char *s) { return (int)strlen(s); }
static int util_email_valid(const char *e) { return strchr(e, '@') != NULL; }
static int util_file_ext(const char *path)
{
const char *dot = strrchr(path, '.');
return dot != NULL;
}
int main(void)
{
printf("2 + 3 = %d\n", util_tambah(2, 3));
printf("panjang 'halo' = %d\n", util_strlen("halo"));
printf("email [email protected] valid? %d\n", util_email_valid("[email protected]"));
printf("ada ext pada a.txt? %d\n", util_file_ext("a.txt"));
return 0;
}Output yang Dihasilkan:
2 + 3 = 5
panjang 'halo' = 4
email [email protected] valid? 1
ada ext pada a.txt? 1Setiap fungsi individual bekerja dengan benar. Tetapi file util.c melanggar prinsip cohesion: empat fungsi tidak berbagi state, tidak saling memanggil, dan tidak memiliki tema bersama. Pembaca yang mencari “fungsi string” harus menelusuri file yang juga berisi matematika. Solusi: pecah menjadi math_util.c, string_util.c, email_util.c, path_util.c. Setiap file memiliki satu tema dan dapat dipakai ulang tanpa menarik kode yang tidak relevan.
Solusi: Dependency Injection #
Cara standar menurunkan coupling di C adalah dengan dependency injection: konfigurasi yang dibutuhkan sebuah fungsi dikirim sebagai parameter, bukan dibaca dari global. Fungsi yang menerima dependency lewat parameter dapat diuji dengan dependency mock dan dapat dipakai ulang dalam konteks berbeda.
/* File: test_dep3.c - Dependency injection: parameter konfigurasi */
#include <stdio.h>
#include <string.h>
/* Konfigurasi sebagai struct, bukan global */
typedef struct {
int debug;
int max_retry;
char log_path[256];
} config_t;
/* Logger menerima config secara eksplisit */
static void log_pesan(const config_t *cfg, const char *msg)
{
if (cfg->debug) {
printf("[DEBUG] %s\n", msg);
}
}
/* Retrier menerima max_retry dari caller */
static int coba_lagi(const config_t *cfg, int (*aksi)(void))
{
for (int i = 0; i < cfg->max_retry; i++) {
if (aksi() == 0) return 0;
}
return -1;
}
static int aksi_sukses(void) { return 0; }
static int aksi_gagal(void) { return 1; }
int main(void)
{
config_t cfg_prod = { .debug = 0, .max_retry = 3, .log_path = "/var/log/app.log" };
config_t cfg_dev = { .debug = 1, .max_retry = 2, .log_path = "app.log" };
printf("--- Mode dev ---\n");
log_pesan(&cfg_dev, "mulai dengan aksi_sukses");
int rc1 = coba_lagi(&cfg_dev, aksi_sukses);
log_pesan(&cfg_dev, rc1 == 0 ? "sukses" : "gagal");
printf("--- Mode prod ---\n");
log_pesan(&cfg_prod, "mulai dengan aksi_gagal");
int rc2 = coba_lagi(&cfg_prod, aksi_gagal);
log_pesan(&cfg_prod, rc2 == 0 ? "sukses" : "gagal");
return 0;
}Output yang Dihasilkan:
--- Mode dev ---
[DEBUG] mulai dengan aksi_sukses
[DEBUG] sukses
--- Mode prod ---Mari kita telaah baris-baris kode di atas:
- Baris 7-11 (
config_t): Konfigurasi dibungkus dalam struct. Setiap modul yang membutuhkan konfigurasi menerimaconst config_t *sebagai parameter. Tidak ada global state yang dibaca diam-diam. - Baris 14-19 (
log_pesan): Fungsi logger sekarang tidak punya coupling ke variabel global. Untuk menguji logger dengan debug aktif, kita buatconfig_t cfg = { .debug = 1, ... }dan panggillog_pesan(&cfg, "test"). Tidak ada setup global yang harus dikembalikan ke nilai awal setelah test. - Baris 22-28 (
coba_lagi): Fungsi retrier menerimacfg->max_retrysebagai parameter. Konfigurasi retry yang berbeda dapat berjalan paralel di dua thread tanpa saling mengganggu—cfg_proddancfg_devadalah instance terpisah yang tidak berbagi memori. - Baris 33-34 (dua instance config): Di mode dev,
debug = 1mengaktifkan logging. Di mode prod,debug = 0mematikan logging—output prod tidak menampilkan[DEBUG]. Perilaku ini dicapai tanpa mengubah kodelog_pesan; hanya berbeda instanceconfig_t. - Output mode prod kosong: Tepat—
log_pesan(&cfg_prod, ...)tidak mencetak apa-apa karenacfg_prod.debug = 0. Inilah harapan: behavior dikendalikan oleh data, bukan oleh recompile.
Arah Dependency Harus Asiklik #
Selain coupling antar modul, arah dependency juga harus dikelola. Aturan dasar: grafik dependency harus asiklik—tidak boleh ada cycle A -> B -> C -> A. Cycle dependency menyebabkan beberapa masalah serius:
- Kompilasi tidak bisa dipecah: file A butuh B, B butuh C, C butuh A—semua harus dikompilasi bersamaan.
- Reuse mustahil: modul A tidak bisa dipakai tanpa membawa B dan C.
- Testing sulit: menguji A memerlukan mock B yang memerlukan mock C yang memerlukan mock A.
Cara mendeteksi cycle: tulis daftar dependency tiap modul (modul X meng-include header modul Y), gambar grafiknya. Jika ada siklus, pecahkan dengan salah satu strategi:
- Ekstrak interface: Buat modul baru yang berisi interface bersama, A dan B sama-sama bergantung ke modul baru tersebut, bukan satu sama lain.
- Inversion of control: Alih-alih A memanggil B langsung, A menerima function pointer dari caller (caller yang tahu B). A tidak perlu tahu B ada.
- Event/callback: Alih-alih A langsung memanggil B, A memancarkan event yang didengarkan B. Hubungan menjadi satu arah.