Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 56.328 phút đọc

Chia module và đóng gói

Sau bài này bạn sẽ làm được

  • Chia một tệp lớn thành các module có ranh giới rõ
  • Áp dụng bốn mức đóng gói của C
  • Vẽ và kiểm tra sơ đồ phụ thuộc
  • Phát hiện phụ thuộc vòng và phá vỡ nó

C không có class, không có module, không có namespace. Nhưng nó có static, có kiểu không hoàn chỉnh, và có sự tách biệt giữa header và tệp nguồn. Ba thứ đó đủ để đóng gói tốt, nếu bạn dùng chúng có kỷ luật.

#Một module là gì

Module trong C
Một cặp ten.h và ten.c có đúng một trách nhiệm. Header là hợp đồng công khai; tệp nguồn là hiện thực, và mọi thứ trong đó không nằm trong header phải là static.
Ba câu hỏi cho một module
/* 1. No chiu trach nhiem VE cai gi?
      Neu ban tra loi bang mot cau co chu "va" thi no lam qua nhieu.

      "quan ly danh sach sinh vien VA doc tep VA in bao cao"
      -> ba module: danh_sach.c, doc_tep.c, bao_cao.c

   2. No CAN nhung gi tu ben ngoai?
      Cang it cang tot. Mot module chi phu thuoc vao thu vien chuan
      la mot module ban kiem thu duoc de dang.

   3. No LO ra nhung gi?
      Dem so ky hieu trong header. Neu hon 20 thi hoac module qua
      to, hoac ban dang lo ra chi tiet ben trong. */
Dấu hiệu module xấuNghĩa là gì
Header hơn 300 dòngGiao diện quá rộng, hoặc struct nên được ẩn
Tệp .c hơn 1000 dòngNhiều trách nhiệm nằm chung
Header include hơn năm header khácPhụ thuộc quá nhiều, khó kiểm thử
Đổi một dòng làm nửa dự án dịch lạiHeader lộ chi tiết bên trong
Không kiểm thử được nếu không có mạng hoặc tệpTrộn lẫn logic với vào ra
Tên module có chữ util, common, miscKhông có trách nhiệm nào cả

#Bốn mức đóng gói

MứcAi thấy đượcCách làm
Biến static trong hàmChỉ hàm đóstatic int dem;
Ký hiệu static trong tệpChỉ tệp .c đóstatic void giup(void);
Kiểu không hoàn chỉnhTên thấy, nội dung khôngtypedef struct Ds Ds;
Ký hiệu ẩn trong thư viện dùng chungTrong thư viện, không ra ngoàivisibility("hidden")
Mức ba là mức quan trọng nhất: kiểu không hoàn chỉnh
/* ds.h, cong khai */
#ifndef DS_H
#define DS_H
#include <stddef.h>

typedef struct Ds Ds;           /* ten thay duoc, noi dung KHONG */

Ds     *ds_tao(size_t suc_chua_dau);
void    ds_huy(Ds *d);
int     ds_them(Ds *d, int x);
size_t  ds_so_luong(const Ds *d);

#endif


/* ds.c, hien thuc */
#include "ds.h"
#include <stdlib.h>

struct Ds {                     /* dinh nghia CHI o day */
    int    *pt;
    size_t  co;
    size_t  suc_chua;
};

Ds *ds_tao(size_t suc_chua_dau) { ... }
size_t ds_so_luong(const Ds *d) { return d->co; }
terminal
# Nguoi dung thu truy cap truong ben trong
gcc -std=c11 -Wall -Wextra sua.c sua_a.c sua_b.c -o sua2.exe
sua.c: In function 'main':
sua.c:10:21: error: dereferencing pointer to incomplete type 'A' {aka 'struct A'}
     printf("%d\n", a->x);
                     ^~

#Sơ đồ phụ thuộc

terminal
# gcc -MM in ra phu thuoc header cua tung tep .c
gcc -MM sua.c sua_a.c sua_b.c
sua.o: sua.c sua_a.h sua_b.h
sua_a.o: sua_a.c sua_a.h sua_b.h
sua_b.o: sua_b.c sua_b.h sua_a.h
# Do thi include tho, mot dong mot canh
for f in *.c *.h; do grep -o '#include "[^"]*"' $f | sed "s|#include \"|$f -> |; s|\"||"; done
sua.c -> sua_a.h
sua.c -> sua_b.h
sua_a.c -> sua_a.h
sua_a.c -> sua_b.h
sua_b.c -> sua_b.h
sua_b.c -> sua_a.h
vong.c -> vong_a.h
vong_a.h -> vong_b.h
vong_b.h -> vong_a.h

