Project quản lý thư viện C++: chương trình hoàn chỉnh nhiều file

Project quản lý thư viện C++: chương trình hoàn chỉnh nhiều file - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ ráp toàn bộ ví dụ xuyên suốt serie thành một project quản lý thư viện C++ hoàn chỉnh: chương trình chạy trên dòng lệnh, nạp kho tài liệu từ file, cho mượn, trả, tìm kiếm, đặt chỗ chờ, ghi nhật ký và lưu lại khi thoát. Đây là project tổng hợp, mỗi phần của nó tương ứng với một bài các bạn đã học, nên rất hợp làm bài tập lớn cho môn C++.

Bài 36 trong Serie Lập trình C++ từ A tới Z

Yêu cầu của project quản lý thư viện C++

  1. Khi khởi động, nạp kho tài liệu (sách và tạp chí) từ file kho.txt. Dòng sai định dạng thì bỏ qua và báo số dòng bị bỏ.
  2. Menu gồm: xem danh sách, tìm theo tên, mượn, trả, đặt chỗ chờ, lưu và thoát.
  3. Mỗi loại tài liệu có thời hạn mượn khác nhau: sách 14 ngày, tạp chí 3 ngày.
  4. Tài liệu đang có người mượn thì không cho mượn tiếp, nhưng cho đặt chỗ chờ. Khi được trả, báo cho người đang chờ.
  5. Mọi lượt mượn, trả được ghi vào file nhật ký.
  6. Nhập sai (chữ thay vì số) thì nhắc nhập lại, chương trình không được crash.

Thiết kế: chia file và chọn kỹ thuật

Sơ đồ cấu trúc project quản lý thư viện C++ gồm main, thu_vien, tai_lieu, logger và các pattern dùng

Chương trình chia thành 5 file, mỗi file một trách nhiệm, đúng nguyên tắc S trong SOLID:

File Nội dung Kiến thức dùng
tai_lieu.h, tai_lieu.cpp Lớp trừu tượng TaiLieu, lớp con Sach, TapChi, hàm factory Kế thừa, đa hình, trừu tượng, Factory
logger.h Ghi nhật ký Singleton, đọc ghi file
thu_vien.h, thu_vien.cpp Class ThuVien quản lý kho và nghiệp vụ STL map, smart pointer, Observer, ngoại lệ
main.cpp Menu và nhập liệu Vòng lặp, rẽ nhánh, string

Định dạng file kho.txt, mỗi dòng một tài liệu, các trường ngăn cách bằng |:

sach|1001|Lap trinh C++|Bjarne Stroustrup
sach|1002|Clean Code|Robert Martin
tapchi|2001|Tap chi Dien tu|125
bando|3001|Ban do Ha Noi|2024

Dòng bando cố ý để sai, để kiểm tra chương trình bỏ qua dòng lỗi mà không dừng.

Tài liệu: lớp trừu tượng và factory

File tai_lieu.h:

#ifndef TAI_LIEU_H
#define TAI_LIEU_H

#include <memory>
#include <string>

class TaiLieu
{
public:
    TaiLieu(int ma, const std::string& ten) : ma_(ma), ten_(ten) {}
    virtual ~TaiLieu() = default;

    int ma() const { return ma_; }
    const std::string& ten() const { return ten_; }
    bool dangMuon() const { return dangMuon_; }
    void datDangMuon(bool giaTri) { dangMuon_ = giaTri; }

    virtual int soNgayMuonToiDa() const = 0;
    virtual std::string moTa() const = 0;          // dong hien thi tren man hinh
    virtual std::string dongLuuFile() const = 0;   // dong ghi ra file

private:
    int ma_;
    std::string ten_;
    bool dangMuon_ = false;
};

class Sach : public TaiLieu
{
public:
    Sach(int ma, const std::string& ten, const std::string& tacGia) : TaiLieu(ma, ten), tacGia_(tacGia) {}
    int soNgayMuonToiDa() const override { return 14; }
    std::string moTa() const override { return "[Sach] " + ten() + " - " + tacGia_; }
    std::string dongLuuFile() const override { return "sach|" + std::to_string(ma()) + "|" + ten() + "|" + tacGia_; }

private:
    std::string tacGia_;
};

class TapChi : public TaiLieu
{
public:
    TapChi(int ma, const std::string& ten, int soKy) : TaiLieu(ma, ten), soKy_(soKy) {}
    int soNgayMuonToiDa() const override { return 3; }
    std::string moTa() const override { return "[Tap chi] " + ten() + " so " + std::to_string(soKy_); }
    std::string dongLuuFile() const override { return "tapchi|" + std::to_string(ma()) + "|" + ten() + "|" + std::to_string(soKy_); }

private:
    int soKy_;
};

