Lập trình hướng thủ tục (POP) và lập trình hướng đối tượng (OOP)
1. Tổng quan
A. Khái niệm
Hướng thủ tục (Procedural-Oriented Programming, POP) là cách tổ chức chương trình thành luồng thực thi tuần tự của một chuỗi hàm (thủ tục), còn hướng đối tượng (Object-Oriented Programming, OOP) là cách gói dữ liệu cùng các hàm xử lý dữ liệu đó thành đối tượng (Object) và tổ chức chương trình bằng việc trao đổi thông điệp (tương tác) giữa các đối tượng.
Lý do gốc để so sánh hai mô thức không đơn thuần là "cú pháp khác nhau", mà nằm ở câu hỏi bản chất của kỹ nghệ phần mềm: khi phần mềm lớn lên và tồn tại lâu dài, phải lấy gì làm trung tâm để dựng cấu trúc thì chi phí bảo trì·quản lý mới thấp. Kinh nghiệm lâu đời cho thấy tổng chi phí phần mềm do bảo trì·thay đổi lớn hơn nhiều so với chi phí phát triển ban đầu (các khảo sát liên tục báo cáo 60~80% chi phí toàn vòng đời phát sinh ở giai đoạn bảo trì), và thứ quyết định chi phí này chính là phạm vi lan truyền của thay đổi. Hai mô thức kiểm soát "khi có thay đổi thì ảnh hưởng lan tới đâu" theo những cách khác nhau.
Hướng thủ tục lấy 'làm gì (hành vi·hàm)' làm khung xương của chương trình. Cấu trúc dữ liệu có trước, và các hàm xử lý dữ liệu đó theo thứ tự chảy từ trên xuống dưới. Nó ăn khớp tự nhiên với phân rã từ trên xuống (Top-Down) chia bài toán thành 'tác vụ lớn → tác vụ nhỏ', và vì luồng thực thi lộ nguyên trong mã nên với chương trình nhỏ và đơn giản thì trực quan, gọn gàng. Tuy nhiên, khi quy mô lớn lên, dữ liệu bị chia sẻ·phơi bày cho nhiều hàm (đặc biệt là dữ liệu toàn cục), thay đổi một cấu trúc dữ liệu buộc phải sửa cùng lúc mọi hàm chạm vào dữ liệu đó, và con người khó truy vết ảnh hưởng của việc sửa lan tới đâu. Tức là dữ liệu và hàm tách rời khiến sự kết dính rơi vào trạng thái 'trông có vẻ lỏng nhưng thực ra lan rộng'.
Hướng đối tượng giải quyết vấn đề này bằng cách đảo ngược sang 'lấy dữ liệu làm trung tâm'. Nó đóng gói dữ liệu liên quan và các hàm xử lý dữ liệu đó thành một đối tượng, giấu dữ liệu bên trong đối tượng (che giấu thông tin) và chỉ cho truy cập qua phương thức — lối đi công khai. Khi đó dù biểu diễn dữ liệu thay đổi, thay đổi chỉ giới hạn bên trong đối tượng đó (vì bên ngoài chỉ biết giao diện), và nhờ kế thừa·đa hình có thể tái sử dụng cấu trúc chung trong khi chỉ mở rộng phần khác biệt. Tóm lại, nếu hướng thủ tục nhìn thế giới như 'luồng của các hàm', thì hướng đối tượng nhìn thế giới như 'các đối tượng mang trách nhiệm và cộng tác với nhau'. Phần mềm càng lớn·vận hành lâu dài·thay đổi yêu cầu thường xuyên thì giá trị của sự cục bộ hóa (localization) cấu trúc này càng lớn.
B. Bối cảnh ra đời và sự cần thiết
Hướng thủ tục định hình trong dòng lập trình có cấu trúc (structured programming) những năm 1970, từ nỗ lực chỉnh đốn 'mã spaghetti' rối rắm do lạm dụng GOTO bằng các cấu trúc điều khiển tuần tự·chọn lựa·lặp và phân rã hàm, với đại diện là C·Pascal·Fortran. Tuy nhiên từ những năm 1980, khi xuất hiện các phần mềm có trạng thái và tương tác tăng bùng nổ như GUI, hệ thống nghiệp vụ quy mô lớn, nhận thức về 'khủng hoảng phần mềm (software crisis)' — chỉ phân rã hàm thì khó gánh nổi độ phức tạp — lan rộng. Tại đây, Smalltalk kế thừa khái niệm lớp của Simula để hiện thực hướng đối tượng thuần túy, và C++·Java phổ biến hướng đối tượng vào công nghiệp, đưa nó lên thành mô thức chuẩn của phát triển cộng tác quy mô lớn. Tức là sự ra đời của OOP không phải vấn đề 'sở thích cú pháp' mà bắt nguồn từ nhu cầu thực tế là quản lý độ phức tạp và phân công lao động theo nhóm.
| Phân loại | Góc nhìn trung tâm | Thế giới quan | Cách phân rã tiêu biểu |
|---|---|---|---|
| Hướng thủ tục | Hàm (hành vi) | Luồng xử lý tuần tự | Phân rã chức năng (Top-Down) |
| Hướng đối tượng | Đối tượng (dữ liệu+hành vi) | Các đối tượng cộng tác | Phân chia trách nhiệm (vai trò·cộng tác) |
2. So sánh cấu trúc của hai mô thức
Sơ đồ khái niệm dưới đây cho thấy khác biệt trong cách hai mô thức bố trí dữ liệu và hàm. Trong hướng thủ tục, dữ liệu nằm bên ngoài các hàm và được chia sẻ cho nhiều hàm, còn trong hướng đối tượng, dữ liệu đi vào bên trong từng đối tượng và chỉ qua lại bằng thông điệp.
flowchart LR
subgraph POP["Hướng thủ tục (POP)"]
D1["Dữ liệu chia sẻ"] --> F1["Hàm 1"] --> F2["Hàm 2"] --> F3["Hàm 3"]
F2 -.đọc/ghi.-> D1
F3 -.đọc/ghi.-> D1
end
subgraph OOP["Hướng đối tượng (OOP)"]
O1["Đối tượng A<br/>(dữ liệu+phương thức)"] <-- "thông điệp" --> O2["Đối tượng B<br/>(dữ liệu+phương thức)"]
O2 <-- "thông điệp" --> O3["Đối tượng C<br/>(dữ liệu+phương thức)"]
end
style OOP fill:#e8f0fe,stroke:#2f6fed
Kết quả mà khác biệt cấu trúc này tạo ra trong thực tế rất rõ ràng. Ví dụ, hãy nghĩ tới chương trình xử lý số dư tài khoản ngân hàng. Trong hướng thủ tục, dữ liệu balance tồn tại toàn cục và các hàm như deposit(), withdraw(), printStatement() đọc ghi trực tiếp. Để cưỡng chế quy tắc số dư không được âm, phải chèn lặp lại cùng một kiểm tra vào mọi hàm chạm tới balance, và nếu nhà phát triển thêm hàm mới quên kiểm tra thì quy tắc bị phá vỡ. Trong hướng đối tượng, balance được giấu ở dạng private trong đối tượng Account và chỉ được giảm thông qua phương thức withdraw(), nên bất biến (invariant) số dư có thể được bảo đảm tại một chỗ. Gắn quy tắc về dữ liệu vào nơi dữ liệu cư trú chính là lợi ích thực chất của đóng gói.
| Phân loại | Hướng thủ tục (POP) | Hướng đối tượng (OOP) |
|---|---|---|
| Đơn vị cơ bản | Hàm (thủ tục) | Đối tượng (lớp) |
| Xử lý dữ liệu | Toàn cục·chia sẻ (phơi bày) | Đóng gói (che giấu) |
| Phương tiện tái sử dụng | Gọi hàm (hạn chế) | Kế thừa·hợp thành·đa hình (dễ dàng) |
| Ảnh hưởng thay đổi | Lan rộng (khó truy vết) | Cục bộ hóa trong đối tượng |
| Hiệu năng | Tương đối nhanh·nhẹ | Chi phí trừu tượng hóa·điều phối động |
| Ngôn ngữ tiêu biểu | C, Pascal, Fortran | Java, C++, C#, Python |
| Lĩnh vực phù hợp | Quy mô nhỏ·xử lý tuần tự·hệ thống·nhúng | Hệ thống nghiệp vụ lớn·phức tạp·thay đổi thường xuyên |
Hạng mục hiệu năng dễ gây hiểu lầm nên cần chỉ ra cả lý do. Đa hình của hướng đối tượng đi kèm điều phối động (dynamic dispatch) quyết định gọi phương thức nào tại thời điểm chạy (tra bảng hàm ảo), nên tốn chi phí nhỏ so với gọi hàm trực tiếp. Còn cộng thêm chi phí gián tiếp của cấp phát bộ nhớ theo đơn vị đối tượng, theo dõi tham chiếu, môi trường máy ảo (JVM v.v.). Tuy nhiên khác biệt này ở mức bỏ qua được trong phần lớn hệ thống nghiệp vụ, và không bù trừ được lợi ích lớn là khả năng bảo trì·mở rộng. Ngược lại, trong các lĩnh vực mà hiệu năng theo từng chu kỳ và hành vi tất định là quan trọng như kernel·driver thiết bị·điều khiển thời gian thực, hướng thủ tục dựa trên C vẫn chiếm ưu thế. Tức là hiệu năng phải được hiểu không phải là 'hơn kém tuyệt đối' mà là đánh đổi theo miền áp dụng.
3. Đi sâu 4 đặc trưng chính của hướng đối tượng
Lợi ích bảo trì·tái sử dụng của OOP đến từ sự ăn khớp của bốn đặc trưng sau. Xem mỗi đặc trưng không phải như định nghĩa đơn thuần mà từ góc độ 'vì sao nó làm giảm chi phí thay đổi'.
A. Đóng gói (Encapsulation) là gói dữ liệu và các phương thức xử lý nó thành một đơn vị, và giấu biểu diễn bên trong khỏi bên ngoài (che giấu thông tin). Hiệu quả cốt lõi là 'bức tường lửa của thay đổi'. Bên ngoài chỉ biết đối tượng có thể làm gì (giao diện công khai) mà không biết làm như thế nào (hiện thực bên trong), nên dù đổi cấu trúc dữ liệu bên trong từ mảng sang hashmap hay tối ưu công thức tính, mã bên ngoài không bị ảnh hưởng. Như ví dụ tài khoản ở trên, bất biến được giữ tại một chỗ, ngăn quy tắc bị rải rác khắp mã.
B. Kế thừa (Inheritance) là lớp con thừa hưởng thuộc tính·hành vi của lớp cha để tái sử dụng·mở rộng. Gom mã chung lên lớp cha giúp giảm trùng lặp, nhưng lớp cha và lớp con bị kết dính chặt nên cũng có điểm yếu là thay đổi ở lớp cha lan xuống toàn bộ lớp con. Vì vậy thiết kế hiện đại khuyến nghị "hợp thành thay vì kế thừa (Composition over Inheritance)", và kế thừa thường chỉ được dùng hạn chế cho 'quan hệ is-a thực sự'.
C. Đa hình (Polymorphism) là tính chất mỗi đối tượng phản ứng khác nhau với cùng một thông điệp. Ví dụ, gọi draw() như nhau trên nhiều hình kiểu Shape thì hình tròn·hình chữ nhật được vẽ theo cách riêng của mình. Phía gọi không cần biết kiểu cụ thể, nên khi thêm lớp hình mới không phải động vào mã gọi hiện có. Đây là nền tảng kỹ thuật của OCP (nguyên tắc đóng-mở) sẽ được bàn phía sau. Biểu đồ lớp dưới đây cho thấy cấu trúc điển hình nơi kế thừa·đa hình ăn khớp. Lớp cha trừu tượng (Shape) quy định draw(), các lớp hiện thực con tự định nghĩa lại, và phía sử dụng (Renderer) chỉ phụ thuộc vào kiểu trừu tượng.
classDiagram
class Shape {
<<abstract>>
+draw()
+area() double
}
class Circle {
+draw()
+area() double
}
class Rectangle {
+draw()
+area() double
}
class Renderer {
+render(Shape) void
}
Shape <|-- Circle
Shape <|-- Rectangle
Renderer ..> Shape
Trong hình này, thay đổi thêm hình mới (ví dụ: Triangle) kết thúc bằng việc thêm một lớp kế thừa Shape, và Renderer chỉ phụ thuộc vào kiểu trừu tượng Shape nên không cần biên dịch lại·sửa đổi. Nếu là hướng thủ tục thì phải sửa câu lệnh rẽ nhánh bên trong hàm xử lý tương ứng với Renderer, và khả năng 'mở rộng mà không sửa' này tạo nên khoảng cách về khả năng mở rộng giữa hai mô thức.
D. Trừu tượng hóa (Abstraction) là rút ra chỉ những thuộc tính bản chất trong miền bài toán để mô hình hóa và giấu đi chi tiết không cần thiết. Dùng interface·lớp trừu tượng chỉ quy định 'làm gì' và giao 'như thế nào' cho lớp hiện thực, tách chính sách cấp cao khỏi chi tiết cấp thấp.
| Đặc trưng | Cơ chế cốt lõi | Lợi ích từ góc độ bảo trì |
|---|---|---|
| Đóng gói | Che giấu thông tin, giao diện public | Cô lập thay đổi bên trong khỏi bên ngoài |
| Kế thừa | Thừa hưởng đặc tính theo phân cấp | Tái sử dụng mã chung (lưu ý kết dính) |
| Đa hình | Điều phối động | Mã hiện có không đổi khi mở rộng |
| Trừu tượng hóa | Interface·lớp trừu tượng | Tách chính sách và hiện thực |
4. Ví dụ so sánh và hàm ý thực tiễn
Khác biệt giữa hai mô thức lộ ra thế nào ở quy mô mã thực tế sẽ rõ ràng khi xem qua kịch bản 'thay đổi yêu cầu'. Giả sử trong hệ thống giám sát điều khiển xử lý hàng chục loại thiết bị, có thay đổi thường gặp là "thêm loại thiết bị mới". Trong hiện thực hướng thủ tục, mã xử lý loại thiết bị bằng rẽ nhánh switch/if nằm rải rác ở nhiều hàm (đăng ký·hiển thị·tổng hợp·cảnh báo), nên thêm một loại thì phải tìm sửa mọi câu lệnh rẽ nhánh đó, bỏ sót một chỗ là thành lỗi. Trong hướng đối tượng, nếu mỗi thiết bị là một lớp hiện thực giao diện chung thì loại mới chỉ cần thêm một lớp, và mã hiện có vẫn hoạt động nguyên vẹn nhờ đa hình. Việc điểm thay đổi giảm từ 'N chỗ' xuống '1 chỗ' chính là lý do định lượng khiến OOP được ưa chuộng trong hệ thống lớn.
Tuy nhiên, kết luận "OOP luôn đúng" là nguy hiểm. Nhân Linux là phần mềm quy mô lớn hơn 20 triệu dòng nhưng được viết·duy trì theo hướng thủ tục dựa trên C (chỉ bắt chước phần đa hình cần thiết bằng struct và con trỏ hàm), đó là do đặc tính miền là hiệu năng·tính khả chuyển·điều khiển sát phần cứng. Ngược lại, dịch vụ web doanh nghiệp quy mô lớn có hàng trăm người cộng tác trên framework hướng đối tượng như Spring (Java), nơi phân tầng và khả năng mở rộng quan trọng hơn hiệu năng. Ngoài ra, khi lập trình hàm (FP) nổi lên, 'trục thứ ba' bảo đảm tính đồng thời·dễ kiểm thử bằng dữ liệu bất biến và hàm thuần túy đã đi vào thực tế. Rốt cuộc, chọn mô thức không phải cuộc đua hơn thua mà là phán đoán về độ phù hợp kỹ thuật với đặc tính miền (hiệu năng·tần suất thay đổi·quy mô nhóm·yêu cầu đồng thời).
5. Chuyên sâu — Nguyên tắc thiết kế·đa mô thức trong ngôn ngữ hiện đại
Hướng đối tượng không tự sinh ra lợi ích chỉ vì dùng cú pháp. Thiết kế sai còn chỉ làm tăng độ phức tạp do bùng nổ lớp và trừu tượng hóa quá mức (còn gọi là 'lạm dụng hướng đối tượng'). Vì vậy giá trị thực của OOP xuất hiện khi kết hợp với nguyên tắc SOLID và mẫu thiết kế (design pattern). SOLID gồm ▲SRP (trách nhiệm đơn nhất) ▲OCP (đóng-mở: mở cho mở rộng, đóng cho sửa đổi) ▲LSP (thay thế Liskov) ▲ISP (phân tách giao diện) ▲DIP (đảo ngược phụ thuộc), và lợi ích 'loại mới chỉ cần thêm lớp' đã thấy ở trên thực chất do sự kết hợp giữa OCP và đa hình tạo ra. Mẫu thiết kế (strategy·observer·factory v.v.) là sự định hình các nguyên tắc này thành cấu trúc cộng tác có thể tái sử dụng. Từ góc độ thiết kế, mục tiêu luôn là độ gắn kết (cohesion) cao và độ kết dính (coupling) thấp, và đây cũng là nguyên tắc phổ quát được theo đuổi như nhau trong hướng thủ tục. [[module-cohesion-coupling]]
Ngôn ngữ hiện đại không áp đặt một mô thức duy nhất. Python·C++·JavaScript·Kotlin là ngôn ngữ đa mô thức (multi-paradigm) hỗ trợ cùng lúc thủ tục·đối tượng·hàm trong một ngôn ngữ, và Java từ phiên bản 8 cũng tiếp nhận mạnh mẽ yếu tố hàm qua lambda·stream. Trong thực tế, việc trộn mô thức theo bài toán như "pipeline biến đổi dữ liệu theo kiểu hàm, mô hình miền và quản lý trạng thái theo hướng đối tượng, vòng lặp cốt lõi về hiệu năng theo kiểu thủ tục" là tự nhiên. Do đó, từ góc nhìn Kỹ sư chuyên nghiệp (Professional Engineer), năng lực quan trọng không phải là bàn "mô thức nào ưu việt hơn", mà là chọn·kết hợp mô thức phù hợp với tính chất của bài toán đã cho và giải thích được đánh đổi của nó.
6. Các điểm cần cân nhắc và hàm ý
Ưu tiên lựa chọn phù hợp với bài toán·quy mô. Lĩnh vực nhúng·hệ thống·thời gian thực nhỏ và coi trọng hiệu năng·tính tất định thì hướng thủ tục (C) có lợi, còn hệ thống nghiệp vụ·dịch vụ lớn, thay đổi thường xuyên và cần cộng tác nhóm thì hướng đối tượng có lợi. Hai thứ không phải hàng thay thế mà là lựa chọn theo miền, và lựa chọn sai sẽ quay lại thành chi phí bảo trì.
Các mô thức cùng tồn tại·pha trộn. Khi ngôn ngữ đa mô thức đã trở thành chuẩn, đòi hỏi cảm quan thiết kế biết kết hợp điểm mạnh của từng mô thức (thủ tục=đơn giản·hiệu năng, đối tượng=cấu trúc·mở rộng, hàm=bất biến·đồng thời) theo từng bài toán, thay vì cố chấp một 'mô thức đáp án'.
Phải kết hợp với nguyên tắc thiết kế thì mới phát huy giá trị thực. Hướng đối tượng mà không có SOLID·mẫu thiết kế thì chỉ tăng độ phức tạp. Lợi ích tái sử dụng·bảo trì chỉ hiện thực hóa trên nền nguyên tắc phổ quát nâng độ gắn kết và hạ độ kết dính, và đây là hằng số của kỹ nghệ phần mềm vượt lên trên mô thức. [[module-cohesion-coupling]]
Chuyển đổi·triển vọng: tính đồng thời và dữ liệu lớn đang thay đổi địa hình mô thức. Trong môi trường đa lõi·phân tán, trạng thái khả biến dùng chung trở thành nguồn gốc lỗi đồng thời, nên tỷ trọng của yếu tố hàm đề cao tính bất biến đang tăng lên. Điều đòi hỏi ở nhà phát triển tương lai là vượt qua sự thành thạo một mô thức cụ thể, tiến tới năng lực đọc hiểu mô thức (paradigm literacy) — chuyển qua lại giữa các mô thức phù hợp tình huống. Kỹ sư chuyên nghiệp khi ra quyết định kiến trúc phải cân nhắc đồng thời hiệu năng·khả năng mở rộng·tính đồng thời·năng lực nhóm để kê đơn mô thức và ngôn ngữ.
Tài liệu tham khảo
- Wikipedia, "Object-oriented programming" — https://en.wikipedia.org/wiki/Object-oriented_programming
- Wikipedia, "Procedural programming" — https://en.wikipedia.org/wiki/Procedural_programming
- Robert C. Martin, "The Principles of OOD (SOLID)" — https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
Tóm tắt một câu: Hướng thủ tục lấy luồng tuần tự của hàm làm trung tâm nên hiệu quả cho lĩnh vực nhỏ·hiệu năng·hệ thống nhưng khi quy mô lớn thì thay đổi lan rộng khiến bảo trì khó khăn; hướng đối tượng đóng gói dữ liệu và hành vi thành đối tượng, dùng kế thừa·đa hình để cục bộ hóa·mở rộng thay đổi nên phù hợp hệ thống lớn·phức tạp; trong thực tế hiện đại, lợi ích chỉ hiện thực hóa khi kết hợp đa mô thức theo tính chất bài toán và gắn với SOLID·nguyên tắc thiết kế.