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

Tái cấu trúc project Phần 8

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

  • Tách một dự án một tệp thành năm module theo thứ tự an toàn
  • Kiểm tra ranh giới module bằng nm và bằng sơ đồ phụ thuộc
  • Chạy qua danh sách nghiệm thu của một dự án nhiều tệp
  • Viết README nói rõ cách build và cấu trúc dự án

Bài này lấy dự án Phần 8 đang nằm trong một tệp và tách nó thành sáu module. Điều quan trọng không phải kết quả cuối cùng, mà là sau mỗi bước chương trình vẫn biên dịch và vẫn chạy.

#Điểm xuất phát

terminal
wc -l main.c && grep -c '^[a-zA-Z].*(' main.c
874 main.c
38
gcc -std=c11 -Wall -Wextra main.c -o ql && echo 'chay duoc'
chay duoc
Phân nhóm 38 hàm
Chi cham vao MOT SinhVien (6 ham)          -> sinhvien
    sv_kiem_mssv, sv_kiem_ten, sv_kiem_diem,
    sv_kiem_nam_sinh, sv_diem_tb_x100, sv_xep_loai

Cham vao MANG sinh vien (11 ham)          -> quanly
    ql_khoi_tao, ql_giai_phong, ql_them, ql_xoa, ql_sua,
    ql_tim_mssv, ql_tim_ten, ql_sap_xep, ql_thong_ke,
    va hai ham so sanh static

Cham vao TEP nhi phan (6 ham)             -> luutru
    lt_ghi_nhi_phan, lt_doc_nhi_phan, ghi_u16, ghi_u32,
    doc_u16, doc_u32

Cham vao TEP van ban (4 ham)              -> csv
    csv_ghi, csv_doc, csv_ghi_truong, csv_tach

Cham vao MAN HINH va BAN PHIM (10 ham)    -> ui
    ui_menu, ui_hien_thi, ui_them, ui_xoa, ui_tim,
    doc_dong, doc_so, doc_diem, in_bang, in_mot_dong

Khong thuoc nhom nao (1 ham)              -> main
    main