// Factory: tao tai lieu tu mot dong trong file, dong sai thi nem std::invalid_argument
std::unique_ptr<TaiLieu> taoTaiLieu(const std::string& dong);

#endif
  • TaiLieu là lớp trừu tượng với 3 hàm thuần ảo. Mỗi loại tài liệu tự quyết định thời hạn mượn, cách hiển thị và cách ghi ra file.
  • Nhờ hàm dongLuuFile() ảo, phần lưu file của ThuVien không cần biết có những loại tài liệu nào.

File tai_lieu.cpp chứa hàm factory, nơi duy nhất trong chương trình biết đến Sach và TapChi:

#include "tai_lieu.h"
#include <sstream>
#include <stdexcept>

std::unique_ptr<TaiLieu> taoTaiLieu(const std::string& dong)
{
    std::stringstream ss(dong);
    std::string loai, ma, ten, them;
    std::getline(ss, loai, '|');
    std::getline(ss, ma, '|');
    std::getline(ss, ten, '|');
    std::getline(ss, them, '|');

    if (loai == "sach")
        return std::make_unique<Sach>(std::stoi(ma), ten, them);
    if (loai == "tapchi")
        return std::make_unique<TapChi>(std::stoi(ma), ten, std::stoi(them));
    throw std::invalid_argument("loai tai lieu khong hop le: '" + loai + "'");
}

Logger: Singleton ghi nhật ký

File logger.h:

#ifndef LOGGER_H
#define LOGGER_H

#include <fstream>
#include <string>

class Logger
{
public:
    static Logger& instance()
    {
        static Logger duyNhat;
        return duyNhat;
    }
    Logger(const Logger&) = delete;
    Logger& operator=(const Logger&) = delete;

    void ghi(const std::string& noiDung) { file_ << noiDung << '\n'; }

private:
    Logger() : file_("nhat_ky.txt", std::ios::app) {}
    std::ofstream file_;
};

#endif

Logger mở file nhat_ky.txt ở chế độ ghi thêm std::ios::app một lần duy nhất, và file tự đóng khi chương trình kết thúc nhờ RAII.

ThuVien: nghiệp vụ chính

File thu_vien.h:

#ifndef THU_VIEN_H
#define THU_VIEN_H

#include "tai_lieu.h"
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <vector>

class ThuVien
{
public:
    using HamThongBao = std::function<void(const std::string& docGia, const std::string& tenTaiLieu)>;

    explicit ThuVien(HamThongBao thongBao) : thongBao_(std::move(thongBao)) {}

    int napTuFile(const std::string& tenFile);     // tra ve so dong bi bo qua
    bool luuRaFile(const std::string& tenFile) const;

    void inDanhSach() const;
    void timKiem(const std::string& tuKhoa) const;
    std::string muon(int ma, const std::string& docGia);    // tra ve thong bao ket qua
    std::string tra(int ma);
    std::string datCho(int ma, const std::string& docGia);

private:
    std::map<int, std::unique_ptr<TaiLieu>> kho_;
    std::map<int, std::vector<std::string>> nguoiCho_;      // ma tai lieu -> danh sach doc gia cho
    HamThongBao thongBao_;
};

#endif
  • Kho là std::map<int, std::unique_ptr<TaiLieu>>: tra cứu theo mã nhanh, tự sắp theo mã khi in, và tự giải phóng mọi tài liệu khi ThuVien bị hủy.
  • Danh sách người chờ là std::map<int, std::vector<std::string>>: mỗi mã tài liệu có một hàng chờ.
  • Hàm báo tin là một std::function truyền vào constructor: ThuVien chỉ biết “gọi hàm này để báo”, còn báo bằng cách in ra màn hình, gửi email hay SMS là do bên ngoài quyết định. Đây là Observer.

File thu_vien.cpp:

#include "thu_vien.h"
#include "logger.h"
#include <fstream>
#include <iostream>

int ThuVien::napTuFile(const std::string& tenFile)
{
    std::ifstream file(tenFile);
    std::string dong;
    int soDongLoi = 0;
    while (std::getline(file, dong))
    {
        try
        {
            auto tl = taoTaiLieu(dong);
            int ma = tl->ma();
            kho_[ma] = std::move(tl);
        }
        catch (const std::exception&)
        {
            soDongLoi++;
        }
    }
    return soDongLoi;
}

