Think Like a Programmer #6 — Lớp đối tượng: Từ code chạy được đến code có kiến trúc

Phong

🧠 Think Like a Programmer #6 — Lớp đối tượng: Từ code chạy được đến code có kiến trúc

Ảnh: Brett Jordan — Pexels

Mở đầu

Mấy bài trước, mình đã cùng mấy bạn đi qua một chặng đường khá dài:

  • Chương 1 — Chiến lược giải quyết vấn đề tổng quát
  • Chương 2 — Pure Puzzles, luyện tư duy với vòng lặp
  • Chương 3 — Arrays, tổ chức dữ liệu có hệ thống
  • Chương 4 — Pointers & Dynamic Memory, hiểu sâu về bộ nhớ

Mỗi chương là một bước tiến từ "làm sao để code chạy""làm sao để code tốt". Và giờ tới Chương 5 — Solving Problems with Classes, mình cảm giác như đây là đích đến của tất cả những chương trước.

Bởi vì class (lớp đối tượng) là nơi hội tụ của mọi thứ: cấu trúc dữ liệu (array + pointer), thuật toán, và — quan trọng nhất — tư duy thiết kế.

Solving Problems with Classes — Lớp đối tượng

1. Class không phải là "cái hộp đựng code"

Nhiều lập trình viên mới học OOP thường nghĩ: class là cái hộp để nhồi code vào cho có tổ chức. Biến để ở đây, hàm để ở kia — gọn gàng hơn mớ functions rời rạc.

Spraul không nghĩ vậy. Và ổng không dạy vậy.

Với Spraul, class là một công cụ tư duy. Khi bạn tạo một class, bạn đang tạo ra một mental model — một cách nghĩ về vấn đề. Class không chỉ nhóm code lại, nó nhóm ý nghĩa lại.

Ví dụ: thay vì có một cái mảng chứa tên sinh viên, một mảng chứa điểm, và một mảng chứa mã số — rời rạc, khó quản lý — bạn gom tất cả vào một struct/class StudentRecord. Đột nhiên, việc suy nghĩ về "một sinh viên" trở nên tự nhiên hơn hẳn. Bạn không còn phải nhớ "cái index thứ 5 trong mảng điểm tương ứng với cái index thứ 5 trong mảng tên" — tất cả đã được đóng gói gọn ghẽ.

🔑 Bài học: Class không chỉ giúp code sạch hơn — nó giúp bộ não bạn làm việc hiệu quả hơn.

2. Encapsulation — Giấu đi để đơn giản hoá

Một trong những ý quan trọng nhất chương này là encapsulation (đóng gói) và information hiding (che giấu thông tin). Spraul giải thích rất rõ: khi bạn thiết kế class, bạn chia thế giới thành hai phần:

  • Interface (public) — những gì người dùng class cần biết để sử dụng
  • Implementation (private) — những chi tiết bên trong, không ai cần thấy

Spraul đưa ra ví dụ về class studentRecord:

class studentRecord {
public:
    studentRecord();
    studentRecord(int newGrade, int newID, string newName);
int grade() const;
void setGrade(int newGrade);

int studentID() const;
void setStudentID(int newID);

string name() const;
void setName(string newName);

string letterGrade() const;

private: int _grade; int _studentID; string _name;

bool isValidGrade(int grade) const;

};

Cái hay là hàm setGrade có validation — chỉ nhận grade từ 0–100, ném lỗi nếu sai. Và hàm letterGrade() dùng lookup table (như kỹ thuật từ Chương 3) để chuyển điểm số thành chữ cái A/B/C/D/F.

Tất cả logic phức tạp nằm ở private. Người dùng class chỉ cần gọi setGrade(85) rồi letterGrade() — thế là xong. Họ không cần biết bên trong có validation, có lookup table, có xử lý ngoại lệ gì không. Đó là sức mạnh của encapsulation.

🔑 Bài học: Interface càng đơn giản, class càng dễ dùng. Giấu hết phức tạp vào bên trong.

Ảnh: Lukas Blazek — Pexels

3. Linked List Class — Khi con trỏ gặp class

