Factory pattern trong C++: tạo đúng loại đối tượng theo dữ liệu

Factory pattern trong C++: tạo đúng loại đối tượng theo dữ liệu - Khuê Nguyễn Creator

Trong bài này chúng ta sẽ học Factory pattern trong C++: gom toàn bộ việc “tạo loại đối tượng nào” về một chỗ duy nhất, phần còn lại của chương trình chỉ làm việc với lớp cha. Đây là pattern các bạn sẽ cần ngay khi chương trình bắt đầu đọc dữ liệu từ file hay từ người dùng để tạo đối tượng đa hình.

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

Bài toán: đọc kho tài liệu từ file

Kho của thư viện gồm sách và tạp chí, lưu trong file, mỗi dòng có dạng loai|ten|thong_tin_them:

  • sach|Lap trinh C++|Bjarne Stroustrup: thông tin thêm là tác giả.
  • tapchi|Tap chi Dien tu|125: thông tin thêm là số kỳ.

Đọc mỗi dòng phải tạo đúng một Sach hoặc TapChi, rồi lưu chung trong một danh sách TaiLieu như bài tính đa hình trong C++.

Cách làm ngây thơ là viết if (loai == "sach") ... else if (loai == "tapchi") ... ngay chỗ đọc file. Rồi chức năng nhập tài liệu từ bàn phím, chức năng nhận dữ liệu từ máy chủ, mỗi chỗ lại một khối if y hệt. Thêm loại “bản đồ” là phải đi tìm và sửa tất cả các chỗ đó, sót một chỗ là lỗi.

Factory pattern là gì?

Factory (nhà máy) là một hàm hoặc class chuyên làm một việc: nhận thông tin đầu vào, quyết định tạo loại đối tượng nào, và trả về đối tượng đó qua kiểu lớp cha. Mọi chỗ cần tạo tài liệu đều gọi factory, không ai tự new Sach(...) nữa.

Giống quầy nhận đặt hàng của một xưởng: khách chỉ đưa phiếu “cần 1 bàn gỗ”, quầy tự chuyển xuống đúng tổ làm bàn. Khách không cần biết xưởng có mấy tổ, tổ nào nằm ở đâu.

Sơ đồ Factory pattern trong C++: các dòng dữ liệu đi qua hàm taoTaiLieu thành Sach, TapChi hoặc ngoại lệ

Viết Factory pattern trong C++

#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <string>
#include <vector>

class TaiLieu
{
public:
    explicit TaiLieu(const std::string& ten) : ten_(ten) {}
    virtual ~TaiLieu() = default;
    virtual void inThongTin() const = 0;

protected:
    std::string ten_;
};

class Sach : public TaiLieu
{
public:
    Sach(const std::string& ten, const std::string& tacGia) : TaiLieu(ten), tacGia_(tacGia) {}
    void inThongTin() const override { std::cout << "[Sach] " << ten_ << " - " << tacGia_ << '\n'; }

private:
    std::string tacGia_;
};

class TapChi : public TaiLieu
{
public:
    TapChi(const std::string& ten, int soKy) : TaiLieu(ten), soKy_(soKy) {}
    void inThongTin() const override { std::cout << "[Tap chi] " << ten_ << " so " << soKy_ << '\n'; }

private:
    int soKy_;
};