bool ThuVien::luuRaFile(const std::string& tenFile) const
{
    std::ofstream file(tenFile);
    if (!file)
        return false;
    for (const auto& [ma, tl] : kho_)
        file << tl->dongLuuFile() << '\n';
    return true;
}

void ThuVien::inDanhSach() const
{
    for (const auto& [ma, tl] : kho_)
        std::cout << "  " << ma << "  " << tl->moTa() << (tl->dangMuon() ? "  (dang muon)" : "") << '\n';
}

void ThuVien::timKiem(const std::string& tuKhoa) const
{
    int dem = 0;
    for (const auto& [ma, tl] : kho_)
    {
        if (tl->ten().find(tuKhoa) != std::string::npos)
        {
            std::cout << "  " << ma << "  " << tl->moTa() << '\n';
            dem++;
        }
    }
    if (dem == 0)
        std::cout << "  Khong tim thay '" << tuKhoa << "'\n";
}

std::string ThuVien::muon(int ma, const std::string& docGia)
{
    auto it = kho_.find(ma);
    if (it == kho_.end())
        return "Khong co tai lieu ma " + std::to_string(ma);
    TaiLieu& tl = *it->second;
    if (tl.dangMuon())
        return "'" + tl.ten() + "' dang co nguoi muon, hay dat cho";
    tl.datDangMuon(true);
    Logger::instance().ghi(docGia + " muon " + tl.ten());
    return docGia + " muon '" + tl.ten() + "', han tra " + std::to_string(tl.soNgayMuonToiDa()) + " ngay";
}

std::string ThuVien::tra(int ma)
{
    auto it = kho_.find(ma);
    if (it == kho_.end() || !it->second->dangMuon())
        return "Tai lieu ma " + std::to_string(ma) + " khong o trang thai dang muon";
    TaiLieu& tl = *it->second;
    tl.datDangMuon(false);
    Logger::instance().ghi("Tra " + tl.ten());

    for (const std::string& nguoi : nguoiCho_[ma])      // Observer: bao cho nguoi dang cho
        thongBao_(nguoi, tl.ten());
    nguoiCho_.erase(ma);
    return "Da tra '" + tl.ten() + "'";
}

std::string ThuVien::datCho(int ma, const std::string& docGia)
{
    auto it = kho_.find(ma);
    if (it == kho_.end())
        return "Khong co tai lieu ma " + std::to_string(ma);
    nguoiCho_[ma].push_back(docGia);
    return docGia + " da dat cho '" + it->second->ten() + "'";
}

Một số điểm đáng chú ý:

  • napTuFile bọc từng dòng trong try: dòng lỗi chỉ làm tăng biến đếm, không làm hỏng cả quá trình nạp.
  • muon và tra trả về chuỗi kết quả thay vì tự in ra màn hình. Class nghiệp vụ không nên quyết định cách hiển thị, như đã bàn ở bài lớp và đối tượng.
  • muon tìm bằng find chứ không dùng kho_[ma], vì toán tử [] của map sẽ tự thêm một phần tử rỗng khi mã không tồn tại.

main.cpp: menu và nhập liệu

#include "thu_vien.h"
#include <iostream>
#include <string>

int docSo(const std::string& loiNhac)
{
    while (true)
    {
        std::cout << loiNhac;
        std::string dong;
        if (!std::getline(std::cin, dong))
            return 0;                                   // het du lieu vao: coi nhu chon thoat
        try
        {
            return std::stoi(dong);
        }
        catch (const std::exception&)
        {
            std::cout << "  Vui long nhap so\n";
        }
    }
}

std::string docChuoi(const std::string& loiNhac)
{
    std::cout << loiNhac;
    std::string dong;
    std::getline(std::cin, dong);
    return dong;
}

