Bài 3: Các tính chất của đối tượng trong lập trình


1. Tính đóng gói (Encapsulation):

Tính đóng gói là quy trình giữ và che giấu một hoặc nhiều thành phần với một gói vật lý hoặc logic. Trong phương pháp lập trình hướng đối tượng, nó ngăn truy cập vào chi tiết triển khai bên trong của đối tượng hay một thư viện.

Tính đóng gói được triển khai bằng cách sử dụng các từ khóa chỉ định truy cập (access specifiers). Một chỉ định truy cập định nghĩa phạm vi và sự ẩn hiện của các thành phần trong class. Các từ khóa bao gồm public, private, protected, internal…

  • Mặc định, tất cả phương thức đều ở trạng thái public. Nếu không chỉ định khả năng truy cập của phương thức, nó sẽ là public.
  • Phương thức protected và private không thể truy cập một cách tự do, và do đó khi có một thể hiện của đối tượng, bạn sẽ không thể gọi được các phương thức đó.
  • Phương thức protected và private cũng có sự khác biết xung quanh cách bạn có thể sử dụng chúng trong ngữ cảnh của đối tượng.

 

Chúng ta triển khai tính đóng gói ra sao?

Chúng ta có thể triển khai tính đóng gói bằng cách sử dụng từ khóa private được thể hiện dưới ví dụ sau:

private string GetEngineMakeFormula()
{
       private string formula = "a*b";
       return formula;
}

Ví dụ – [Encapsulation]

public class Bike
{
       public int mileage = 65;
       public string color = "Black";
       private string formula = "a*b";

       // public – có thể truy xuất từ bên ngoài class
       public int GetMileage()
       {
               return mileage;
       }

       // public – có thể truy xuất từ bên ngoài class
       public string GetColor()
       {
               return color;
       }

       //private – Không thể truy xuất từ bên ngoài class
       private string GetEngineMakeFormula()
       {
               return formula;
       }
}

public class Program
{
       public static void Main(string[] args)
       {
               Bike objBike = new Bike();
               Console.WriteLine("Bike mileage is : " + objBike.GetMileage()); //accessible outside "Bike"
               Console.WriteLine("Bike color is : " + objBike.GetColor()); //accessible outside "Bike"
               //Chúng ta không thể gọi phương thức này từ bên ngoài "Bike"
               //objBike.GetEngineMakeFormula(); //commented because we can't access it
               Console.Read();
       }
}

Vậy bạn có thể thấy ví dụ trên đã ẩn phương thức GetEngineMakeFormula() bằng cách sử dụng chỉ định private bởi vì không cần thiết phải đưa ra thông tin về động cơ cho người dùng. Để chỉ đưa ra những phương thức cần thiết cho người dùng, chúng ta sử dụng từ khóa public.

2.  Tính trừu tượng (Abstraction):

Tính trừu tượng là xử lý đưa ra chỉ những thông tin cơ bản của đối tượng trong thế giới thực và ẩn đi toàn bộ chi tiết của một đối tượng. Nó được dựa trên sự chia tách của interface và các triển khai của interface.

Ví dụ chúng ta tiếp tục với “Bike” như một ví dụ, chúng ta không truy cập trực tiếp vào pit-tông, chúng ta có thể sử dụng một nút START để khởi động pit-tông. Hãy tưởng tượng nếu có một nhà máy sản xuất xe máy cho phép truy xuất trực tiếp vào pit-tông, nó sẽ rất khó để điều khiển các hành động trên pit-tông đó. Đó là lý do tại sao một nhà sản xuất xe máy chia tách những chi tiết máy nội bộ ra khỏi giao diện đối với người dùng.

Nhằm làm đơn giản hóa việc sử dụng của người dùng.

Ví dụ – [Abstraction]

public class Bike
{
       public int mileage = 65;
       public string color = "Black";
       private string formula = "a*b";

       //public – có thể truy cập bên ngoài class
       public int GetMileage()
       {
               return mileage;
       }

       //public – có thể truy cập bên ngoài class
       public string GetColor()
       {
               return color;
       }