#Phụ thuộc vòng

vong_a.h và vong_b.h
/* vong_a.h */
#ifndef VONG_A_H
#define VONG_A_H
#include "vong_b.h"                     /* A can B */
typedef struct { B *b; int x; } A;
A   *a_tao(void);
void a_dung_b(A *a);
#endif

/* vong_b.h */
#ifndef VONG_B_H
#define VONG_B_H
#include "vong_a.h"                     /* B can A: VONG */
typedef struct { A *a; int y; } B;
B   *b_tao(void);
void b_dung_a(B *b);
#endif
terminal
gcc -std=c11 -Wall -Wextra -c vong.c -o vong.o
In file included from vong_a.h:3,
                 from vong.c:1:
vong_b.h:4:18: error: unknown type name 'A'
 typedef struct { A *a; int y; } B;
                  ^
Vòng, phải include lẫn nhau
/* vong_a.h */ #include "vong_b.h" typedef struct { B *b; int x; } A; /* vong_b.h */ #include "vong_a.h" typedef struct { A *a; int y; } B;
Khai báo trước, không include
/* sua_a.h */ typedef struct A A; /* khai bao truoc */ typedef struct B B; A *a_tao(void); void a_dung_b(A *a, B *b); /* sua_b.h */ typedef struct A A; typedef struct B B; B *b_tao(void); void b_dung_a(B *b, A *a); /* sua_a.c: dinh nghia struct A o day, include CA HAI header */ #include "sua_a.h" #include "sua_b.h" struct A { int x; };
terminal
gcc -std=c11 -Wall -Wextra sua.c sua_a.c sua_b.c -o sua.exe
./sua.exe
hai module doc lap, khong con vong
Cách ba, đảo ngược phụ thuộc bằng con trỏ hàm
/* TRUOC: mang.c goi giao_dien_ve() -> mang phu thuoc giao dien
           giao_dien.c goi mang_gui()      -> giao dien phu thuoc mang
   VONG. */

/* SAU: mang.h khong biet gi ve giao dien */
typedef void (*HamNhan)(const char *tin, void *ctx);

void mang_dat_ham_nhan(HamNhan f, void *ctx);
int  mang_gui(const char *tin);

/* mang.c */
static HamNhan g_nhan;
static void   *g_ctx;

void mang_dat_ham_nhan(HamNhan f, void *ctx) { g_nhan = f; g_ctx = ctx; }

static void khi_co_tin(const char *tin) {
    if (g_nhan) g_nhan(tin, g_ctx);     /* khong biet ai se nhan */
}

/* giao_dien.c */
#include "mang.h"
static void hien_thi(const char *tin, void *ctx) { ... }

void giao_dien_khoi_tao(void) {
    mang_dat_ham_nhan(hien_thi, NULL);  /* giao dien -> mang, MOT chieu */
}

/* Bay gio do thi la:  giao_dien.c -> mang.h
   Khong con vong, va mang.c kiem thu duoc ma khong can giao dien. */

Tự làm thử

  1. Lấy một tệp .c hơn 500 dòng của bạn và tách nó thành ít nhất hai module có ranh giới rõ.
  2. Chuyển một struct công khai của bạn thành kiểu không hoàn chỉnh và sửa mọi chỗ dùng.
  3. Kiểm tra mọi header có tự đủ không bằng -fsyntax-only.
  4. Sinh đồ thị include của dự án và vẽ nó ra.
  5. Tái hiện lỗi phụ thuộc vòng rồi chữa bằng khai báo trước.
  6. Thêm -MMD -MP vào Makefile và chứng minh rằng sửa một header làm đúng những tệp cần thiết dịch lại.

Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.

Tóm tắt

  • Một module là một cặp .h và .c với đúng một trách nhiệm; util.c không phải là một trách nhiệm.
  • Bốn mức đóng gói; quan trọng nhất là kiểu không hoàn chỉnh, đổi lại là bắt buộc cấp phát động.
  • Header phải tự đủ, include tối thiểu, và không định nghĩa biến hay hàm.
  • gcc -MM cho bạn đồ thị phụ thuộc; -MMD -MP cho Make biết khi nào dịch lại.
  • Phá vòng bằng khai báo trước, tách module chung, đảo ngược bằng con trỏ hàm, hoặc gộp lại.