Nội dung chính
Lập trình hướng đối tượng (OOP) là một trong những kỹ năng cốt lõi mà các Software Developer cần phải nắm vững. Để đánh giá kỹ năng OOP của ứng viên, các nhà tuyển dụng thường đưa ra những câu hỏi phỏng vấn rất đa dạng. Hãy cùng ITviec khám phá 50 câu hỏi phỏng vấn OOP thường gặp nhất hiện nay cùng câu trả lời chuẩn xác. Từ những khái niệm cơ bản đến những vấn đề nâng cao, chúng tôi sẽ giúp bạn nắm vững kiến thức OOP và thành công trong các buổi phỏng vấn. Phần 1 trong bài viết này sẽ là những câu hỏi phỏng vấn OOP cấp độ đầu vào.
Trong Phần 1, hãy cùng tìm hiểu về:
- OOP là gì
- 25 câu hỏi phỏng vấn OOP cấp độ đầu vào (Fresher/ Junior Developer) kèm câu trả lời chi tiết
Nếu bạn đang chuẩn bị cho các vị trí Middle/ Senior Developer hoặc tìm kiếm những câu hỏi phỏng vấn OOP thực hành, bạn có thể tham khảo Phần 2 của Các câu hỏi phỏng vấn OOP.
OOP là gì?
Object-oriented programming (OOP) hay còn gọi lập trình hướng đối tượng là một mô hình lập trình máy tính tổ chức thiết kế phần mềm xung quanh dữ liệu (data) hoặc đối tượng (object), thay vì các hàm và logic. Đối tượng có thể được định nghĩa là một trường dữ liệu có các thuộc tính và hành vi duy nhất.
OOP tập trung vào các object mà Developer muốn thao tác thay vì logic cần thiết để thao tác chúng. Cách tiếp cận lập trình này rất phù hợp với phần mềm lớn, phức tạp và được cập nhật hoặc bảo trì tích cực. Điều này bao gồm các chương trình sản xuất và thiết kế, cũng như các ứng dụng di động.
Ví dụ, OOP có thể được sử dụng cho phần mềm mô phỏng hệ thống sản xuất. Phương pháp này có lợi cho phát triển cộng tác, trong đó các dự án được chia thành các nhóm. Các lợi ích bổ sung của OOP bao gồm khả năng tái sử dụng mã, khả năng mở rộng và hiệu quả.
Đọc thêm: OOP là gì? 4 đặc tính cơ bản của OOP
Các câu hỏi phỏng vấn OOP cấp độ Fresher
OOP là gì?
OOP là phương pháp lập trình sử dụng các class và object để tổ chức các chương trình phần mềm thành các mẫu code có thể tái sử dụng.
Các ngôn ngữ như Java, C++, Python và JavaScript hỗ trợ OOP bằng cách tạo điều kiện thuận lợi cho việc tạo các class chỉ định các thuộc tính và phương thức, trong khi các object là thể hiện của các class này. Các nguyên tắc OOP bao gồm kế thừa, đóng gói, đa hình và trừu tượng hóa, tạo điều kiện thuận lợi cho việc thể hiện các thực thể trong thế giới thực của thiết kế phần mềm.
Liệt kê các ngôn ngữ lập trình hướng đối tượng chính
Các ngôn ngữ lập trình sử dụng và tuân theo mô hình OOP được gọi là ngôn ngữ lập trình hướng đối tượng. Một số ngôn ngữ lập trình hướng đối tượng chính bao gồm:
- Java
- C++
- Javascript
- Python
- PHP
Tại sao cần OOP?
OOP cần sử dụng bởi các lợi ích:
- Tính mô-đun: OOP thúc đẩy thiết kế mô-đun, giúp chia các vấn đề phức tạp thành các phần nhỏ hơn, dễ quản lý hơn.
- Khả năng tái sử dụng: Với OOP, bạn có thể sử dụng các class và object nhiều lần ở các phần khác nhau của chương trình hoặc các chương trình khác, giúp tiết kiệm thời gian và làm cho chương trình hiệu quả hơn.
- Đóng gói: Đóng gói ẩn đi hoạt động bên trong của một object và chỉ hiển thị những gì cần thiết, giúp mọi thứ được an toàn và ngăn ngừa những thay đổi vô tình.
- Kế thừa: OOP cho phép bạn tạo các class mới dựa trên các class hiện có, giúp tái sử dụng code và xây dựng các mối quan hệ có tổ chức giữa các class.
- Đa hình: OOP cho phép các object khác nhau được coi như thuộc cùng một nhóm, giúp phần mềm linh hoạt hơn và dễ mở rộng hơn.
- Trừu tượng hóa: OOP đơn giản hóa các đối tượng trong thế giới thực thành các mô hình dễ hiểu hơn, tập trung vào những yếu tố cần thiết và ẩn đi các chi tiết không cần thiết.
Liệt kê lợi thế của việc sử dụng OOP
- OOP rất hữu ích trong việc giải quyết những vấn đề phức tạp.
- Các chương trình cực kỳ phức tạp có thể được tạo, xử lý và bảo trì dễ dàng bằng cách sử dụng lập trình hướng đối tượng.
- OOP thúc đẩy việc tái sử dụng mã, do đó giảm thiểu sự trùng lặp.
- OOP cũng giúp ẩn đi những chi tiết không cần thiết với sự trợ giúp của Trừu tượng hóa dữ liệu.
- Đa hình mang lại nhiều tính linh hoạt trong OOP.
Tại sao OOP lại phổ biến?
Mô hình lập trình OOP được xem là một phong cách lập trình tốt hơn. OOP không chỉ giúp viết một đoạn mã phức tạp trở nên đơn giản mà còn cho phép người dùng xử lý và bảo trì chúng một cách dễ dàng. Không chỉ vậy, 4 tính năng chính của OOP – Trừu tượng hóa dữ liệu, Đóng gói, Kế thừa và Đa hình, giúp các lập trình viên dễ dàng giải quyết các tình huống phức tạp. Theo thời gian, OOP trở nên phổ biến hơn.
Các đặc điểm cơ bản của OOP là gì?
Các tính năng cơ bản của OOP là:
- Tính đóng gói (Encapsulation)
- Tính kế thừa (Inheritance)
- Tính đa hình (Polymorphism)
- Tính trừu tượng (Abstraction).
Giải thích khái niệm tính kế thừa bằng ví dụ thực tế
Parent class là một khái niệm logic, chẳng hạn như vehicle là base class xác định các thuộc tính chung được chia sẻ bởi tất cả các vehicle. Tuy nhiên, các subclass là một loại class cụ thể hơn như xe tải, xe buýt, ô tô… Kế thừa cho phép các subclass kế thừa các thuộc tính chung của một vehicle và xác định các thuộc tính và phương thức cụ thể cho riêng chúng.
Ví dụ bằng code Java:
// Lớp cha Vehicle (Phương tiện) class Vehicle { String color; int numberOfWheels; void move() { System.out.println("Phương tiện đang di chuyển"); } } // Lớp con Car (Ô tô) kế thừa từ Vehicle class Car extends Vehicle { int numberOfDoors; void honk() { System.out.println("Tút tút"); } } // Lớp con Truck (Xe tải) kế thừa từ Vehicle class Truck extends Vehicle { int loadCapacity; // Khả năng tải }
Các loại Inheritance (kế thừa) trong OOP
- Single Inheritance (kế thừa đơn): Subclass được kế thừa trực tiếp từ base class.
- Multiple Inheritance (kế thừa đa lớp): Một subclass kế thừa từ nhiều base class.
- Multi-level Inheritance (kế thừa đa cấp): Subclass được kế thừa từ parent class, parent class lại được kế thừa từ một class khác.
- Multi-path Inheritance (kế thừa đa đường dẫn): Một subclass kế thừa từ hai hoặc nhiều lớp có một base class chuẩn.
- Hierarchical Inheritance (kế thừa phân cấp): Nhiều class kế thừa được tạo ra từ một base class duy nhất.
- Hybrid Inheritance (di truyền lai): Sự kết hợp của hai hoặc nhiều loại kế thừa.
Encapsulation (tính đóng gói) là gì?
Tính đóng gói là sự liên kết dữ liệu và các phương pháp xử lý chúng thành một đơn vị duy nhất sao cho dữ liệu nhạy cảm được ẩn khỏi người dùng. Tính đóng gói được triển khai theo các quy trình:
- Ẩn dữ liệu: Một tính năng của ngôn ngữ để hạn chế truy cập vào các thành phần của một đối tượng. Ví dụ, các thành phần private và protected trong C++.
- Đóng gói dữ liệu và phương thức: Dữ liệu và các phương thức hoạt động trên dữ liệu đó được gói gọn lại với nhau. Ví dụ, các thành phần dữ liệu và các phương thức thành phần hoạt động trên chúng được đóng gói thành một đơn vị duy nhất được gọi là class.
Polymorphism (tính đa hình) là gì? Có những loại Polymorphism nào?
Đa hình là một trong những khái niệm cốt lõi và được sử dụng nhiều nhất trong ngôn ngữ OOP. Tính đa hình giải thích khái niệm các class khác nhau có thể được sử dụng với cùng một giao diện. Mỗi class này cũng có thể có triển khai giao diện riêng. Đa hình được phân loại thành hai loại dựa trên thời gian khi lệnh gọi đến đối tượng hoặc hàm được giải quyết:
- Compile-Time Polymorphism (đa hình thời gian biên dịch): Đa hình thời gian biên dịch, còn được gọi là static polymorphism hoặc early binding là loại đa hình trong đó liên kết lệnh đến mã của nó được thực hiện tại thời điểm biên dịch. Method overloading và operator overloading là ví dụ về đa hình thời gian biên dịch.
- Runtime Polymorphism (đa hình thời gian chạy): Còn được gọi là dynamic polymorphism hoặc late binding, là loại đa hình trong đó việc triển khai thực tế của hàm được xác định trong thời gian chạy hoặc thực thi. Method overriding là một ví dụ về phương thức này.
Abstraction (tính trừu tượng) là gì? Việc trừu tượng hóa dữ liệu được thực hiện như thế nào?
Nếu bạn là người dùng và bạn có một problem statement, bạn không muốn biết các thành phần của phần mềm hoạt động như thế nào hoặc nó được tạo ra như thế nào. Bạn chỉ muốn biết phần mềm giải quyết vấn đề của bạn như thế nào. Lúc này, trừu tượng hóa là phương pháp ẩn các chi tiết không cần thiết khỏi các chi tiết cần thiết.
Đây là một trong những tính năng chính của OOP. Trừu tượng hóa được triển khai bằng cách sử dụng các class và giao diện.
Ngoài OOP, còn có những mô hình lập trình nào khác?
Các mô hình lập trình đề cập đến phương pháp phân loại ngôn ngữ lập trình dựa trên các tính năng của chúng. Về cơ bản có hai loại mô hình lập trình:
Mô hình lập trình mệnh lệnh: Lập trình mệnh lệnh tập trung vào cách thực thi logic chương trình và định nghĩa control flow là các câu lệnh thay đổi trạng thái chương trình. Có thể được phân loại thêm thành:
- Mô hình lập trình thủ tục: Lập trình thủ tục chỉ định các bước mà chương trình phải thực hiện để đạt đến trạng thái mong muốn, thường được đọc theo thứ tự từ trên xuống dưới.
- Lập trình hướng đối tượng hoặc OOP: OOP tổ chức các chương trình dưới dạng các object, chứa một số dữ liệu và hành vi.
- Lập trình song song: Mô hình lập trình song song chia một tác vụ thành các tác vụ con và tập trung vào việc thực hiện chúng đồng thời tại cùng một thời điểm.
Mô hình lập trình khai báo: Lập trình khai báo tập trung vào những gì cần thực thi và định nghĩa logic chương trình, nhưng không phải là control flow chi tiết. Mô hình khai báo có thể được phân loại thành:
- Mô hình lập trình logic: Mô hình lập trình logic dựa trên logic hình thức, đề cập đến một tập hợp các câu thể hiện sự kiện và quy tắc giải quyết một vấn đề
- Mô hình lập trình hàm: Lập trình hàm (Functional Programming) là một mô hình lập trình trong đó các chương trình được xây dựng bằng cách áp dụng và biên soạn các hàm.
- Mô hình lập trình cơ sở dữ liệu: Được sử dụng để quản lý dữ liệu và thông tin được cấu trúc dưới dạng trường, bản ghi và tệp.
Các câu hỏi phỏng vấn OOP cấp độ Junior Developer
Sự khác biệt giữa lập trình thủ tục (SOP) và lập trình hướng đối tượng (OOP)?
Sự khác biệt quan trọng giữa Lập trình hướng đối tượng (OOP) và Lập trình thủ tục (còn gọi là Lập trình có cấu trúc – Structured Programming) nằm ở cách tiếp cận của chúng đối với việc tổ chức và thiết kế chương trình:
Lập trình hướng đối tượng (OOP) | Lập trình thủ tục (Structured Programming) |
Lập trình hướng đối tượng được xây dựng trên các đối tượng có trạng thái và hành vi. | Cấu trúc logic của chương trình được cung cấp bởi lập trình cấu trúc, chia chương trình thành các chức năng tương ứng. |
Chương trình được tổ chức thành các lớp, trong đó mỗi lớp chứa dữ liệu (thuộc tính) và các phương thức (hành vi) để thao tác trên dữ liệu đó và có thể tương tác với nhau thông qua các phương thức. | Chương trình được chia thành các hàm và thủ tục. Dữ liệu và hàm thường được tổ chức riêng rẽ. Dữ liệu được truyền vào hàm thông qua các tham số, và các hàm có thể sửa đổi dữ liệu toàn cục. |
Hạn chế luồng dữ liệu mở chỉ dành cho những bộ phận được ủy quyền, qua đó cung cấp khả năng bảo mật dữ liệu tốt hơn. | Không hạn chế luồng dữ liệu. Bất kỳ ai cũng có thể truy cập dữ liệu. |
Khả năng tái sử dụng mã được cải thiện nhờ vào các khái niệm đa hình và kế thừa. | Khả năng tái sử dụng mã đạt được bằng cách sử dụng các hàm và vòng lặp. |
Các phương thức hoạt động theo kiểu động, thực hiện các lệnh gọi dựa trên hành vi của đối tượng và nhu cầu của mã khi chạy. | Các hàm được gọi tuần tự và các dòng mã được xử lý từng bước. |
Việc sửa đổi và cập nhật mã dễ dàng hơn. | Việc sửa đổi mã khó hơn so với OOP. |
Dữ liệu được coi trọng hơn trong OOP. | Mã được coi trọng hơn. |
Lập trình có cấu trúc là gì?
Lập trình có cấu trúc đề cập đến phương pháp lập trình bao gồm control flow có cấu trúc hoàn chỉnh. Ở đây, cấu trúc đề cập đến một khối, chứa một tập hợp các quy tắc và có control flow xác định, chẳng hạn như (if/then/else), (while/for), cấu trúc khối và chương trình con. Hầu như tất cả các mô hình lập trình đều bao gồm lập trình có cấu trúc, bao gồm cả mô hình OOP.
Trừu tượng hóa dữ liệu là gì?
Trừu tượng hóa dữ liệu thực sự là một tính năng quan trọng của lập trình hướng đối tượng. Nó bao gồm việc chỉ hiển thị thông tin cần thiết trong khi ẩn các chi tiết triển khai. Ví dụ, khi sử dụng điện thoại di động, bạn biết cách nhắn tin hoặc gọi điện cho ai đó, nhưng bạn không cần biết các quy trình kỹ thuật phức tạp đằng sau nó.
Điều này minh họa cho việc trừu tượng hóa dữ liệu vì nó bảo vệ người dùng khỏi những phức tạp không cần thiết, tập trung vào các hành động họ có thể thực hiện mà không gây thêm gánh nặng về các chi tiết triển khai. Có thể thực hiện trừu tượng hóa dữ liệu bằng hai cách:
- Abstraction class (lớp trừu tượng)
- Abstraction method (phương thức trừu tượng)
Phân biệt giữa trừu tượng hóa dữ liệu và đóng gói dữ liệu
Trừu tượng hóa dữ liệu (data abstraction) | Đóng gói dữ liệu (encapsulation) |
Giải quyết vấn đề ở cấp độ thiết kế | Giải quyết vấn đề ở cấp độ triển khai |
Giúp ẩn các chi tiết thực hiện | Gom mã và dữ liệu lại với nhau thành một đơn vị duy nhất và giúp ẩn đi |
Virtual functions là gì?
Virtual function (hàm ảo) được khai báo trong một base class mà các derived class có thể overriding. Chúng cho phép đa hình động, cho phép lựa chọn triển khai hàm thích hợp tại runtime dựa trên loại đối tượng. Trong các ngôn ngữ như C++, các hàm ảo được sử dụng để đạt được late binding hoặc đa hình runtime.
Constructor là gì? Các loại constructor
Một Constructor là một block code khởi tạo đối tượng mới. Constructor là các phương thức duy nhất trong một class được tự động gọi khi một instance của class được tạo. Chúng được sử dụng để khởi tạo trạng thái của đối tượng hoặc thực hiện bất kỳ hoạt động thiết lập cần thiết nào. Constructor thường là phương thức có cùng tên với class nhưng cũng có thể khác trong một số ngôn ngữ. Ví dụ:
- Trong Python, hàm tạo được đặt tên là __init__.
- Trong C++ và Java, hàm tạo được đặt tên giống như tên class.
Ví dụ:
Trong C++:
class base { public: base() { cout << "Đây là constructor"; } }
Trong Java:
class base { base() { System.out.printIn("Đây là constructor"); } }
Trong Python:
class base: def __init__(self): print("Đây là constructor")
Có các loại Constructor sau:
- Private Constructor (hàm riêng tư): Điều này hạn chế việc khởi tạo một class từ bên ngoài. Nó thường được sử dụng cho các class tiện ích có phương thức tĩnh.
- Default Constructor (hàm mặc định): Nếu không có hàm tạo nào được định nghĩa rõ ràng, nó sẽ được trình biên dịch tự động tạo ra và khởi tạo các biến thể hiện thành giá trị mặc định.
- Copy Constructor (hàm sao chép): Hàm này tạo ra một đối tượng mới bằng cách sao chép trạng thái của một đối tượng hiện có. Nó được sử dụng để tạo bản sao.
- Static Constructor (hàm tĩnh): Khởi tạo các thành phần dữ liệu tĩnh hoặc thực hiện các tác vụ thiết lập một lần cho class, chỉ được thực thi một lần khi class được tải.
- Parameterized Constructor (hàm tham số): Chấp nhận tham số để khởi tạo các biến thành phần trong quá trình tạo đối tượng, cho phép tùy chỉnh trạng thái của đối tượng.
Trình Copy constructor là gì?
Copy Constructor là một loại Constructor, có mục đích sao chép một đối tượng sang một đối tượng khác. Điều đó có nghĩa là một Copy constructor sẽ sao chép một đối tượng và các giá trị của nó vào một đối tượng khác, với điều kiện là cả hai đối tượng đều thuộc cùng một class.
Destructor là gì?
Ngược lại với Constructor, khởi tạo đối tượng và chỉ định không gian cho chúng, Destructor (hàm hủy) cũng là phương thức đặc biệt. Nhưng Destructor giải phóng tài nguyên và bộ nhớ mà đối tượng chiếm giữ. Destructor là phương thức được gọi tự động khi đối tượng hết phạm vi hoặc bị hủy. Trong C++, tên Destructor cũng giống như tên class nhưng có ký hiệu dấu ngã ( ~ ) làm tiền tố. Trong Python, Destructor được đặt tên là __del__ .
Ví dụ:
Trong C++:
class base { public: ~base() { cout << "Đây là destructor"; } }
Trong Python:
class base: def __del__(self): print("Đây là destructor")
Trong Java, trình thu gom rác tự động xóa các đối tượng không hữu ích nên không có khái niệm về Destructor trong Java. Chúng ta có thể sử dụng phương thức finalize() như một giải pháp thay thế cho Destructor của Java nhưng nó cũng đã lỗi thời kể từ Java 9.
Superclass là gì?
Superclass (hay còn gọi Base class / parent class) cũng là một phần của Inheritance. Superclass là một class, cho phép các subclass kế thừa từ superclass. Ví dụ, nếu Student là class bắt nguồn từ lớp Person, thì Person sẽ được gọi là superclass.
Subclass là gì?
Subclass là một class kế thừa các thuộc tính và hành vi từ một class khác, được gọi là superclass hoặc parent class của nó. Subclass mở rộng hoặc chuyên biệt hóa chức năng superclass của nó. Ví dụ:
class Vehicle: def __init__(self, brand): self.brand = brand def drive(self): print("Lái xe", self.brand) class Car(Vehicle): def __init__(self, brand, model): super().__init__(brand) self.model = model def honk(self): print("Tiếng kèn của xe", self.brand, self.model) # Tạo một instance của class Car my_car = Car("Toyota", "Camry") my_car.drive() # Output: Lái xe Toyota my_car.honk() # Output: Tiếng kèn của xe Toyota Camry
Trong ví dụ này, ‘Car’ là subclass của ‘Vehicle’. Nó kế thừa phương thức ‘drive()’ từ Vehicle và thêm phương thức ‘honk()’ của class này. Các thể hiện của Car có thể truy cập vào cả phương thức của superclass và subclass.
Method overloading là gì?
Method overloading (nạp chồng hàm) là một tính năng trong OOP cho phép một class có nhiều phương thức có cùng tên nhưng khác nhau về tham số hoặc kiểu tham số. Điều này cho phép các Developer xác định nhiều phiên bản khác nhau của một kỹ thuật, mỗi phiên bản được thiết kế riêng để chấp nhận các loại hoặc số lượng tham số khác nhau. Khi một phương thức được gọi, phiên bản thích hợp sẽ được chọn dựa trên các tham số được cung cấp.
Ví dụ, hãy xem xét Calculator class có phương thức add():
Ví dụ với Java:
public class Calculator { public int add(int num1, int num2) { return num1 + num2; } public double add(double num1, double num2) { return num1 + num2; } }
Trong ví dụ này, phương thức add()được nạp chồng để chấp nhận các tham số là số nguyên và số thực. Tùy thuộc vào việc giá trị số nguyên hay số đôi được truyền làm đối số, phiên bản tương ứng của phương thức add() sẽ được gọi. Điều này mang lại sự linh hoạt và tiện lợi khi làm việc với các loại dữ liệu khác nhau.
Các trình sửa đổi quyền truy cập là gì?
Access modifier hay còn gọi là trình sửa đổi truy cập là các từ khóa được sử dụng trong ngôn ngữ lập trình hướng đối tượng để kiểm soát khả năng hiển thị và khả năng truy cập của các class, phương thức và các thành phần khác trong một chương trình. Access modifier xác định cách các thành phần này có thể được truy cập từ các phần khác nhau của chương trình hoặc bên ngoài các mô-đun. Các access modifiers chính bao gồm:
- Public: Cho phép các thành viên có thể được truy cập từ mọi vị trí của chương trình.
- Private: Chỉ cho phép truy cập trong phạm vi class hiện tại., ngăn chặn quyền truy cập từ bên ngoài.
- Protected: Cấp quyền truy cập cho các thành phần trong cùng một class và các subclass của nó, nhưng không cấp quyền truy cập từ bên ngoài.
- Internal: Giới hạn quyền truy cập trong cùng một assembly hoặc mô-đun.
- Protected Internal: Kết hợp giữa protected và internal, cho phép truy cập trong cùng một assembly hoặc từ các subclass, bất kể có cùng assembly hay không.
Các access modifier là một công cụ mạnh mẽ trong OOP, giúp các lập trình viên kiểm soát quyền truy cập vào các thành phần của chương trình, bảo vệ dữ liệu và tăng tính bảo mật, đồng thời giúp tạo ra code rõ ràng, dễ bảo trì và mở rộng.
Sự khác biệt giữa overloading và overriding như thế nào?
Overloading là hai hoặc nhiều phương thức có cùng tên nhưng tham số khác nhau. Nó được giải quyết trong compile-time. Trong khi đó, Overriding là một khái niệm OOP cho phép các subclass có một triển khai cụ thể của một phương thức đã được parent class của nó cung cấp. Overriding được giải quyết trong runtime, được triển khai với sự trợ giúp của các Virtual function (hàm ảo).
Tổng kết Câu hỏi phỏng vấn OOP (Phần 1)
Nắm vững 25 câu hỏi phỏng vấn OOP trên đồng nghĩa bạn nắm chắc kỹ năng cơ bản về OOP, tự tin chinh phục các vị trí đầu vào và cần ít kinh nghiệm. Để có cơ hội sở hữu vị trí cấp cao về OOP, bạn cần trang bị cho mình nhiều kiến thức chuyên ngành hơn nữa và kết hợp với kinh nghiệm thực tế nhiều năm.
Ở Phần 2, ITviec sẽ chia sẻ đến bạn các câu hỏi phỏng vấn OOP cấp độ nâng cao cũng như các câu hỏi thực hành. Cùng theo dõi để nâng cao kỹ năng lập trình OOP nhé.