Spraul tiếp tục với một ví dụ thực tế hơn: xây dựng một class quản lý danh sách sinh viên dùng linked list (kế thừa kiến thức từ Chương 4).

Class studentCollection có các phương thức:

class studentCollection {
public:
    studentCollection();
    ~studentCollection();
void addRecord(studentRecord newStudent);
studentRecord recordWithNumber(int idNum);
void removeRecord(int idNum);

private: struct studentNode { studentRecord studentData; studentNode* next; }; studentNode* _listHead; };

Điều thú vị: struct studentNode được định nghĩa bên trong private của class. Bên ngoài không ai biết có cái node, có con trỏ, có linked list gì hết. Họ chỉ thấy addRecord, recordWithNumber, removeRecord — những thao tác trực quan với danh sách sinh viên.

Spraul hướng dẫn từng bước:

  • addRecord — thêm vào đầu danh sách (O(1), đơn giản)
  • recordWithNumber — duyệt linked list, tìm bằng ID, trả về dummy record nếu không tìm thấy
  • removeRecord — xoá node, phải xử lý trường hợp đặc biệt: xoá node đầu tiên khác với xoá node ở giữa

🔑 Bài học: Class cho phép bạn đóng gói cả cấu trúc dữ liệu phức tạp vào một interface sạch sẽ. Người dùng không cần biết bên trong là linked list, array, hay database — chỉ cần gọi addRecord là đủ.

4. Rule of Three — Bẫy chết người với Dynamic Memory

Đây là phần mình thấy quan trọng nhất trong chương này. Khi class của bạn có con trỏ và cấp phát bộ nhớ động (như studentCollection với linked list), bạn cần triển khai ba thứ:

  1. Destructor (~studentCollection) — giải phóng tất cả nodes khi đối tượng bị huỷ
  2. Copy constructor (studentCollection(const studentCollection &other)) — tạo bản sao sâu (deep copy), không chỉ copy con trỏ
  3. Assignment operator (operator=) — gán bản sao sâu, kèm xử lý self-assignment

Tại sao lại cần cả ba? Nếu bạn copy một đối tượng studentCollection mà chỉ copy con trỏ _listHead (shallow copy), hai đối tượng sẽ cùng trỏ tới một linked list. Khi một cái bị destruct, list bị xoá sạch — cái kia trở thành dangling pointer. Rồi khi cái kia destruct lần nữa — double delete, crash.

Spraul gọi đây là Rule of Three (ba anh em không thể tách rời). Trong C++ hiện đại, bạn có thể dùng smart pointers để tránh, nhưng hiểu được "tại sao" vẫn rất quan trọng. Đây cũng là nền tảng cho ownership system trong Rust sau này.

🔑 Bài học: Dynamic memory trong class không phải chuyện đùa. Luôn nhớ Rule of Three: destructor + copy constructor + assignment operator.

5. "Fake Class" và "Single-Tasker" — Hai sai lầm thường gặp

Cuối chương, Spraul chỉ ra hai lỗi phổ biến khi thiết kế class:

📛 Fake Class: Khi bạn tạo một class nhưng nó không đại diện cho một khái niệm thống nhất. Ví dụ: class DatabaseAndPrintingAndNetworking — nhồi nhét mọi thứ vào một class vì "tiện". Dấu hiệu nhận biết: bạn không thể đặt tên class bằng một danh từ duy nhất.

📛 Single-Tasker: Khi bạn tạo một class chỉ để làm đúng một việc, quá cụ thể, không tái sử dụng được. Ví dụ: class ComputeAverageGradeOfSeniorStudents — quá hẹp. Thay vào đó, hãy thiết kế class StudentRecord tổng quát, rồi viết hàm riêng để tính trung bình.

Quy tắc của Spraul: tên class phải là một danh từ có nghĩa. Nếu bạn phải dùng 5 từ để đặt tên class, có thể bạn đang sai.

Ảnh: ThisIsEngineering — Pexels

Cảm nhận của mình