       //public – không thể truy cập bên ngoài class
       private string GetEngineMakeFormula()
       {
               return formula;
       }

       //public – có thể truy cập bên ngoài class
       public string DisplayMakeFormula()
       {
               //"GetEngineMakeFormula()" là private nhưng được truy cập giới hạn trong class               return GetEngineMakeFormula();
       }
}

public class Program
{
       public static void Main(string[] args)
       {
               Bike objBike = new Bike();
               Console.WriteLine("Bike mileage is : " + objBike.GetMileage()); //accessible outside "Bike"
               Console.WriteLine("Bike color is : " + objBike.GetColor()); //accessible outside "Bike"
               //we can't call this method as it is inaccessible outside "Bike"
               //objBike.GetEngineMakeFormula(); //commented because we can't access it
               Console.WriteLine("Bike color is : " + objBike.DisplayMakeFormula()); //accessible outside
               Console.Read();
       }
}

Như bạn đã thấy từ ví trụ trên, các phương thức và thuộc tính cần thiết được đưa ra với từ khóa public và những phương thức và thuộc tính không cần thiết sẽ được giấu đi sử dụng từ khóa private. Cách này có thể triển khai trừu tượng hóa hoặc chúng ta có thể hoàn thành việc trừu tượng hóa trong ứng dụng.

Chú ý: Tính trừu tượng và tính đóng gói là hai tính chất có liên quan đến nhau trong lập trình hướng đối tượng. Tính trừu tượng cho phép các thông tin liên quan hiển thị và tính đóng gói cho phép một lập trình viên triển khai các mức độ mong muốn của trừu tượng hóa. Điều đó có nghĩa các phần của class được ẩn đi như là Đóng gói và hiển thị ra như là trừu tượng hóa.

3. Che giấu thông tin (Information Hiding):

Che giấu thông tin là khái niệm giới hạn trực tiếp  phơi bày dữ liệu. Dữ liệu được truy cập gián tiếp sử dụng các cơ chế an toàn, các phương thức trong lập trình hướng đối tượng. Hãy xem lại các ví dụ của phần trừu tượng.

4. Tính kế thừa (Inheritance):

Tính kế thừa trong OOP cho phép chúng ta tạo mới các class sử dụng một class có sẵn và có thể mở rộng chúng ra.

Khái niệm này cũng có thể liên quan đến ví dụ thực tế. Hãy tạo một ví dụ Bike. Một nhà sản xuất xe máy sử dụng cùng một cơ chế của một phiên bản xe đã có sẵn trong lúc chuẩn bị ra mắt một phiên bản xe mới. Ví dụ dùng máy 110 của Wave nhưng thêm một số các tính năng tiện ích đồng thời thay vỏ để tạo ra một dòng xe mới WaveS. Điều này cho phép nhà sản xuất tiết kiệm thời gian và công sức.

Lợi ích chính của việc mở rộng các class là cung cấp một cách thức tiện lợi để sử dụng lạp các đoạn code đã được kiểm thử trong lần trước giúp tiết kiệm nhiều thời gian và công sức.

Ví dụ – [Inheritance]

public class Base
{
       public Base()
       {
               Console.WriteLine("Constructor of Base Class");
       }

       public void DisplayMessage()
       {
               Console.WriteLine("Hello, how are you?");
       }
}

public class Child : Base
{
       public Child()
       {
               Console.WriteLine("Constructor of Child class");
       }
}

public class Program
{
       public static void Main(string[] args)
       {
               Child objChild = new Child();
               //Child class don't have DisplayMessage() method but we inherited from "Base" class
               objChild.DisplayMessage();
               Console.Read();
       }
}

Như bạn đã thấy trong ví dụ trước, chúng ta đã tạo mới một đối tượng của class Child trong phương thức Main() và sau đó gọi DisplayMessage() của class Base. Nếu bạn để ý thấy là class Child không hề có phương thức DisplayMessage() trong nó. Rõ ràng là nó được kế thừa từ class Base. Khi bạn thực thi đoạn code sẽ được kết quả như sau:


Kết quả ví dụ

