Bốn tính chất cần lưu tâm khi lập trình hướng đối tượng – OOP Concepts
Khi nói đến các tính chất của lập trình hướng đối tượng. Anh em lập trình có thể liệt kê ra cả bốn đặc tính ngay lập tức. Đó là tính đóng gói, tính kế thừa, tính đa hình và tính trừu tượng. Đối với một số anh em, lý thuyết phần này khá khó hiểu và khó nhớ. Ở phần 2 của series “Hướng đối tượng bỏ túi”. Mình sẽ hệ thống lại kiến thức về bốn đặc tính quan trọng khi lập trình hướng đối tượng.
MỤC LỤC
Đặt vấn đề
Để nội dung của bài viết liền mạch hơn, mình sẽ đặt vấn đề trước. Mọi tính chất của hướng đối tượng sẽ được mình lồng ghép và xây đắp từ vấn đề này.
Tiếp nối phần 1 của series. Sau khi thiết kế ra chiếc xe chỉ chạy được trên … NetBeans. Mình đã được sếp thăng chức, chuyển sang làm nhân viên vườn thú. Với người thông minh và tài giỏi như mình, việc này dễ như tìm đường vào tim crush vậy. Suốt ngày chỉ có cho ăn, đếm thú, tối đến lại lùa vào chuồng. Dù thời gian bận không nhiều nhưng mình vẫn rãnh để mô phỏng sở thú này trên máy tính. Vậy mình nên thiết kế như nào anh em nhỉ?
Encapsulation – Tính bao đóng
Tính bao đóng là gì?
Encapsulation means that a group of related properties, methods, and other members are treated as a single unit or object.
Tính chất đầu tiên chúng ta cần nhớ chính là tính bao đóng (đóng gói). Tính bao đóng yêu cầu thực hiện gom những thứ liên quan lại thành một đơn vị hoặc đối tượng, Class. Đồng thời ẩn những dữ liệu nhạy cảm khỏi khả năng truy xuất của người dùng.
Tính bao đóng có thể đạt được bằng cách sử dụng Access Modifier
và Getter - Setter
. (xem lại định nghĩa ở phần 1)
Phân tích và áp dụng tính bao đóng
Tính chất này cũng dễ hiểu thôi, chúng ta sẽ bắt đầu với con Vịt nhé. Vậy là giờ chúng ta cần gom mấy thứ có liên quan lại thành một class, đặt tên là Duck. Để xem nào, nó là loài là vịt nè, phải có tên riêng nữa. Nó có thể ăn, bơi rồi kêu cak cak cak. 🦆 Nếu như thế thì tên loài sẽ không cho phép bên ngoài thay đổi, còn tên riêng con vịt có thể đặt sao cũng được. Sau đó là định nghĩa các phương thức đại diện cho hành động ăn, bơi, kêu của con vịt.
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 |
class Duck { // Constructor để tạo con vịt public Duck(string name) { Name = name; } // Tên loài, thông tin này dùng nội bộ private string Species { get; set; } = "Duck"; // Đặt tên cho con vịt // Chỉ đọc và đặt tên 1 lần lúc tạo public string Name { get; private set; } // Các hành động của con vịt public void Eat() { Console.WriteLine($"{Species} name: {Name} is Eating!"); } public void Swim() { Console.WriteLine($"{Species} name: {Name} is Swimming!"); } public void Sound() { Console.WriteLine($"{Name} - Mot con Vit xoe ra hai cai canh, no keu rang cak cak cak cak cak cak."); } } |
Như thế là chúng ta đã định nghĩa được con vịt trông như thế nào rồi. Việc còn lại là viết hàm main và chạy thử thôi. Demo ngôn ngữ Java anh em cho mình xin khất. Ai cần hãy comment trong bài viết, mình sẽ bổ sung sau.
Inheritance – Tính kế thừa
Tính kế thừa cần được hiểu như thế nào?
Inheritance describes the ability to create new classes based on an existing class.
Đây là tính chất dễ hiểu nhất của hướng đối tượng. Nó cho phép chúng ta định nghĩa một class mới dựa trên một class đã định nghĩa trước đó. Nói cách khác là class con kế thừa lại những đặc tính đã có của class cha.
Tính kế thừa có ba cấp độ:
- Base class inheritance: Kế thừa toàn bộ từ lớp cơ sở.
- Abstract class inheritance: Kế thừa từ lớp abstract. Gọi là kế thừa một phần vì phải định nghĩa lại những hàm abstract.
- Interface inheritance: Kế thừa khuôn mẫu, phải định nghĩa rất cả những gì interface yêu cầu.
Tuy nhiên, để đảm bảo tính kế thừa, ta cần chú ý khá nhiều thứ:
- Lớp kế thừa được sử dụng những thành phần được cho phép của lớp cơ sở quy định bở Access Modifier (public, protected, v.v…).
- Nói cho chuẩn: Kế thừa class gọi là
extends
và kế thừa interface gọi làimplements
. Java sử dụng 2 từ khóa đó để thực hiện kế thừa. Đối với C#, chỉ cần dùng dấu:
cho cả class và interface. - Từ khóa
this
: Đại diện cho lớp chứa cục code hiện tại. base class
đại diện cho lớp cơ sở, lớp cha. Ngôn ngữ Java sử dụng phương thứcsuper()
, C# sử dụngbase
. Đối với C++ là Google.com.- Kế thừa 1 cấp & kế thừa nhiều cấp.
- Lớp kế thừa có thể ép kiểu về lớp cơ sở mà không làm mất tính đúng đắn của chương trình.
- Hầu hết các ngôn ngữ không cho phép đa kế thừa class. Tránh việc 2 class kế thừa có cùng các thuộc tính giống nhau, nhưng có thể đã implement khác nhau.
- Có thể đa kế thừa Interface.
Phân tích và xây dựng sở thú dựa trên tính kế thừa
Nếu như chúng ta xây dựng sở thú chỉ dựa vào tính bao đóng như ở ví dụ 1. Thì nghĩa là cứ 10 loài ta phải tạo 10 class, sau đó định nghĩa lại thuộc tính, hành động một lần nữa. Nếu đặc điểm chung giữa chúng có điểm cần thay đổi lại phải sửa thủ công 10 class khác nhau. Rất là mất thời gian, công sức và hơi khổ râm. Đó là lý do chúng ta phải áp dụng tính kế thừa.
Lúc này, chúng ta cần gom những đặc tính chung của động vật vào một lớp gọi là Animal. Sau đó muốn tạo ra loài mới, ta chỉ cần kế thừa từ lớp động vật là nó đã có những đặc tính của động vật rồi. Chương trình của chúng ta cần nâng cấp như sau:
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 |
// Lớp Động vật class Animal { private string Species { get; set; } // Loài public string Name { get; private set; } // Tên public Animal(string species, string name) { this.Species = species; this.Name = name; } // Lấy thông tin con vật public string GetInfo() => $"{Species} name: {Name}"; // Hành động chung của các con vật public void Eat() { Console.WriteLine($"{GetInfo()} is Eating!"); } public void Sound() { Console.WriteLine($"{GetInfo()} is Chirped"); } } |
Lúc này, class Duck cần kế thừa hay nói đúng hơn là mở rộng (extends) class Animal. Và nó được tinh gọn như sau:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Loài Vịt class Duck : Animal { // Gọi lại constructor của lớp cha bằng hàm base public Duck(string name) : base("Duck", name) { /* Some code here */ } // Hành động riêng của con Vịt public void Swim() { Console.WriteLine($"{GetInfo()} is Swimming!"); } } |
Khi cần định nghĩa một loài mới, ta chỉ cần tạo class mới, kế thừa class Animal, sau đó thêm các thuộc tính của riêng loài mới là xong. Chúng ta sẽ định nghĩa thêm loài khỉ cho sở thú.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Loài Khỉ class Monkey : Animal { // Gọi lại constructor của lớp cha bằng hàm base public Monkey(string name) : base("Monkey", name) { /* Some code here */ } // Hành động riêng của con Khỉ public void Climb() { Console.WriteLine($"{GetInfo()} is Climbing!"); } } |
Sau khi định nghĩa 3 class trên, anh em tạo hàm main để chạy thử và xem kết quả:
1 2 3 4 5 6 7 |
static void Main(string[] args) { var duck = new Duck("Donal"); // Tạo con vịt Donal var monkey = new Monkey("Wukong"); // Tạo con khỉ wukong duck.Eat(); // Cho vịt ăn monkey.Eat(); // Cho khỉ ăn } |
Vậy là chúng ta đã áp dụng tính kế thừa cho sở thú rồi đấy. Bạn thử dự đoán kết quả in ra màn hình như thế nào nhé. 😇
Định nghĩa tính đa hình
Ngay trong từ không làm ảnh hưởng đến tính đúng đắn của chương trình.
Method Overloading – Nạp chồng phương thức
Method Overloading cho phép triển khai cùng một tính năng với nhiều loại tham số khác nhau. Nạp chồng được gọi là
compiletime polymorphism
.
Ví dụ phương thức cho vịt ăn ngoài việc cho ăn mặc định, đôi lúc ta cần cho cho biết thêm loại thức ăn, số lượng, thời gian, v.v… .
Phương thức nạp chồng phải cùng tên và cùng kiểu trả về và thỏa mãn một trong các điều kiện sau:
- Khác số lượng tham số truyền vào (parameters).
- Khác kiểu dữ liệu của các tham số truyền vào.
- Khác thứ tự của tham số truyền vào.
Method Overriding – Ghi đè phương thức
Method Overriding là một phương pháp cho phép lớp kế thừa tái định nghĩa một phương thức đã định nghĩa ở lớp cha. Ghi đè được gọi là
runtime polymorphism
.
Ghi đè phương thức phải thỏa mãn ba điều kiện sau:
- Phải có quan hệ kế thừa giữa hai class.
- Cùng tên và cùng kiểu trả về (hoặc sub-type).
- Cùng các tham số truyền vào.
Thực tế, ghi đè phương thức của Java và C# có nhiều điểm khác nhau. Ngày trước còn tay mơ mình rất hay nhầm lẫn mấy vấn đề này.
Java Method Overriding
Đối với Java, chỉ cần định nghĩa phương thức ghi đè trong class con thì nó được ngầm hiểu là phương thức ghi đè. Từ khóa @Override
có dùng hay không cũng được, nhưng anh em nên dùng để lúc đọc code đỡ lú.
Ví dụ: Ghi đè phương thức Sound()
của con Vịt.
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 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
package blog.hieuda.com; class Animal { private String species; private String name; public Animal(String species, String name) { this.species = species; this.name = name; } public String getName() { return name; } private void setName(String name) { this.name = name; } public String getInfo() { return species + " name: " + name; } public void eat() { System.out.println(getInfo() + " is eating"); } // Overloading public void eat(String food) { System.out.println(getInfo() + " is eating " + food); } public void sound() { System.out.println(getInfo() + " is Chirped"); } } class Duck extends Animal { // Gọi lại constructor của animal public Duck(String name) { super("Duck", name); } // Ghi đè hàm sound, ko cần sử dụng annotation vẫn được // @Override public void sound(){ System.out.println(getInfo() + " - Mot con vit xoe ra hai cai canh. No keu rang: cắk cắk cắk cằk cằk cằk."); } // Hành động riêng của con Vịt public void swim(){ System.out.println(getInfo() + " is swimming."); } } public class Main { public static void main(String[] args) { Animal a = new Duck("Donal Trung"); a.Sound(); // Output: // Duck name: Donal Trung - Mot con vit xoe ra hai cai canh. No keu rang: cắk cắk cắk cằk cằk cằk. } } |
C# Method Overriding
Đối với C#, khi ghi đè phương thức, ta bắt buộc phải sử dụng từ khóa virtual
cho phương thức của lớp cha. Và sử dụng từ khóa override
khi định nghĩa phương thức ghi đè. Nếu không, compiler sẽ quăng cho bạn một cái warning bảo là this method bị hide gì đó. Khi đó, ta cần dùng từ khóa new
để định nghĩa phương thức đó là Method Hiding. Đây không phải là bug, đây là cơ chế sẽ được đề cập ở bài viết Method Hidding.
Ví dụ C# Method Overriding mình đính kèm vào code trừu tượng bên dưới luôn nha.
Trước khi bắt đầu, cần thẳng thắn với nhau rằng “Không có tính trừu trượng trong OOP, chỉ có Data Abstraction”. Các bài viết trên mạng đang ra rả rằng Hướng đối tượng có bốn tính chất. Như vậy là không đúng bản chất. Trong các giai đoạn phát triển phần mềm, Data Abstraction nằm trong giai đoạn thiết kế. Còn OOP nằm ở giai đoạn triển khai. Do đó, nó phải đáp ứng được các yêu cầu nghiệp vụ, là tầng trung gian kết nối business logic với các kiến trúc phần mềm. Data Abstraction là mục tiêu mà lập trình hướng đến. OOP sử dụng các object, class, interface, và ba tính chất đóng gói, kế thừa, đa hình cũng để đạt đến trạng thái Abstraction. Đó cũng là lý do mình giới thiệu Data Abstraction sau khi đã nói về những thứ khác của OOP.
Nghe có vẻ trừu tượng, nhưng nghĩ lại thì rất trừu tượng 🤨
Trừu tượng hóa dữ liệu nghĩa là che giấu những thành phần không cần thiết khỏi người dùng. Các bạn tránh nhầm lần với việc “ẩn những dữ liệu nhạy cảm khỏi khả năng truy xuất của người dùng” của tính đóng gói. Điều này cho phép người dùng có thể triển khai những logic phức tạp dựa trên một lớp trừu tượng có sẵn mà không cần quan tâm bên trong thực sự làm gì.
Trừu tượng có thể đạt được bằng cách sử dụng:
- Abstract class
- Interface
Phân tích và trừu tượng hóa sở thú
Ở ba phần trên, chúng ta đã lần lượt xây dựng những thứ nền tảng nhất. Sở thú đã có thể đi vào hoạt động bình thường. Lần này, ta sẽ nâng cấp bằng cách trừu tượng hóa chúng. Làm sao khi sở thú thuê nhân viên mới, họ vẫn có thể cho thú ăn, ngủ, chạy mà không cần quan tâm cụ thể công việc đó phải làm gì.
Interface – Chia tách các chức năng thành các Interface
Mindset của interface là quy định một số chức năng cho đối tượng. Nói đơn giản là interface báo cho người dùng biết class đang được sử dụng có thể làm gì. Thực tế, các interface có sẵn trong Java thường được đặt kiểu: Runnable
, Enumerable
,… đại diện cho khả năng mà class đó có thể phục vụ. Ở bài viết này, chúng ta sẽ tạo 3 interface là IAnimal
, IRunnable
, ISwimmable
đại diện cho loài vật và khả năng di chuyển của chúng.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
interface IAnimal { void Eat(); void Eat(string food); void Sound(); } interface IRunnable { void Run(); } interface ISwimmable { void Swim(); } |
Abstract class – Implement một số chức năng
Trong thực tế, động vật chỉ là một khái niệm, không phải là con vật cụ thể nên được gọi là trừu tượng. Chúng ta bắt đầu xây dựng lớp trừu tượng Animal. Trong đó có các property và implement sẵn một vài chức năng. Lúc này chúng ta không thể tạo object kiểu Animal ani = new Animal();
được mà nó sẽ được dùng để đại diện cho các object khác kế thừa từ nó.
Ví dụ: Animal ani = new Duck("Donal")
hoặc Animal ani = new Monkey("Wukong");
Hoặc vjp pro hơn: IList<IAnimal> zoo = new List<IAnimal>();
Sau đó zoo.Add(new Duck("Donal"));
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 |
abstract class Animal : IAnimal { private string Species { get; set; } public string Name { get; private set; } public Animal(string species, string name) { this.Species = species; this.Name = name; } // Lấy thông tin con vật public string GetInfo() => $"{Species} name: {Name}"; // Hành động ăn có thể ghi đè hoặc không public virtual void Eat() { Console.WriteLine($"{GetInfo()} is eating!"); } public virtual void Eat(string food) { Console.WriteLine($"{GetInfo()} is eating {food}!"); } // Các lớp kế thừa phải implement tiếng kêu public abstract void Sound(); } |
Tạo lớp Duck và Monkey
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 |
class Duck : Animal, ISwimmable { // Constructor để tạo con Vịt // Gọi lại constructor của lớp cha bằng hàm base public Duck(string name) : base("Duck", name) { /* Some code here */ } // Ghi đè hành động ăn public override void Eat() { Console.WriteLine("Override eating from Duck.Eat()"); base.Eat(); } public override void Eat(string food) { Console.WriteLine("Override eating from Duck.Eat(food)"); base.Eat(food); } // Triển khai abstract method public override void Sound() => Console.WriteLine($"{GetInfo()} - Mot con vit xoe ra hai cai canh. No keu rang: cắk cắk cắk cằk cằk cằk."); // Khả năng bơi của con Vịt từ ISwimmable public void Swim() => Console.WriteLine($"{GetInfo()} is Swimming!"); } class Monkey : Animal, IRunnable { public Monkey(string name) : base("Monkey", name) { /* Some code here */ } public override void Sound() => Console.WriteLine($"{GetInfo()} - Con khỉ kêu éc éc éc"); public void Run() => System.Console.WriteLine($"{GetInfo()} is running."); } |
Viết hàm Main để tạo sở thú
Chúng ta sẽ tạo một danh sách các con vật và cho chúng ăn mà không cần quan tâm chúng là con gì.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void Main(string[] args) { IList<IAnimal> zoo = new List<IAnimal>(); zoo.Add(new Duck("Donal")); zoo.Add(new Duck("Trung")); zoo.Add(new Monkey("Son")); zoo.Add(new Monkey("Wukong")); foreach (IAnimal animal in zoo) { animal.Sound(); animal.Eat("Special food"); } } |
Và đây là kết quả khi chạy hàm main trên. Kết quả có giống những gì bạn dự đoán không? Nếu không, trả lời câu hỏi vì sao nhé!
Chính vì thế, mình không đặt tiêu đề bài viết là “Bốn tính chất của hướng đối tượng” mà đổi thành “Bốn tính chất cần lưu tâm khi học…”.
Tổng kết
Đây là bài viết thứ 2 của series Hướng đối tượng bỏ túi. Và cũng là quan trọng nhất trong ba bài. Ở phần này, chúng ta đã nói về bốn tính chất của lập trình hường đối tượng. Có thể các bạn không để ý, mình sắp xếp theo thứ tự tính đóng gói, kế thừa, đa hình và trừu tượng là vì tính chất về sau yêu cầu nắm tính chất trước mới ứng dụng được.
Series hướng đối tượng bỏ túi – OOP in basic:
- Các khái niệm căn bản trong lập trình hướng đối tượng.
- Bốn tính chất cần lưu tâm khi lập trình hướng đối tượng.
- Nguyên tắc S.O.L.I.D – Cảnh giới tối thượng của hướng đối tượng.
Bài viết nặng lý thuyết quá phải không anh em 😅 Để tổng hợp và viết bài này mình cũng mất mấy ngày lận. Cũng không thể tránh khỏi sai sót. Nếu anh em phát hiện chỗ nào không ổn hoặc giải thích khó hiểu. Hãy comment bên dưới để mình chỉnh sửa lại phù hợp hơn nhen. Cuối cùng, mình đính kèm bức ảnh tổng quan của 3 bài viết về hướng đối tượng.
Refs
Difference between Compile-time and Run-time Polymorphism in Java | Method Hiding in C# | Method Overriding in Java | Abstraction is not a principle of Object-Oriented Programming