Trong bài này chúng ta sẽ so sánh lập trình hướng đối tượng (OOP) và lập trình hướng cấu trúc (POP) bằng cách giải cùng một bài toán theo cả 2 cách. Đây là bài mở đầu chương lập trình hướng đối tượng của serie: hiểu được OOP sinh ra để giải quyết vấn đề gì thì các bài về class, kế thừa, đa hình phía sau sẽ dễ hơn rất nhiều.
Bài 14 trong Serie Lập trình C++ từ A tới Z
Các cách tổ chức chương trình
Từ khi có máy tính tới nay, cách viết chương trình đi qua 3 giai đoạn chính:
- Lập trình tuyến tính: cả chương trình là một khối lệnh chạy từ trên xuống dưới. Hợp với chương trình vài chục dòng, lớn hơn là không quản lý nổi.
- Lập trình hướng cấu trúc (Procedure Oriented Programming, POP): chia chương trình thành các hàm, mỗi hàm làm một việc. Dữ liệu để riêng, hàm nhận dữ liệu vào để xử lý. Ngôn ngữ C là đại diện tiêu biểu.
- Lập trình hướng đối tượng (Object Oriented Programming, OOP): gói dữ liệu và các hàm xử lý dữ liệu đó vào chung một đối tượng. C++, Java, C#, Python đều hỗ trợ OOP.
Lấy ví dụ chiếc ô tô. Theo kiểu POP, chiếc xe là một đống dữ liệu (tốc độ, xăng, số km), còn “tăng tốc”, “đổ xăng” là các hàm nằm ngoài, ai cũng có thể chỉnh thẳng kim đồng hồ tốc độ. Theo kiểu OOP, chiếc xe là một đối tượng: muốn tăng tốc thì đạp ga, tức là gọi hàm của chính chiếc xe, chứ không ai được chui vào chỉnh kim đồng hồ.
Bài toán: quản lý một đầu sách
Chương trình thư viện cần quản lý một đầu sách gồm tên sách và số cuốn còn trong kho. Mượn sách thì giảm số lượng, hết sách thì báo hết. Ràng buộc quan trọng: số lượng không bao giờ được âm.
Cách 1: hướng cấu trúc với struct và hàm
Viết theo kiểu C: một struct chứa dữ liệu và một hàm xử lý nhận struct vào:
#include <iostream>
#include <string>
struct Sach
{
std::string ten;
int soLuong;
};
void muonSach(Sach& s)
{
if (s.soLuong > 0)
{
s.soLuong--;
std::cout << "Muon '" << s.ten << "', con " << s.soLuong << " cuon\n";
}
else
{
std::cout << "'" << s.ten << "' da het\n";
}
}
int main()
{
Sach s{"Lap trinh C++", 1};
muonSach(s);
muonSach(s);
s.soLuong = -5; // khong ai ngan duoc dong nay
std::cout << "So luong bay gio: " << s.soLuong << '\n';
return 0;
}
Hàm muonSach làm đúng: hết sách thì không cho mượn. Nhưng mọi chỗ trong chương trình vẫn sửa thẳng được s.soLuong, và dòng s.soLuong = -5 phá vỡ ràng buộc mà không ai ngăn được. Chương trình 100 dòng thì dễ tìm, chương trình 100 nghìn dòng với 20 người cùng viết thì đây là lỗi rất khó truy ra.
Nếu đã học struct và typedef trong C, các bạn sẽ thấy cách viết này rất quen.
Cách 2: hướng đối tượng với class
Viết lại bằng class, đưa dữ liệu vào phần private và chỉ cho bên ngoài thao tác qua các hàm public:
#include <iostream>
#include <string>
class Sach
{
public:
Sach(const std::string& ten, int soLuong) : ten_(ten), soLuong_(soLuong) {}
void muon()
{
if (soLuong_ > 0)
{
soLuong_--;
std::cout << "Muon '" << ten_ << "', con " << soLuong_ << " cuon\n";
}
else
{
std::cout << "'" << ten_ << "' da het\n";
}
}
int soLuong() const { return soLuong_; }
private:
std::string ten_;
int soLuong_;
};
int main()
{
Sach s("Lap trinh C++", 1);
s.muon();
s.muon();
std::cout << "So luong bay gio: " << s.soLuong() << '\n';
return 0;
}
Kết quả mượn sách giống hệt. Khác biệt nằm ở chỗ: thử thêm dòng s.soLuong_ = -5; vào main thì g++ không cho biên dịch:
soLuong_ là private, chỉ các hàm của chính class Sach mới được sửa. Bên ngoài muốn thay đổi số lượng thì phải gọi muon(), mà muon() luôn kiểm tra trước khi giảm. Ràng buộc “số lượng không âm” giờ được trình biên dịch bảo vệ, chứ không phụ thuộc vào việc mọi người nhớ cẩn thận.
Cú pháp class, hàm khởi tạo Sach(...), public, private chúng ta sẽ học kỹ ở các bài tiếp theo. Bài này các bạn chỉ cần thấy được ý tưởng.
So sánh lập trình hướng cấu trúc và hướng đối tượng
| Tiêu chí | Hướng cấu trúc (POP) | Hướng đối tượng (OOP) |
|---|---|---|
| Đơn vị chính | Hàm | Đối tượng (dữ liệu + hàm) |
| Dữ liệu | Tách riêng, hàm nào cũng truy cập được | Giấu bên trong đối tượng |
| Bảo vệ dữ liệu | Dựa vào người viết code cẩn thận | Trình biên dịch chặn truy cập sai |
| Tái sử dụng | Sao chép hàm | Kế thừa, đa hình |
| Hợp với | Chương trình nhỏ, code gần phần cứng | Chương trình lớn, nhiều người cùng làm |
| Ngôn ngữ tiêu biểu | C | C++, Java, C#, Python |
C++ hỗ trợ cả 2 cách. Các bạn hoàn toàn có thể viết hàm tự do và struct như C, và chỉ dùng class ở những chỗ cần.
4 tính chất của lập trình hướng đối tượng
OOP được xây dựng trên 4 tính chất, mỗi tính chất có một bài riêng trong serie:
- Tính đóng gói (Encapsulation): giấu dữ liệu bên trong đối tượng, chỉ cho truy cập qua các hàm được phép. Đây chính là cái
privateở ví dụ trên. Xem bài tính đóng gói trong C++. - Tính kế thừa (Inheritance): tạo class mới dựa trên class có sẵn, dùng lại code cũ. Ví dụ
SachvàTapChicùng kế thừa từTaiLieu. Xem bài tính kế thừa trong C++. - Tính đa hình (Polymorphism): cùng một lời gọi hàm nhưng mỗi loại đối tượng xử lý theo cách riêng. Xem bài tính đa hình trong C++.
- Tính trừu tượng (Abstraction): chỉ đưa ra những gì người dùng cần biết, giấu chi tiết cài đặt. Xem bài tính trừu tượng trong C++.
Khi nào không cần OOP?
OOP không phải lúc nào cũng tốt hơn. Một vài trường hợp viết kiểu hướng cấu trúc là hợp lý:
- Chương trình nhỏ, một file, vài hàm: tạo class chỉ làm code dài thêm.
- Các hàm thuần tính toán như
tinhTienPhat(soNgay): không có dữ liệu nào cần bảo vệ, để hàm tự do là đủ. - Lập trình vi điều khiển rất ít bộ nhớ: nhiều project vẫn viết bằng C thuần. Dù vậy, các thư viện Arduino cho ESP32 hay thư viện của STM32 viết bằng C++ đều dùng class rất nhiều.
Lưu ý: Đừng cố biến mọi thứ thành class chỉ vì “đang học OOP”. Dấu hiệu nên dùng class là có dữ liệu cần giữ ràng buộc (số lượng không âm, ngày trả sau ngày mượn) hoặc có nhiều loại đối tượng giống nhau nhưng xử lý khác nhau.
Bài tập
- Viết struct
TaiKhoangồm số dư, cùng hàmrutTien(TaiKhoan&, int)không cho rút quá số dư. Trongmain, tìm cách làm số dư âm mà không qua hàm. - Viết lại bài 1 bằng class với số dư là
private. Cách làm số dư âm ở bài 1 còn dùng được không? - Liệt kê 3 đối tượng trong một phần mềm quen thuộc (ví dụ ứng dụng ngân hàng, game), mỗi đối tượng có những dữ liệu và hành động gì.
Lời giải sẽ có trong bài tổng hợp bài tập OOP C++ nhé.
Kết
Lập trình hướng đối tượng không thêm được phép tính nào mà lập trình hướng cấu trúc không làm được, cái nó mang lại là cách tổ chức: gói dữ liệu với các hàm xử lý dữ liệu đó, và để trình biên dịch bảo vệ các ràng buộc thay cho con người. Chương trình càng lớn, nhiều người cùng viết, lợi ích này càng rõ.
Bài tiếp theo chúng ta sẽ đi vào chi tiết cú pháp lớp và đối tượng trong C++. Các bạn thấy bài viết dễ hiểu thì chia sẻ cho bạn bè đang học OOP nhé, và tham gia hội Nghiện lập trình để cùng thảo luận.






Quá hay ạ