Đọc tới chương này, mình có cảm giác như Spraul đã dẫn dắt mình đi hết một hành trình:

  • Chương 1: Học cách suy nghĩ có chiến lược
  • Chương 2–3: Học cách dùng cấu trúc điều khiển + array để giải quyết vấn đề
  • Chương 4: Học cách quản lý bộ nhớ — nền tảng của mọi thứ
  • Chương 5: Gộp tất cả lại — dùng class để xây dựng hệ thống

Có một câu trong sách mình rất thích (mình paraphrase): "The biggest payoff from using classes isn't cleaner code — it's cleaner thinking."

Khi bạn thiết kế một class, bạn buộc phải tư duy ở một tầm cao hơn. Bạn không còn nghĩ "tôi cần in dòng này ra màn hình" — bạn nghĩ "tôi cần một đối tượng ReportGenerator để tạo báo cáo". Sự thay đổi mindset này rất quan trọng.

Cá nhân mình thấy cái hay nhất của chương này là phần về interface design. Hồi mới học OOP, mình toàn để public hết mọi biến — cho nhanh, cho tiện. Kết quả là code chạy được nhưng không ai dám đụng vào sợ hỏng. Sau này đi làm, mình mới thấm: một interface tốt (ít method, rõ ràng, dễ hiểu) quyết định chất lượng của cả hệ thống.

Rule of Three cũng là thứ làm mình nhức nhối. Hồi đi học, mình từng mất 3 ngày debug một cái bug mà cuối cùng chỉ vì thiếu copy constructor. Hai đối tượng "tưởng là riêng" nhưng thực ra dùng chung một vùng nhớ — thay đổi cái này ảnh hưởng cái kia. Kinh nghiệm xương máu. 😅

Một điểm thú vị: các nguyên lý Spraul dạy trong chương này — encapsulation, information hiding, interface design — thực ra không chỉ áp dụng cho C++ hay OOP. Nó áp dụng cho mọi ngôn ngữ, mọi paradigm. Kể cả khi bạn viết functional programming (Haskell, Rust, Elixir), bạn vẫn cần biết cách nhóm logic, giấu implementation, thiết kế API sạch.

Kết

Chương 5 là một trong những chương hay nhất của cuốn sách. Nó không dạy cú pháp class (bạn có thể đọc ở bất kỳ tutorial nào). Nó dạy tại sao class tồn tại và khi nào nên dùng chúng.

Bài học lớn nhất mình rút ra:

  • Class là công cụ tư duy, không chỉ là công cụ tổ chức code
  • Interface càng đơn giản càng tốt — giấu hết phức tạp vào private
  • Luôn nhớ Rule of Three khi class có dynamic memory
  • Đặt tên class bằng một danh từ — nếu không được, hãy thiết kế lại

Hẹn mấy bạn ở bài tiếp theo — Chương 6: Solving Problems with Recursion — nơi Spraul đưa ta vào thế giới "hàm gọi chính nó" và cách tư duy đệ quy (Big Recursive Idea). Chương này nghe đồn là khó nhất cuốn sách, nhưng mình tin là sẽ rất thú vị! 🚀

📋 Phụ lục thuật ngữ
- Class — lớp đối tượng, một cấu trúc dữ liệu kết hợp dữ liệu (thuộc tính) và thao tác (phương thức)
- Encapsulation — đóng gói, che giấu chi tiết cài đặt bên trong class
- Information hiding — che giấu thông tin, chỉ để lộ interface cần thiết
- Interface — giao diện công khai của class (các method public)
- Implementation — cài đặt bên trong class (private)
- Deep copy — bản sao sâu, copy toàn bộ dữ liệu chứ không chỉ copy con trỏ
- Shallow copy — bản sao nông, chỉ copy con trỏ dẫn tới cùng vùng nhớ
- Rule of Three — quy tắc: nếu class có dynamic memory, cần destructor + copy constructor + assignment operator
- Dangling pointer — con trỏ trỏ tới vùng nhớ đã được giải phóng
- Linked list — danh sách liên kết, cấu trúc dữ liệu dùng con trỏ nối các node
- Lookup table — bảng tra cứu, dùng mảng tĩnh thay cho logic rẽ nhánh (kỹ thuật từ Chương 3)

Tags: #sách #thinklikeaprogrammer #problemsolving #softwareengineering #OOP #classes #encapsulation