int main()
{
    const std::string FILE_KHO = "kho.txt";
    ThuVien tv([](const std::string& docGia, const std::string& ten) {
        std::cout << "  [Thong bao] " << docGia << ": '" << ten << "' da co, moi den muon\n";
    });

    int soDongLoi = tv.napTuFile(FILE_KHO);
    std::cout << "Da nap kho tu " << FILE_KHO << (soDongLoi ? ", bo qua " + std::to_string(soDongLoi) + " dong loi" : "") << '\n';

    int chon;
    do
    {
        std::cout << "\n1.Danh sach 2.Tim 3.Muon 4.Tra 5.Dat cho 0.Luu va thoat\n";
        chon = docSo("Chon: ");
        switch (chon)
        {
        case 1:
            tv.inDanhSach();
            break;
        case 2:
        {
            std::string tuKhoa = docChuoi("Tu khoa: ");
            tv.timKiem(tuKhoa);
            break;
        }
        case 3:
        {
            int ma = docSo("Ma tai lieu: ");
            std::string docGia = docChuoi("Ten doc gia: ");
            std::cout << "  " << tv.muon(ma, docGia) << '\n';
            break;
        }
        case 4:
        {
            int ma = docSo("Ma tai lieu: ");
            std::string ketQua = tv.tra(ma);
            std::cout << "  " << ketQua << '\n';
            break;
        }
        case 5:
        {
            int ma = docSo("Ma tai lieu: ");
            std::string docGia = docChuoi("Ten doc gia: ");
            std::cout << "  " << tv.datCho(ma, docGia) << '\n';
            break;
        }
        case 0:
            std::cout << (tv.luuRaFile(FILE_KHO) ? "Da luu kho. Tam biet!\n" : "Loi luu file!\n");
            break;
        default:
            std::cout << "  Lua chon khong hop le\n";
            break;
        }
    } while (chon != 0);
    return 0;
}

Hàm docSo đọc cả dòng bằng std::getline rồi mới đổi sang số bằng std::stoi, nên không bao giờ bị lỗi trôi dòng, và gõ chữ thì nhắc nhập lại thay vì làm chương trình chạy sai. Mỗi case đọc xong dữ liệu vào biến rồi mới in kết quả, để các câu hỏi và kết quả hiện ra đúng thứ tự.

Biên dịch và chạy thử

Các file của project quản lý thư viện, nội dung kho.txt và lệnh biên dịch g++ nhiều file

Chạy thử một kịch bản: xem danh sách, An mượn “Clean Code”, Bình mượn cùng cuốn thì bị từ chối nên đặt chỗ, An trả sách thì Bình được báo:

Chạy chương trình quản lý thư viện: mượn, bị từ chối, đặt chỗ, trả sách và nhận thông báo

Sau khi thoát, file kho được lưu lại và nhật ký có đủ các lượt mượn, trả:

Nội dung file kho.txt và nhat_ky.txt sau khi chạy, dòng bản đồ lỗi đã bị mất

Lưu ý: Để ý dòng bando đã biến mất khỏi kho.txt sau khi lưu. Chương trình bỏ qua dòng lỗi lúc nạp, rồi ghi đè cả file lúc lưu, nên dữ liệu đó mất vĩnh viễn. Chương trình thật nên giữ nguyên các dòng không hiểu được, hoặc ghi ra file mới rồi mới thay thế file cũ, hoặc ít nhất là sao lưu file cũ trước khi ghi đè.

Hướng mở rộng

Project này cố ý giữ đơn giản để đọc được trong một bài. Một số hướng các bạn có thể tự làm tiếp để luyện tập:

  • Lưu cả trạng thái đang mượn và người mượn ra file, hiện tại khởi động lại là mất.
  • Thêm class DocGia với giới hạn số cuốn mượn, và tính tiền phạt theo Strategy pattern ở bài 33.
  • Dùng State pattern cho trạng thái tài liệu (có sẵn, đang mượn, quá hạn, đã đặt trước) thay cho biến bool.
  • Thêm luồng tự động sao lưu kho mỗi 5 phút như bài đa luồng.
  • Viết lại phần giao diện bằng Qt, phần ThuVien giữ nguyên, vì nó không phụ thuộc vào cách hiển thị.

Kết

Project quản lý thư viện ráp gần như toàn bộ kiến thức của serie vào một chương trình chạy thật: class trừu tượng và đa hình cho các loại tài liệu, STL và smart pointer để quản lý kho, Factory, Singleton, Observer cho thiết kế, ngoại lệ và đọc ghi file cho phần dữ liệu. Điều quan trọng nhất là cách chia trách nhiệm: mỗi file làm một việc, nên muốn đổi giao diện hay thêm loại tài liệu đều chỉ phải sửa đúng một chỗ.

Muốn tra cứu chi tiết về std::unique_ptr và std::map, các bạn xem thêm tài liệu chuẩn trên cppreference.

Tự gõ lại project từ đầu, thay vì copy, là cách ôn tập tốt nhất cho cả serie. Bài tiếp theo là project quản lý sinh viên, tập trung nhiều hơn vào đọc ghi file và xử lý dữ liệu. Nếu thấy bài viết hữu ích, hãy chia sẻ cho bạn bè đang học C++ nhé.

Rate this post

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *