OOP - Có thật sự chỉ để mô hình hóa thế giới thực vào code?
Từ một bug nhỏ kéo theo cả hệ thống, bài viết nhìn lại encapsulation, inheritance và polymorphism như những công cụ để kiểm soát dependency trong OOP.
20 tháng 8, 2026 · 19 phút đọc

Phần 3 Road to Pure Solution Architect
Code bẩn là gì?
Một ngày đẹp trời cuối tháng 8, tiết trời dịu mát trong làn gió mong manh của 1 mùa thu đang về tới Đà Nẵng. Như thường lệ, anh chàng engineer bon bon trên con đường đến công ty cùng chiếc xe thân quen với nhiều suy nghĩ thần thờ về chuyến du lịch cuối tuần sắp tới. Gió nhẹ rít qua tấm kính nón bảo hiểm làm hắn mơ màng và mộng mị. Chiếc điện thoại bỗng rung lên với loạt tin nhắn làm hắn quay về với thực tại. "Fuck, lại task rồi, cmn!"
Văn phòng vẫn ồn như thường lệ. Những tiếng bàn tán rôm rả cùng những cuộc daily không hồi kết. Hắn vất chiếc cặp 1 cách nặng nề xuống chiếc ghế xoay có chức năng massage fake lỏ được quảng cáo 1 cách thần thánh mà hắn chưa từng tìm được cách sử dụng. Vừa thở hắt ra 1 cái thì PM đến và nói team vừa phát hiện một bug khá nhỏ. Nhìn qua requirement, nó chỉ là một thay đổi rất cục bộ trong một module. Những lời giải thích vụt qua tai nhưng hắn không để ý quá nhiều. Mắt hắn đang nhìn vào đoạn feature trên màn hình của PM và liếc qua những dòng code vô hồn tương ứng. Cảm giác quen thuộc nào đấy bỗng ùa về, hình như đoạn code này chính tay hắn đã đặt vào tuần trước. Hắn vẫn nhớ như in ngày hôm đó hắn vừa nhâm nhi cốc cafe tự pha ngon nhức nách, vừa prompt nhẹ nhàng vài câu để AI thực hiện cái feature loằng ngoằng đến đau mắt. Nhưng với kinh nghiệm lâu năm và kỹ năng sinh tồn được tích lũy quá nhiều, hắn phớt lờ cảm giác quen thuộc đấy và giả vờ như xa lạ :))
"5 ngày nhé bro!"
Tự tin với context mình đã nắm, hắn đinh ninh bug này chỉ mất 1 ngày để sửa. 4 ngày còn lại hắn có thể ngồi đọc báo, lướt bảng điện chứng khoán với màu đỏ rực đang ngày càng cắn xé những đồng lương ít ỏi của hắn. PM đồng ý với khuôn mặt xộc xệch miễn cưỡng. Hắn cười xuề, 1 món hời của thượng đế.
Ngày hôm sau hắn bắt đầu sửa đúng function gây bug. Thay đổi chỉ vài dòng, nhưng code không build vì một module khác phụ thuộc vào phần vừa sửa. Hắn chuyển sang module đó, chỉnh tiếp một chỗ. Sau khi chỉnh xong, một module khác lại phải thay đổi. Cứ như vậy, một bug nằm trong một function dần biến thành một chuỗi thay đổi chạy xuyên qua nhiều module mà về mặt business không thuộc phạm vi của bug ban đầu.
Hắn bắt đầu cảm thấy những cơn ớn lạnh chạy dọc sống lưng. Một bug sống còn của dự án, tưởng như có thể fix trong 1 ngày nhưng đã đến ngày thứ 3 và tất cả vẫn chỉ là 1 đống lộn xộn. Những giọt mồ hôi bắt đầu lấm tấm chảy dọc những nếp nhăn khắc khổ trên trán hắn. Ngày thứ năm, PM đến, vẫn với khuôn mặt méo xệch và bầu không khí ảm đạm. Tất nhiên là bug vẫn chưa xong. Hắn cười khắc khổ với khuôn mặt tội lỗi. "Ngày mai sẽ xong!" Deal thành công và tất nhiên hôm nay sẽ là 1 ngày OT sấp mặt, có thể là ON. Chuyến du lịch cuối tuần dường dư là quá xa xỉ với hắn bây giờ.
Đây là lúc một khái niệm rất kinh điển trong software design xuất hiện: Rigidity. Hệ thống trở nên cứng nhắc khi một thay đổi nhỏ không thể được thực hiện một cách cục bộ. Ta sửa một module nhưng bị buộc phải sửa thêm một loạt module khác chỉ để code giữa các module được consistent với nhau, hoặc ít nhất là phải build được và chạy.
Câu chuyện vẫn chưa kết thúc.
Hôm sau hắn cũng deploy được bug fix với nỗ lực của cả 1 đêm trắng mắt và mệt mỏi. Function chạy đúng. Tester say yes. Hắn thở hắt và lao về chiếc giường thân yêu, đánh 1 giấc ngon nhất cuộc đời. Sáng hôm sau, hắn lại đến công ty và làm công việc như thói quen hàng ngày, pha cốc cà phê, thêm ít đường để cuộc đời bớt đắng. Notification trên Teams bắt đầu hiện lên. Một team khác báo rằng thay đổi hôm qua đã làm hỏng function của họ. Một cảm giác đắng ngắt vụt qua đầu môi hắn, cốc cafe vẫn bốc những làn khói ẻo lả, còn hắn thì cảm thấy được vị đắng đã trôi xuống tới họng :)) Hắn không hề chủ động sửa business logic của họ, nhưng trong quá trình xử lý cascade dependency, hắn buộc phải chạm vào module đó. Unit test của phần hắn biết vẫn pass, nhưng một assumption business nằm sâu trong module của team khác đã bị phá vỡ.
Đó là Fragility. Hệ thống dễ vỡ khi một thay đổi ở nơi này có thể làm nơi khác hỏng theo một cách developer không dự đoán được.
PM lúc này không còn muốn để hắn fix nữa, tình huống đã trở nên urgent hơn. Anh ấy đưa ra một phương án tưởng như an toàn hơn: A, một developer ở team khác, đã có một function xử lý gần như chính xác business case này. Thay vì tiếp tục sửa code cũ, hãy lấy function của A sang dùng.
Thế là hắn mở module của A như 1 chiếc phao cứu sinh giữa cơn bão đang đánh dồn dập chuyến du lịch của hắn 1 cách tơi tả. Business logic đúng thật, nhưng function đó phụ thuộc vào một vector database mà module của hắn không dùng. Nó còn gọi một function nhỏ nằm trong một module rất lớn khác. Nếu muốn bê đoạn code này sang, hắn không chỉ bê business logic; hắn phải mang theo cả database, module lớn và những dependency nằm phía sau chúng.
Đó là Non-reusability. Code có logic ta cần nhưng không thể tái sử dụng vì nó bị dính quá chặt vào những detail không liên quan.
Cháy nhà ra mặt chuột! Có bug thì mới biết code bẩn! Ba triệu chứng xuất hiện từ cùng một bug:
Một thay đổi nhỏ
|
+--> phải sửa hàng loạt module -> Rigidity
|
+--> vô tình làm module khác hỏng -> Fragility
|
+--> không lấy được code tốt để reuse -> Non-reusability
Khi kể đến đây trong tech talk, tôi mới quay lại câu hỏi OOP là gì. Bởi nếu bắt đầu OOP bằng Dog, Cat hay Car, ta rất dễ bỏ lỡ vấn đề mà OOP có thể giúp giải quyết ở cấp độ kiến trúc.
Video gốc của talk: Are You Sure You Really Know OOP?
Bản chất của ba vấn đề không nằm ở số dòng code
Một function dài hai nghìn dòng rõ ràng khó đọc, duplication làm code khó maintain, naming kém khiến cognitive load tăng. Nhưng trong case vừa rồi, kể cả mỗi function đều ngắn và format rất đẹp, hệ thống vẫn có thể cứng nhắc, dễ đổ vỡ và khó reuse.
Bản chất nằm ở dependency.
Module của tôi không tồn tại độc lập. Nó phụ thuộc vào module khác, và các module khác cũng phụ thuộc ngược lại theo những cách không hề mong muốn. Dependency bản thân nó không phải vấn đề; một hệ thống thực tế luôn cần các thành phần cộng tác với nhau. Vấn đề là dependency có đi theo hướng mà ta mong muốn hay không.
Có thể thu nhỏ hệ thống trong câu chuyện thành ba tầng:
High-level Module
|
v
Middle-level Module
|
v
Low-level Module
Ở runtime, high-level module gọi xuống middle-level module, middle-level module tiếp tục gọi low-level module. Đây là flow of control bình thường.
Nếu source code cũng phụ thuộc theo đúng hướng đó:
High-level Module
|
| import
v
Middle-level Module
|
| import
v
Low-level Module
thì low-level module trở thành một dependency trực tiếp của toàn bộ chuỗi phía trên. Một thay đổi ở dưới có khả năng kéo theo rebuild, retest và thậm chí source changes ở những module phía trên. Trong một monolith nhỏ, đây có thể chỉ là vài giây compile. Trong một hệ thống lớn với nhiều module, team ownership và release cycle khác nhau, dependency này trở thành một vấn đề kiến trúc.
Bug nhỏ mất năm ngày không phải vì developer không biết sửa function. Phần khó nằm ở việc function đó nằm trong một mạng dependency mà change không còn là local.
OOP không phải là biến thế giới thực thành class
Một cách giải thích OOP rất phổ biến là “mô hình hóa thế giới thực vào code”: Car là một object, Dog kế thừa Animal, BankAccount có property và method. Cách này rất hữu ích khi giải thích syntax cho người mới, và nó cũng có một phần nguồn gốc lịch sử. Simula được Dahl và Nygaard phát triển cho simulation, nên việc biểu diễn các entity của một hệ thống bằng object thực sự nằm gần lịch sử ra đời của object-oriented programming.
Nhưng nếu coi đó là bản chất cuối cùng của OOP, ta dễ đi sai hướng.
Trong bug story ở trên, thứ chúng ta cần không phải một class diagram phản ánh thế giới thực chính xác hơn. Chúng ta cần các module có boundary rõ hơn để business policy không bị trói vào database, framework hay implementation detail.
Trong một codebase thực tế có rất nhiều abstraction không đại diện cho bất kỳ “vật thể ngoài đời” nào:
Repository
RetryPolicy
UnitOfWork
MessageDispatcher
CacheProvider
AuthorizationPolicy
SearchStore
Chúng tồn tại để software kiểm soát responsibility và dependency.
Vì vậy tôi nhìn OOP trước hết như một triết lý tổ chức software quanh object boundary và protocol, trong đó dynamic polymorphism cho phép chúng ta kiểm soát source-code dependency mà vẫn giữ runtime control flow cần thiết.
Ba khái niệm cổ điển — encapsulation, inheritance và polymorphism — trở nên thú vị hơn rất nhiều khi đặt vào đúng bài toán đó.
Encapsulation: nếu module chỉ lộ contract cần thiết, bug sẽ khó lan hơn
Quay lại bug ban đầu. Một lý do thay đổi dễ lan qua module boundary là caller biết quá nhiều về implementation bên trong module được gọi. Nó có thể biết concrete type, data structure, internal state hoặc một sequence operation mà đáng lẽ chỉ module đó được quyền quyết định.
Khi implementation detail trở thành knowledge của caller, mỗi detail có thể biến thành một dependency.
Encapsulation cố gắng cắt bớt loại dependency này. Caller chỉ nên biết contract cần thiết để cộng tác, còn representation và decision bên trong boundary được giữ kín.
Điều thú vị là OOP không phát minh ra encapsulation. C đã có thể làm representation hiding rất chặt trước khi private trở thành keyword quen thuộc.
Encapsulation rất chặt bằng .h và .c trong C
Giả sử low-level module trong bug story quản lý một Store. Client chỉ cần biết một số operation, không cần biết store dùng SQL, vector index hay một data structure nào khác.
Filename: store.h
#ifndef STORE_H
#define STORE_H
typedef struct Store Store;
Store* store_create(void);
void store_destroy(Store* store);
int store_save(Store* store, const char* key, const char* value);
const char* store_find(Store* store, const char* key);
#endif
Header chỉ công bố contract. Store ở đây là một incomplete type; client biết type tồn tại nhưng không biết representation.
Implementation nằm ở file khác:
Filename: store.c
#include "store.h"
#include <stdlib.h>
struct Store {
void* internal_index;
unsigned long version;
};
Store* store_create(void)
{
Store* store = malloc(sizeof(Store));
if (!store) {
return NULL;
}
store->internal_index = NULL;
store->version = 0;
return store;
}
int store_save(Store* store, const char* key, const char* value)
{
if (!store || !key || !value) {
return 0;
}
/* implementation detail */
store->version++;
return 1;
}
const char* store_find(Store* store, const char* key)
{
/* implementation detail */
return NULL;
}
void store_destroy(Store* store)
{
free(store);
}
Module sử dụng chỉ include file .h.
Filename: feature.c
#include "store.h"
void execute(Store* store)
{
store_save(store, "order-1", "completed");
// Không thể truy cập representation:
// store->internal_index
// store->version
// sizeof(Store)
}
Client không chỉ “không được phép” access field; nó không nhìn thấy declaration của những field đó. Ở mức source representation, đây là một dạng encapsulation rất chặt.
Các ngôn ngữ OOP làm việc này thuận tiện hơn:
public sealed class Store
{
private object internalIndex;
private long version;
public void Save(string key, string value)
{
// ...
}
}
Developer không phải tách thủ công contract và representation qua hai file theo cùng cách C thường làm. Compiler và object model hỗ trợ access control bằng public, private, protected.
Sự tiện lợi này đi kèm một khác biệt đáng chú ý. Trong C++ truyền thống, private members vẫn xuất hiện trong class declaration. C++ standard quy định access control không ngăn member tham gia name lookup; nó chỉ cấm code ở context không có quyền sử dụng member đó. Trong opaque type của C, client thậm chí không biết representation chứa những member nào.
Nếu dùng “perfect encapsulation” theo nghĩa representation hoàn toàn biến mất khỏi source view của client, cách .h/.c với opaque type trong C có thể chặt hơn một class declaration thông thường.
OOP không làm encapsulation mạnh hơn về mọi mặt. Nó làm encapsulation tiện hơn, gần với object model hơn và dễ sử dụng thường xuyên hơn.
Trong bug story, giá trị thực tế là: nếu middle-level module chỉ expose một contract nhỏ và giữ representation bên trong, thay đổi implementation có ít cơ hội ép caller sửa theo.
Encapsulation không loại bỏ dependency. Nó thu hẹp những gì caller được phép phụ thuộc vào.
Inheritance: reuse sai cách có thể tạo thêm đúng loại dependency đang làm ta đau đầu
Sau khi PM mất kiên nhẫn với bug, anh ấy yêu cầu lấy code của A sang reuse. Đây là thời điểm developer rất dễ nghĩ đến inheritance: nếu hai module có behavior giống nhau, tạo base class rồi kế thừa có vẻ là cách nhanh để dùng chung implementation.
Inheritance có thể đúng nếu giữa hai type thực sự tồn tại một quan hệ substitutable ổn định. Nhưng nếu mục tiêu chỉ là “lấy được code”, inheritance thường làm dependency sâu hơn thay vì giảm dependency.
Giả sử module của A và module của tôi cùng cần một behavior tìm kiếm:
BaseSearchModule
├── MyModule
└── A_Module
Nếu BaseSearchModule bắt đầu chứa vector database state, lifecycle, caching assumption và helper function phục vụ riêng module của A, module của tôi giờ không còn chỉ reuse một algorithm. Nó inherit cả một tập assumption.
Đây chính là loại coupling khiến non-reusability xuất hiện trong câu chuyện.
C++ multiple inheritance cho thấy inheritance không phải một quan hệ miễn phí
C++ cho phép một class kế thừa nhiều base class:
class Searchable {
public:
virtual void search() = 0;
};
class Cacheable {
public:
virtual void cache() = 0;
};
class FeatureModule : public Searchable, public Cacheable {
public:
void search() override { /* ... */ }
void cache() override { /* ... */ }
};
Multiple inheritance rất mạnh vì một object có thể nhận implementation hoặc contract từ nhiều hierarchy. Nhưng khi hai nhánh cùng có một base, complexity xuất hiện rõ ràng.
class Module {
public:
int version;
};
class SearchModule : public Module {};
class CacheModule : public Module {};
class FeatureModule : public SearchModule, public CacheModule {};
Ta có diamond:
Module
/ \
SearchModule CacheModule
\ /
FeatureModule
Nếu inheritance bình thường, một FeatureModule chứa hai Module subobject:
FeatureModule
├── SearchModule
│ └── Module
└── CacheModule
└── Module
feature.version trở nên ambiguous vì có hai version.
C++ có virtual inheritance để nói rằng hai nhánh phải chia sẻ cùng một base:
class SearchModule : virtual public Module {};
class CacheModule : virtual public Module {};
class FeatureModule : public SearchModule, public CacheModule {};
Most-derived object lúc này chỉ có một shared Module subobject.
C++ giải được bài toán, nhưng developer phải hiểu thêm object layout, construction order, ambiguity và virtual base semantics. Multiple inheritance không sai; nó chỉ cho thấy inheritance mang theo state, implementation và lifecycle dependency sâu hơn nhiều so với vẻ ngoài của một mũi tên trên class diagram.
Java và C# giữ một class hierarchy nhưng cho phép nhiều protocol
Java và C# chọn một hướng khác: một class chỉ có một direct base class, nhưng có thể implement nhiều interface.
Nếu áp vào case module của A, thay vì bắt module của tôi inherit implementation của module đó, cả hai có thể cùng thỏa một protocol:
public interface ISearchStore
{
Task<Result> Search(Query query);
}
public sealed class VectorSearchStore : ISearchStore
{
public Task<Result> Search(Query query)
{
// vector database implementation
}
}
public sealed class SqlSearchStore : ISearchStore
{
public Task<Result> Search(Query query)
{
// SQL implementation
}
}
Một class có thể implement nhiều capability:
public sealed class FeatureModule :
ISearchable,
ICacheable,
IAuditable
{
// ...
}
Interface truyền thống không mang instance state như concrete base class. Java interface fields là public static final; C# interface không chứa instance fields. Vì vậy multiple interface inheritance cho phép một object có nhiều contract mà không tạo hai bản state của cùng một base class như diamond inheritance.
Java và C# hiện đại đều hỗ trợ default behavior trong interface ở những mức độ nhất định, nên conflict về behavior vẫn có thể xảy ra. Nhưng conflict này được xử lý bằng rule compile-time thay vì tạo một graph instance state như multiple class inheritance.
Trade-off có thể tóm lại như sau:
Multiple class inheritance
= kế thừa contract + state + implementation relationship
Multiple interfaces
= nhận nhiều contract,
trong khi instance state vẫn thuộc về class
Trong câu chuyện bug, điều ta cần để reuse module của A không nhất thiết là inherit module đó. Ta cần tách capability mà mình muốn dùng khỏi những implementation detail mà mình không muốn kéo theo.
Đó là bước dẫn tự nhiên sang polymorphism.
Polymorphism là điểm mà hướng dependency bắt đầu thay đổi
Trong tech talk, phần quan trọng nhất không nằm ở việc một Dog và một Cat cùng override Speak(). Dynamic polymorphism hữu ích hơn nhiều khi nó cho phép runtime control flow đi một hướng trong khi source-code dependency đi hướng khác.
Quay lại module của A.
Hiện tại dependency có thể trông như sau:
My Feature
|
v
A Search Module
|
+--> Vector Database
|
+--> Large Shared Module
Nếu tôi gọi trực tiếp A_SearchModule, muốn reuse nó tôi phải chấp nhận những dependency mà module đó mang theo.
Ta có thể đặt protocol ở phía high-level code:
public interface ISearchStore
{
Task<Result> Search(Query query);
}
Business module chỉ biết abstraction:
public sealed class FeatureService
{
private readonly ISearchStore searchStore;
public FeatureService(ISearchStore searchStore)
{
this.searchStore = searchStore;
}
public Task<Result> Execute(Query query)
{
return searchStore.Search(query);
}
}
Vector implementation nằm ở phía detail:
public sealed class VectorSearchStore : ISearchStore
{
public Task<Result> Search(Query query)
{
// call vector database
}
}
Module của tôi có thể dùng một implementation khác:
public sealed class SqlSearchStore : ISearchStore
{
public Task<Result> Search(Query query)
{
// call SQL
}
}
Runtime control flow vẫn từ business xuống implementation:
FeatureService
|
| runtime call
v
SqlSearchStore
Nhưng source dependency được tổ chức:
FeatureService
|
v
ISearchStore
^
|
SqlSearchStore
Low-level implementation phụ thuộc vào protocol mà high-level module cần, thay vì high-level module import concrete implementation.
Đây là Dependency Inversion.
Dynamic polymorphism cho phép implementation được lựa chọn ở runtime. FeatureService không cần biết nó đang chạy với SqlSearchStore, VectorSearchStore, một in-memory test double hay một implementation chưa tồn tại ở thời điểm business module được viết.
Điểm quan trọng không phải là interface làm code đẹp hơn. Interface tạo ra một boundary nơi dependency đổi hướng.
Nếu boundary này có từ đầu, bug nhỏ sẽ khác thế nào?
Không có architecture nào đảm bảo mọi minor bug chỉ mất mười phút. Business logic có thể phức tạp, dữ liệu production có thể xấu, distributed system có thể tạo ra những failure mode rất khó tái hiện. OOP và Dependency Inversion không xóa complexity của software.
Nhưng chúng thay đổi phạm vi mà một thay đổi được phép lan.
Giả sử bug ban đầu nằm trong SqlSearchStore. Nếu high-level module chỉ phụ thuộc vào ISearchStore, một implementation detail bên trong SQL adapter có thể được sửa mà business module không phải thay đổi miễn là contract vẫn giữ nguyên.
Before
Business
|
v
Concrete Module
|
v
Database
Change Database Detail
|
+--> Business có thể phải sửa
+--> Module khác có thể phải sửa
+--> Reuse kéo theo database
After
Business
|
v
Protocol
^
|
Concrete Module
|
v
Database
Change Database Detail
|
+--> giữ bên trong concrete boundary
Rigidity giảm vì một low-level change không bắt buộc cascade lên high-level source code. Fragility giảm vì caller không dựa vào internal detail của implementation; contract nhỏ hơn cũng cho phép test tập trung hơn. Non-reusability giảm vì business capability phụ thuộc vào protocol, nên có thể thay adapter thay vì kéo theo toàn bộ technology stack của nơi code được viết ban đầu.
Tôi dùng từ “giảm” có chủ ý. Abstraction không tạo ra phép màu. Nếu ta thay semantics của contract, mọi implementation và caller có thể vẫn phải đổi. Nếu interface quá rộng, coupling chỉ chuyển từ concrete class sang một abstraction tệ. Nếu implementation vi phạm behavioral contract, runtime vẫn có thể break.
OOP tốt không làm dependency biến mất. Nó làm dependency có chủ đích hơn.
Từ câu chuyện đó, OOP có 1 định nghĩa cân bằng hơn
Nếu chỉ nói “OOP là để quản lý dependency”, định nghĩa này rất hữu ích cho architecture nhưng hơi hẹp. Nó bỏ qua những đặc điểm làm object model khác với một tập function pointer hay callback.
Một định nghĩa cân bằng hơn và phù hợp hơn nên là:
Object-oriented programming là cách tổ chức phần mềm thành các object có identity, state và behavior được encapsulate, cộng tác thông qua các protocol; dynamic polymorphism cho phép implementation được lựa chọn tại runtime mà caller chỉ phụ thuộc vào abstraction, từ đó hỗ trợ substitution và dependency inversion.
Đặt định nghĩa này trở lại bug story sẽ dễ hiểu hơn.
Identity
Mỗi module hoặc object runtime có thể là một receiver cụ thể với lifecycle và ownership riêng. Hai implementation cùng contract không vì thế mà là cùng một object.
State
Một object có thể sở hữu state cần thiết cho responsibility của nó. VectorSearchStore có thể giữ connection, index handle hoặc cache state của riêng nó; SqlSearchStore có state khác.
Behavior
State không tồn tại như một data bag để caller tùy ý sửa. Object cung cấp behavior mà nó chịu trách nhiệm thực hiện.
Encapsulation
Caller không cần biết internal index, connection strategy hay data layout. Những quyết định đó nằm sau boundary của object.
Protocol
FeatureService cộng tác qua ISearchStore. Nó biết capability mà nó cần, không biết concrete implementation.
Dynamic polymorphism
Runtime quyết định ISearchStore thực sự là VectorSearchStore, SqlSearchStore hay một implementation khác.
Substitution
Nếu các implementation tuân thủ cùng semantic contract, ta có thể thay một implementation bằng implementation khác mà high-level policy không phải biết concrete type.
Dependency inversion
Bởi high-level policy sở hữu nhu cầu về protocol và low-level detail implement protocol đó, source dependency không còn bị buộc phải đi cùng runtime control flow.
Các phần này nối với nhau thành một câu chuyện nhất quán hơn nhiều so với việc học riêng từng keyword.
Encapsulation, Inheritance và Polymorphism không có trọng lượng ngang nhau
Trong giáo trình nhập môn, ba khái niệm thường đứng cạnh nhau như ba feature tương đương. Khi nhìn từ bug story, vai trò của chúng khác nhau khá rõ.
Encapsulation giúp giảm số implementation decision bị lộ ra ngoài boundary. Nó giảm knowledge coupling.
Inheritance có thể xây type hierarchy và hỗ trợ substitution, nhưng cũng có thể tạo dependency mạnh lên base implementation. Vì vậy inheritance đáng được dùng thận trọng, đặc biệt khi mục tiêu thật sự chỉ là reuse code.
Polymorphism, đặc biệt dynamic polymorphism qua một stable protocol, cho phép tách runtime behavior khỏi compile-time dependency. Đây là khả năng dẫn trực tiếp tới plugin architecture, test seam, substitution và Dependency Inversion.
Có thể tóm tắt:
Encapsulation
-> caller biết ít hơn về implementation
Inheritance
-> tạo type relationship, nhưng cũng tạo base dependency
Polymorphism
-> caller gọi behavior mà không biết concrete receiver
Dependency Inversion
-> high-level policy không còn import low-level detail
Đó là lý do khi nhìn OOP ở cấp architecture, polymorphism thường mang ý nghĩa sâu hơn chuyện override method.
Interface cũng có thể trở thành một dependency tệ
Sau khi hiểu câu chuyện này, một phản xạ nguy hiểm khác là tạo interface cho mọi class.
IUserService
UserService
IOrderService
OrderService
IInvoiceService
InvoiceService
Nếu interface và implementation luôn thay đổi cùng nhau, nằm cùng module, không có alternate implementation, không tạo test seam hữu ích và không bảo vệ một volatility boundary nào, ta chỉ thêm indirection.
Trong bug story, ISearchStore có lý do tồn tại vì business capability và storage technology thay đổi vì những nguyên nhân khác nhau. Vector database, SQL database hoặc vendor có thể thay đổi mà use case vẫn giữ nguyên.
Một abstraction có giá trị khi nó đứng giữa hai phía có reason to change khác nhau.
Business policy
more stable
|
v
Protocol
^
|
Database / Framework / Vendor
more volatile
Đây cũng là cách tôi hiểu sâu hơn về Single Responsibility Principle. “Một class chỉ làm một việc” là cách nhớ rất đơn giản nhưng dễ gây hiểu nhầm. Responsibility hữu ích hơn khi được hiểu qua reason to change. Những code thay đổi vì cùng một actor hoặc cùng một business reason có xu hướng thuộc cùng boundary; những code thay đổi vì lý do khác nên được tách ra để không kéo nhau theo.
Và chúng ta lại quay về dependency.
OOP không độc quyền khả năng đảo dependency
Nếu chỉ nhìn vào mục tiêu dependency inversion, OOP không phải cách duy nhất để đạt được nó.
C có function pointer:
typedef int (*SearchFn)(const char* query);
int execute_feature(const char* query, SearchFn search)
{
return search(query);
}
Functional programming có higher-order function:
type Search = (query: Query) => Promise<Result>;
async function executeFeature(query: Query, search: Search) {
return search(query);
}
Trong cả hai trường hợp, high-level function nhận behavior từ bên ngoài thay vì hard-code concrete implementation.
Điều OOP làm thuận tiện là kết hợp identity, state, behavior, encapsulation, protocol và dynamic dispatch thành một object model mà compiler và runtime hỗ trợ trực tiếp. Developer không phải tự xây toàn bộ discipline này bằng function pointer, convention hay manual dispatch table.
Vì vậy tôi không xem Dependency Inversion là tài sản độc quyền của OOP. Tôi xem khả năng sử dụng dynamic polymorphism để kiểm soát dependency là một trong những giá trị kiến trúc mạnh nhất của OOP.
“Bốn trụ cột” là một điểm bắt đầu tốt, nhưng không nên là nơi dừng lại
Encapsulation, abstraction, inheritance và polymorphism vẫn là cách nhập môn dễ hiểu. Vấn đề chỉ xuất hiện khi ta trả lời được bốn từ này rồi cho rằng đã hiểu OOP.
Bug nhỏ mất năm ngày cho thấy một tầng khác.
Encapsulation
không chỉ là private
mà là giới hạn thứ caller được biết
Inheritance
không chỉ là "is-a"
mà là một dependency relationship có cost
Polymorphism
không chỉ là nhiều object cùng một method
mà là behavior được chọn ở runtime qua stable protocol
OOP
không chỉ là model noun thành class
mà là tổ chức object và dependency quanh những boundary có ý nghĩa
Nhìn như vậy, những khái niệm tưởng như rất cơ bản bắt đầu nối trực tiếp tới software architecture.
Tài liệu tham khảo
- Are You Sure You Really Know OOP? — Tech Talk
- Computer History Museum — Simula, 1965
- WG14 C specification material — incomplete structure types
- C++ Draft Standard — Member Access Control
- C++ Draft Standard — Multiple Base Classes
- Bjarne Stroustrup — C++ FAQ on Object-Oriented Programming
- Java Language Specification — Classes
- Java Language Specification — Interfaces
- C# Language Specification — Classes
- C# Language Specification — Interfaces
Bài viết này hữu ích với bạn?
Bình luận (0)
Chưa có bình luận nào được xuất bản.
Hãy là người đầu tiên chia sẻ góc nhìn của bạn.

Zi
With more than 11 years of experience as a software engineer, I specialize in consulting on and designing robust enterprise systems. I am passionate about programming and software development, and I have mastered industry best practices and developed innovative solutions that improve operational efficiency. As a consultant, I am committed to understanding each client's unique needs and goals and developing tailored strategies to address their specific challenges. I would welcome the opportunity to contribute my expertise as a knowledgeable and proactive partner in helping your enterprise thrive.
Solution Architect