#Sáu bước tách

  1. Bước 1: tách loi.h, không có tệp .c

    Bắt đầu từ tầng thấp nhất, thứ không phụ thuộc gì cả. Kiểu MaLoi và hàm mô tả lỗi.

    terminal
    gcc -std=c11 -Wall -Wextra main.c loi.c -o ql && ./ql du-lieu.svdb < kich-ban.txt | diff - mong-doi.txt && echo DAT
    DAT
  2. Bước 2: tách sinhvien

    Chuyển sáu hàm cùng struct SinhVien và các hằng số kích thước. Đây là module dễ nhất vì nó không gọi ai.

    /* Sau khi chuyen, kiem ngay bang mot chuong trinh nho: */
    gcc -std=c11 -Wall -Wextra sinhvien.c tests/kiem-sinhvien.c -o kiem
    ./kiem
  3. Bước 3: tách quanly

    Mười một hàm. Chú ý: hai hàm so sánh dùng cho qsort phải là static, vì không ai ngoài module này gọi chúng.

  4. Bước 4: tách luutru và csv

    Bốn hàm đóng gói số nguyên phải là static trong luutru.c. Nếu csv.c cũng cần chúng thì đó là dấu hiệu chúng thuộc về một module thứ ba, hoặc đơn giản là chép lại vì chúng chỉ vài dòng.

  5. Bước 5: tách ui

    Mười hàm. Bốn hàm nhập liệu là static. Đây là module duy nhất được phép gọi printf và fgets trên stdin.

  6. Bước 6: dọn main.c

    main.c, dưới năm mươi dòng
    #include <stdio.h>
    #include <string.h>
    
    #include "quanly.h"
    #include "luutru.h"
    #include "ui.h"
    
    int main(int argc, char **argv) {
        const char *ten_tep = (argc > 1) ? argv[1] : "du-lieu.svdb";
    
        QuanLy q;
        ql_khoi_tao(&q);
    
        MaLoi m = lt_doc_nhi_phan(ten_tep, &q.ds, &q.n);
    
        if (m == QL_OK) {
            q.suc_chua = q.n;
            printf("Da tai %zu sinh vien tu %s\n", q.n, ten_tep);
        } else if (m != QL_LOI_MO_TEP) {
            fprintf(stderr, "%s: %s\n", ten_tep, ql_mo_ta_loi(m));
            ql_giai_phong(&q);
    
            return 1;
        }
    
        ui_vong_lap(&q, ten_tep);
    
        ql_giai_phong(&q);
    
        return 0;
    }
    terminal
    wc -l src/*.c include/*.h
       38 src/main.c
      118 src/sinhvien.c
      241 src/quanly.c
      186 src/luutru.c
      132 src/csv.c
      208 src/ui.c
       14 include/loi.h
       36 include/sinhvien.h
       28 include/quanly.h
       18 include/luutru.h
       14 include/csv.h
       10 include/ui.h
     1043 total

#Kiểm tra ranh giới

terminal
# Mỗi module xuất ra bao nhiêu, header khai báo bao nhiêu
for o in build/*.o; do t=$(basename $o .o); x=$(nm -g --defined-only $o | wc -l); k=$(grep -c ');' include/$t.h 2>/dev/null || echo 0); printf '%-12s xuat %2d, khai bao %2d\n' $t $x $k; done
csv          xuat  2, khai bao  2
loi          xuat  1, khai bao  1
luutru       xuat  2, khai bao  2
main         xuat  1, khai bao  0
quanly       xuat 11, khai bao  8
sinhvien     xuat  6, khai bao  6
ui           xuat  1, khai bao  1
terminal
# Mỗi module cần gì từ bên ngoài
nm -u build/sinhvien.o
                 U isdigit
                 U isspace
                 U strlen
nm -u build/quanly.o | grep -v ' U _'
                 U free
                 U memmove
                 U qsort
                 U realloc
                 U strcmp
                 U sv_kiem_tat_ca
terminal
# Kiểm thử một module một mình
gcc -std=c11 -Iinclude tests/kiem-sinhvien.c src/sinhvien.c src/loi.c -o kiem && ./kiem
18/18 phep kiem dat
# Nếu phải liên kết cả ui.c thì ranh giới chưa sạch
gcc -std=c11 -Iinclude tests/kiem-quanly.c src/quanly.c src/sinhvien.c src/loi.c -o kiem2 && ./kiem2
24/24 phep kiem dat

#Danh sách nghiệm thu

Hạng mụcBắt buộcCách kiểm
Mọi header có include guardCófor f in include/*.h; do grep -q '^#ifndef' $f || echo $f; done
Không có tên guard trùng nhauCógrep -rh '^#ifndef' include/ | sort | uniq -d
Mỗi header dịch được một mìnhCóTạo tệp .c chỉ include nó rồi gcc -fsyntax-only
Không có định nghĩa biến trong .hCógrep -n '^[a-z].*=' include/*.h
Số ký hiệu xuất ra bằng số hàm trong .hCónm -g --defined-only, so với header
Không có phụ thuộc vòng trònCóVòng lặp tìm cặp include lẫn nhau ở Bài 20.5
main.c dưới năm mươi dòngCówc -l src/main.c
main.c không chứa logic nghiệp vụCóĐọc lại: chỉ có phân tích tham số, gọi hàm, dọn dẹp
make clean && make chạy sạchCóKhông một dòng cảnh báo
Kiểm thử được từng module riêngCógcc tests/kiem-x.c src/x.c -o kiem
Valgrind không rò rỉCóvalgrind --leak-check=full với kịch bản đầy đủ
README nói rõ cách build và cấu trúcCóNhờ người khác làm theo

Một tập lệnh kiểm tất cả

kiem-du-an.sh
#!/bin/sh
loi=0

echo "== Header guard"
for f in include/*.h; do
    grep -q '^#ifndef' "$f" || { echo "  THIEU: $f"; loi=1; }
done

echo "== Trung ten guard"
trung=$(grep -rh '^#ifndef' include/ | sort | uniq -d)
[ -n "$trung" ] && { echo "  TRUNG: $trung"; loi=1; }

echo "== Header tu du"
for f in include/*.h; do
    echo "#include \"$f\"" > /tmp/tu-du.c
    gcc -Iinclude -fsyntax-only /tmp/tu-du.c 2>/dev/null \
        || { echo "  KHONG TU DU: $f"; loi=1; }
done

echo "== Dinh nghia bien trong header"
if grep -n '^[a-zA-Z].*[^=]=[^=]' include/*.h | grep -v '#define'; then
    loi=1
fi

echo "== Bien dich sach"
make clean >/dev/null
make 2>&1 | grep -E 'warning|error' && loi=1

echo "== Ky hieu xuat thua"
for o in build/*.o; do
    t=$(basename "$o" .o)
    [ -f "include/$t.h" ] || continue

    x=$(nm -g --defined-only "$o" | wc -l)
    k=$(grep -c ');' "include/$t.h")

    [ "$x" -gt "$k" ] && { echo "  $t xuat $x, khai bao $k"; loi=1; }
done

echo "== main.c ngan"
n=$(wc -l < src/main.c)
[ "$n" -gt 50 ] && { echo "  main.c co $n dong"; loi=1; }

[ "$loi" -eq 0 ] && echo "TAT CA DAT" || echo "CO MUC CHUA DAT"

exit $loi
terminal
sh kiem-du-an.sh
== Header guard
== Trung ten guard
== Header tu du
== Dinh nghia bien trong header
== Bien dich sach
== Ky hieu xuat thua
  quanly xuat 11, khai bao 8
== main.c ngan
CO MUC CHUA DAT
# Sau khi thêm static cho ba hàm so sánh
sh kiem-du-an.sh
== Header guard
== Trung ten guard
== Header tu du
== Dinh nghia bien trong header
== Bien dich sach
== Ky hieu xuat thua
== main.c ngan
TAT CA DAT

Tự làm thử

  1. Ghi lại kết quả chạy đúng của dự án Phần 8 vào mong-doi.txt trước khi sửa gì.
  2. Tách loi.h và sinhvien, build lại, và diff với kết quả cũ.
  3. Tách nốt bốn module còn lại, mỗi lần một module, kiểm sau mỗi bước.
  4. Chạy nm -g --defined-only trên từng tệp .o và thêm static cho mọi ký hiệu không có trong header.
  5. Viết một tệp kiểm thử cho sinhvien và liên kết nó chỉ với hai tệp nguồn.
  6. Viết kiem-du-an.sh và chạy cho tới khi mọi mục đều đạt.

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

  • Tách từng module một, và sau mỗi bước phải build lại rồi so kết quả với bản ghi trước khi sửa.
  • Bắt đầu từ tầng thấp nhất, thứ không phụ thuộc gì cả, rồi đi dần lên trên.
  • Tổng số dòng tăng khoảng hai mươi phần trăm, và đó là cái giá hợp lý cho ranh giới rõ ràng.
  • nm -g --defined-only so với số hàm trong header cho biết ngay bạn quên static ở đâu.
  • Phép thử tốt nhất cho ranh giới module là: kiểm thử nó có cần kéo theo module khác không.