Constructor of Base Class
Constructor of Child class
Hello, how are you?

Như ví dụ trên, chúng ta có thể nói rằng hàm khởi tạo(constructor) của class “Base” sẽ tự động gọi trước hàm khởi tạo của class “Child”.

Vì thế, kết luận là “Base” là class cha sẽ tự động khởi tạo trước class Child là con.

5.  Tính đa hình (Polymorphism):

Từ đa hình(Polymorphism) nghĩa là có nhiều khuôn mẫu. Tổng quan, đa hình xuất hiện khi có một danh sách class chúng có liên quan đến nhau thông qua kế thừa.

Hãy lấy ví dụ Bike, một Bike có thể có 2 khuôn mẫu như xe đề và xe đạp nổi. Chugns ta có thể quyết định phương thức vận hành nào muốn sử dụng để khởi động (nghĩa là lúc chạy)

Có 2 kiểu đa hình:

  • Đa hình ở thời điểm biên dịch (Compile time polymorphism): Đây là kiểu đa hình mà trình biên dịch sẽ nhận dạng khuôn mẫu nào có thể thực hiện ở thời điểm biên dịch gọi là compile time polymorphism hay còn gọi là early binding. Ví dụ chúng ta gọi là Nạp chống phương thức (Method Overloading) và Nạp chồng toán tử (Operator Overloading). Method Overloading nghĩa là có nhiều hơn một phương thức có cùng tên nhưng khác số lượng về tham số trong cùng 1 class hoặc khác class.

– Lợi ích: Thực thi nhanh bởi vì tất cả các phương thức được nhận dạng ở thời điểm biên dịch.
– Điểu yếu: Không mềm dẻo.

Ví dụ – [Method Overloading]

public class Base
{
       //1st: same method name, return type (object) but different parameter type (object)
       public object Display(object a)
       {
               return (a);
       }
               
       //2nd: same method name, return type (int) but different parameter type (int)
       public int Display(int a)
       {
               return (a);
       }
}

public class Program
{
       public static void Main(string[] args)
       {
               Base objBase = new Base();
               int val = 7;

               //here 2nd method will be called with type "int"
               Console.WriteLine(objBase.Display(val));
               Console.Read();
       }
}

Ở ví dụ trên, khi bạn chạy chương trình, hàm Display(int a) sẽ được gọi lần đầu vì biến val là kiểu int. Biến val được gán chỉ tham chiếu như kiểu int ở thời điểm thực thi.

Kết quả

7

Chú ý: Chú ý: Khi nạp chống phương thức, một quy tắc tuân theo là nạp chống phương thức phải so sánh số lượng đối số hoặc kiểu dữ liệu của ít nhất một đối số. Chúng ta có thể xem Method Overriding như là một ví dụ của việc đa hình ở thời điểm biên dịch được gọi trực tiếp bởi đối tượng được khởi tạo.

  • Đa hình ở thời điểm thực thi (Runtime polymorphism): Trong kiểu đa hình này, trình biên dịch sẽ nhận dạng khuôn mẫu đa hình nào sẽ được thực thi ở thời điểm chạy nhưng không phải ở thời diểm biên dịch được gọi là đa hình ở thời điểm thực thi hoặc late binding. Ví dụ early binding là Method Overriding. Method Overriding nghĩa là có hai phương thức với cùng tên cùng signature, một phương thức trong class Base, phương thức kia ở class Child. Nó được yêu cầu thay đổi hành vi của phương thức trong class Base khi class con sử dụng.

– Lợi ích: Nó có thể mềm dẻo đề điều chỉnh các kiểu đối tượng ở thời điểm thực thi.
– Hạn chế: Thực thi sẽ chậm hơn vì phải mất thời gian lấy thông tin về phương thức ở thời điểm chạy.

Chúng ta cần sử dụng một trong 2 cách là virtual method và abstract method cho phép lớp dẫn xuất ghi đè một phương thức của class Base.

Ví dụ – [Method Overriding bằng cách sử dụng phương thức virtual]

public class Base
{
       public virtual string BlogName()
       {
               return "AspnetO";
       }
}

