Tính trừu tượng (Abstraction) là một trong 4 tính chất đặc trưng quan trọng của các ngôn ngữ lập trình hướng đối tượng (OOP – object-oriented programming). Mục tiêu chính của nó là làm giảm sự phức tạp bằng cách ẩn các chi tiết không liên quan trực tiếp tới người dùng (người dùng ở đây không phải người dùng cuối mà là lập trình viên). Điều đó cho phép người dùng vẫn thực hiện được các công việc cần thiết dựa trên một thực thể trừu tượng được cung cấp mà không cần hiểu hoặc thậm chí không nghĩ về tất cả sự phức tạp ẩn giấu bên trong.
Thực ra từ “trừ tượng” là một khái niệm chung được sử dụng cả trong cuộc sống chứ không phải là thuật ngữ chỉ giới hạn trong lĩnh vực lập trình.
Trừu tượng (Abstraction) trong đời sống thật
Tôi là một “con nghiện” cafe, mỗi buổi sáng đến công ty, sau khi ăn sáng xong là tôi phải làm 1 nháy cafe sữa đá. Để có cốc cafe đó, việc duy nhất tôi cần làm là đến quầy bán cafe và gọi “em ơi, cho a 1 cốc cafe như mọi hôm (nhiều sữa ít đá)“. Khoảng 2~3 phút sau là tôi đã có ngay một cốc cafe trên tay mà méo cần quan tâm nó được làm ra như thế nào, tôi chỉ cần biết tôi gọi 1 cốc cafe, sau đó trả tiền và lấy cafe, chấm hết.
Một ví dụ khác là khi chúng ta gửi một email cho một ai đó, sau khi viết mail xong chúng ta chỉ cần bấm nút “Gửi (Send)” là xong, cái gì thực sự diễn ra sau khi chúng ta gửi, dữ liệu được truyền như thế nào trên network cho đến khi đến được với người nhận thì chúng ta không quan tâm.
Đó là 2 ví dụ về “trừu tượng” trong đời sống thật.
Trừu tượng (Abstraction) trong lập trình hướng đối tượng (OOP)
Trong OOP thì Abstraction có thể chia thành 2 level (2 cảnh giới)
Cảnh giới thứ nhất: Dữ liệu (data) và một số hàm (methods) không cần thiết đưa ra bên ngoài sẽ được đưa vào trong class và chỉ định đặc tả truy cập là private (hoặc protected). Các data hoặc methods đó sẽ không thể truy cập từ bên ngoài của class đó. Ở cảnh giới này trừu tượng giúp cho code dễ hiểu hơn vì nó tập trung vào các tính năng / hành động cốt lõi và không tập trung vào các chi tiết nhỏ nhặt. Ngoài ra nó còn giúp chương trình dễ bảo trì, hạn chế lỗi do truy cập data bừa bãi, sai cách. Ở level này có thể coi Abstraction = Encapsulation + Data Hiding (tìm hiểu thêm tại đây)
Cảnh giới thứ hai: Ở cảnh giới này chúng ta thực hiện trừu tượng hoá từ giai đoạn design cho đến coding. Ở giai đoạn design – thiết kế chúng ta sẽ tập trung vào việc đưa ra “what” – cái mà một module hoặc 1 class sẽ làm chứ không tập trung vào “how” – các việc đó được thực hiện như thế nào. Kết quả của bước thiết kế này sẽ là một cái gọi là interface. Các class (hoặc module) sẽ làm việc với nhau thông qua interface chứ không cần biết cụ thể về nhau.
Một interface là một bản mô tả hành vi hoặc khả năng của một class mà không đưa ra cách thực hiện cụ thể của class đó như thế nào.
Trong C++ thì interface là một class mà chỉ chứa khai báo của hàm huỷ ảo (virtual destructor) và các hàm thuần ảo (poor virtual methods) khác. Ở giai đoạn coding thì các interface sẽ được xây dựng cụ thể bằng các class cụ thể khác, các class này bắt buộc phải kết thừa interface class và định nghĩa cụ thể các hàm thuần ảo đã được khai báo trong interface đó. Bằng cách này thì các module (hoặc class) sẽ không phụ thuộc vào nhau mà chỉ phụ thuộc vào interface, việc sửa code của các module (hoặc class) sẽ không kéo theo việc phải sửa code ở các module (hoặc class) khác.
Ví dụ sơ sơ, đơn giản về interface như sau (chú ý, code chỉ dùng để minh hoạ, muốn chạy được cần code thêm) →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
class IShape // class này đóng vai trò là interface vì tất cả các hàm của nó đều là hàm thuần ảo { public: virtual ~IShape(); virtual void move_x(int x) = 0; virtual void move_y(int y) = 0; virtual void draw() = 0; }; class Line : public IShape { public: virtual ~Line(); virtual void move_x(int x); // class Line sẽ phải có code cụ thể của hàm move_x virtual void move_y(int y); // class Line sẽ phải có code cụ thể của hàm move_y virtual void draw(); // class Line sẽ phải có code cụ thể của hàm draw private: point end_point_1, end_point_2; //... }; int main (void) { IShape* shape = new Line(); // Gọi một số hàm setup cho shape //... // Vẽ shape shape->move_x(10); shape->move_y(20); shape->draw(); //... } |
Trong đoạn code trên thì tôi có comment một số chỗ bằng tiếng Việt để các bạn dễ hiểu nhưng anh em tuyệt đối đừng làm như thế, hãy comment bằng tiếng Anh. Bây giờ tôi sẽ giải thích qua một chút về code ví dụ này. Trong ví dụ trên thì IShape được gọi là interface còn Line là class implement cụ thể interface đó. IShape chỉ đưa ra danh sách các hàm mà một class kế thừa nó cần phải định nghĩa, bản thân class IShape không định nghĩa các hàm đó. Line 28~30 trong hàm main thực hiện vẽ đối tượng trỏ bởi con trỏ shape mà không quan tâm đến đối tượng cụ thể trong đó là đối tượng của class nào, nó chỉ làm việc đó thông qua interface IShape mà không cần biết đến sự tồn tại của Line. Lợi ích của mô hình thiết kế kiểu này là:
- Giả sử chúng ta muốn thay thế Line bằng Square thì chúng ta cần tạo ra class Square kế thừa IShape, sau đó định nghĩa các hàm move_x(), move_y(), draw() tương ứng. Trong hàm main() thì chỉ sửa lại đoạn code mô phỏng từ line 24 đến 26, line 28~30 không cần sửa lại →
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
class IShape // class này đóng vai trò là interface vì tất cả các hàm của nó đều là hàm thuần ảo { public: virtual ~IShape(); virtual void move_x(int x) = 0; virtual void move_y(int y) = 0; virtual void draw() = 0; }; class Line : public IShape { public: virtual ~Line(); virtual void move_x(int x); // class Line sẽ phải có code cụ thể của hàm move_x virtual void move_y(int y); // class Line sẽ phải có code cụ thể của hàm move_y virtual void draw(); // class Line sẽ phải có code cụ thể của hàm draw private: point end_point_1, end_point_2; //... }; class Square : public IShape { public: virtual ~Square(); virtual void move_x(int x); // class Square sẽ phải có code cụ thể của hàm move_x virtual void move_y(int y); // class Square sẽ phải có code cụ thể của hàm move_y virtual void draw(); // class Square sẽ phải có code cụ thể của hàm draw private: point end_point_1; point end_point_2; point end_point_3; point end_point_4; //... }; int main (void) { IShape* shape = new Square(); // Gọi một số hàm setup cho shape //... // Vẽ shape shape->move_x(10); shape->move_y(20); shape->draw(); //... } |
- Nếu chương trình được mở rộng ra và có những phần code chỉ phục thuộc vào interface IShape mà không phụ thuộc vào định nghĩa cụ thể như Line, Square thì khi thay đổi code của Line, Square sẽ không cần compile lại các phần code không liên quan.
Nếu anh em nào mới tiếp cận C++ cũng như lập trình hướng đối tượng thì đọc về cảnh giới 2 này sẽ khá khó hiểu. Để hiểu được thì cần phải học qua các bài về lớp trừu tượng (Abstract Class) và tìm hiểu về design pattern như Abstract Factory. Nói chung anh em mới tiếp cận thì cố gắng hiểu Abstraction ở level 1 và ghi nhớ rằng có Abstraction ở level 2 là được rồi. Nói ra thì khá xấu hổ vì lúc mới học và làm C++ mình cũng không hiểu Abstraction nó là cái khỉ gì, trong quá trình làm dần dần mới ngộ ra.
Tham khảo
- [1] : https://crmbusiness.wordpress.com/2015/08/12/why-understanding-abstractions-helps-you-write-better-code/
- [2] : https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Interface_Class
Xem thêm
— Phạm Minh Tuấn (Shun) —