// Factory: nhan mot dong du lieu, tu quyet dinh tao loai tai lieu nao
std::unique_ptr<TaiLieu> taoTaiLieu(const std::string& dong)
{
    std::stringstream ss(dong);
    std::string loai, ten, thongTinThem;
    std::getline(ss, loai, '|');
    std::getline(ss, ten, '|');
    std::getline(ss, thongTinThem, '|');

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

int main()
{
    std::vector<std::string> duLieu = {
        "sach|Lap trinh C++|Bjarne Stroustrup",
        "tapchi|Tap chi Dien tu|125",
        "bando|Ban do Ha Noi|2024",
        "sach|Clean Code|Robert Martin",
    };

    std::vector<std::unique_ptr<TaiLieu>> kho;
    for (const std::string& dong : duLieu)
    {
        try
        {
            kho.push_back(taoTaiLieu(dong));
        }
        catch (const std::exception& e)
        {
            std::cout << "Bo qua dong loi: " << e.what() << '\n';
        }
    }

    std::cout << "Kho co " << kho.size() << " tai lieu:\n";
    for (const auto& tl : kho)
        tl->inThongTin();
    return 0;
}

Factory tạo tài liệu từ các dòng dữ liệu, bỏ qua dòng bản đồ không hợp lệ

Các điểm chính của hàm factory taoTaiLieu:

  • Kiểu trả về là std::unique_ptr<TaiLieu>, lớp cha trừu tượng. Người gọi không biết, và không cần biết, bên trong là Sach hay TapChi.
  • Trả về unique_ptr thay vì con trỏ thô: quyền sở hữu đối tượng được chuyển rõ ràng cho người gọi, không ai phải nhớ delete. Chi tiết ở bài smart pointer.
  • Loại không biết thì ném ngoại lệ, như dòng bando. Người gọi bắt lỗi và bỏ qua dòng đó, các dòng khác vẫn được nạp. Cách xử lý giống bài xử lý ngoại lệ trong C++.

Giờ thêm loại BanDo chỉ cần 2 bước: viết class BanDo kế thừa TaiLieu, và thêm một dòng if trong taoTaiLieu. Vòng lặp nạp kho, chức năng in, chức năng tìm kiếm… không phải sửa gì.

Lưu ý: Factory chỉ nên là nơi duy nhất trong chương trình biết danh sách các lớp con cụ thể. Nếu thấy ở chỗ khác còn viết if (loai == "sach") ... new Sach hay ép kiểu dynamic_cast<Sach*> để làm việc riêng cho từng loại, đó là dấu hiệu thiết kế đang rò rỉ, nên chuyển việc đó thành hàm ảo của lớp cha.

Factory method

Ví dụ trên là dạng đơn giản nhất, gọi là simple factory: một hàm chứa toàn bộ quyết định. Sách GoF còn mô tả factory method: lớp cha khai báo một hàm ảo tạo đối tượng, mỗi lớp con tự quyết định tạo gì.

Ví dụ, thư viện có nhiều chi nhánh, mỗi chi nhánh in phiếu mượn theo mẫu riêng. Lớp ChiNhanh có hàm ảo taoPhieuMuon() trả về unique_ptr<PhieuMuon>, chi nhánh trung tâm trả về phiếu đầy đủ, chi nhánh trường học trả về phiếu rút gọn. Code xử lý mượn sách chỉ gọi chiNhanh.taoPhieuMuon() mà không cần biết đang ở chi nhánh nào.

Với phần lớn chương trình, simple factory là đủ. Factory method hữu ích khi chính “nơi tạo” cũng có nhiều biến thể.

Khi nào nên dùng Factory?

  • Khi tạo đối tượng đa hình dựa trên dữ liệu chỉ biết lúc chạy: dòng trong file, lựa chọn của người dùng, gói tin từ mạng.
  • Khi việc tạo đối tượng phức tạp (kiểm tra dữ liệu, chọn tham số, cấu hình thêm) và lặp lại ở nhiều nơi.
  • Không cần khi chỉ có một loại đối tượng, hoặc loại được biết sẵn lúc viết code: cứ std::make_unique<Sach>(...) trực tiếp là đủ.

Bài tập

  1. Thêm class BanDo (thông tin thêm là năm xuất bản) và sửa factory để dòng bando|Ban do Ha Noi|2024 được nạp thành công.
  2. Dòng tapchi|Tap chi X|abc có số kỳ không phải số. Chuyện gì xảy ra? Sửa factory để ném ngoại lệ với thông báo rõ ràng hơn.
  3. Kết hợp với bài đọc ghi file: đọc danh sách tài liệu từ file kho.txt qua factory, in ra số dòng nạp được và số dòng bị bỏ qua.

Lời giải sẽ có trong bài tập tổng hợp của chương nhé.

Kết

Factory pattern trong C++ gom việc quyết định tạo loại đối tượng nào về một chỗ, trả về std::unique_ptr tới lớp cha, nhờ đó phần còn lại của chương trình làm việc hoàn toàn qua đa hình. Thêm loại mới chỉ phải sửa factory và viết class mới, không động tới code cũ, đúng nguyên tắc mở để mở rộng, đóng để sửa đổi.

Tham khảo thêm ở Refactoring.Guru: Factory Method. Bài trước là Singleton, bài tiếp theo chúng ta học Observer pattern để báo tin cho nhiều nơi khi dữ liệu thay đổi. Nếu thấy bài viết có ích, hãy chia sẻ cho bạn bè 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 *