public class Child : Base
{
       //same method name with same signature/parameters
       public override string BlogName()
       {
               return "AspnetO – Quick Way To Learn Asp.net";
       }
}

public class Program
{
       public static void Main(string[] args)
       {
               Base objBase = new Child();
               Console.WriteLine(objBase.BlogName());
               Console.Read();
       }
}

Ở ví dụ trên, khi bạn chạy chương trình, ở thời điểm biên dịch kiểu của objBase là Base nhưng nó sẽ vẫn gọi nạp chống phương thức của class con vì ở thời điểm chạy kiểu của objBase được hiểu là Child.

Kết quả ví dụ

AspnetO – Quick Way To Learn Asp.net

Ví dụ – [Method Overriding  sử dụng phương thức abstract]

public abstract class Base
{
       public abstract string BlogName();
}

public class Child : Base
{
       //same method name with same signature/parameters
       public override string BlogName()
       {
               //It's mandatory to implement abstract method in derived/child class
               return "AspnetO – Quick Way To Learn Asp.net";
       }
}

public class Program
{
       public static void Main(string[] args)
       {
               Base objBase = new Child();
               Console.WriteLine(objBase.BlogName());
               Console.Read();
       }
}

Ở ví dụ trên, khi bạn chạy chương trình, ở thời điểm biên dịch kiểu của objBase là Base nhưng nó sẽ vẫn gọi class con để ghi đè phương thức bởi vì ở thời điểm runtime, kiểu của objBase được tham chiếu là Child.

Kết quả ví dụ

AspnetO – Quick Way To Learn Asp.net

Chú ý: Cả ghi đè phương thức và nạp chống phương thức là các khái niệm khác nhau và rất quan trọng của OOP. Nên chú ý kỹ vì tên nó tương tự nhau.

6.  Hàm khởi dựng (Constructors):

Constructor là một phương thức đặc biệt, được sử dụng khi khởi tạo một thể hiện của một class. Một constructor  không trả về gì, đó là tại sao chúng ta không định nghĩa một kiểu trả về cho nó.

Một phương thức bình thường được định nghĩa:

public string bike()
{
}

Môt constructor đơn giản (không có tham số) được định nghĩa như sau:

public bike()
{
}

Và đây là ví dụ một constructor có tham số:

public class bike
{
       private int mileage;
       private string color;

       public bike()
       {
               //constructor không có tham số
       }

       public bike(int mil, string col)
       {
               //constructor với 2 tham số là "mil" và "col"
               mileage = mil;
               color = col;
       }

       public void DisplayBikeData()
       {
               Console.WriteLine("Bike's Mileage is " + mileage + " and color is " + color);
       }
}

Các điểm quan trọng về constructor:

  • Nếu không có constructor nào được định nghĩa thì CLR (Common Language Runtime) sẽ tự khởi tạo một constructor mặc định.
  • Constructor không trả về giá trị.
  • Constructor có thể được nạp chồng.
  • Một class có thể có số lượng constructor không giới hạn và số lượng tham số trong constructor cũng vậy.
  • Chúng ta không sử dụng tham chiếu hoặc con trỏ trong constructor vì chúng không được cấp địa chỉ.
  • Constructor không được khai báo với từ khóa virtual.

7. Hàm hủy (Destructors):

Bởi vì trình dọn rác GC (Garbage Collection) sẽ tự động làm sạch hệ thống, nó sẽ hủy các đối tượng lâu không sử dụng nhưng có nhiều khi chúng ta cần làm một số thứ bằng tay. Trong trường hợp này chúng ta có thể sử dụng một Destructor để hủy các đối tượng không còn sử dụng.

Một hàm hủy là một phương thức được gọi khi đối tượng hủy, có thể sử dụng để giải phóng các tài nguyên được sử dụng bởi đối tượng. Hàm hủy không giống như các phương thức khác.

Dưới đây là ví dụ về hàm hủy trong class Bike:

public class Bike
{
       public Bike()
       {
               //Constructor
       }
       ~Bike()
       {
               //Destructor
       }
}