C++ Programming – Access functions and encapsulation


Trong bài trước chúng ta đã được học về các biến trong một class, và các biến này thường bị “private” đi. Và các bạn có biết tại sao mà người ta phải đưa các biến trong class thành private không? Nhiều lập trình viên đã dành khá nhiều thời gian để suy nghĩ về vấn đề này. Và trong bài hôm nay, chúng ta sẽ tìm hiểu về nó.

Bây giờ chúng ta có một ví dụ sau đây: Trong cuộc sống hiện đại ngày nay, có ta có rất nhiều các thiết bị điện (chẳng hạn như TV). Và bạn có thể điều khiên TV của bạn thông qua một remote hoặc nút on/off trên TV. Bạn lái một chiếc xe, bạn chụp ảnh bằng camera kĩ thuật số. Tất cả các thứ trên đều đưa ra cho bạn một vài “interface” như nút nhấn trên TV, nút chụp hình trên camera hay chân ga, thắng,… trong xe hơi. Và bạn chỉ sử dụng các thứ mà được đưa ra cho bạn thấy (chúng ta gọi là “interface”), nhưng tất cả những thứ bên trong, bạn không hề biết, bạn đâu biết làm sao TV có thể hiển thị hình ảnh, rồi tắt khi bạn nhấn nút, bạn cũng không biết trong xe hơi hay camera có những thứ cảm biến gì, hệ thống như thế nào. Tóm lại là bạn sử dụng nó nhưng bạn không hề biết những thứ bên trong nó (và đâu cần phải biết).
Chúng ta gọi những thứ bên ngoài bạn sử dụng là “interface”, thì những thứ bên trong bạn có thể gọi là “separation of implementation”. Những thứ trên này có liên quan gì tới bài học thì mời các bạn xem tiếp.

Encapsulation

Đây là một trong 4 tính chất quan trọng nhất của lập trình hướng đối tượng (OOP), chúng ta hay gọi là tính đóng gói (bao đóng), nhưng đúng hơn thì chúng ta nên gọi là tính che dấu thông tin (information hiding). Encapsulation là phương pháp để giữ cho các dữ liệu trong một object được “bảo mật” hơn, bên ngoài hoàn toàn không biết bên trong có gì và làm gì. Chúng ta chỉ có thể thấy được các interface mà class cho public. Vì như vậy, bạn sử dụng object nhưng bạn không hề hiểu bên trong nó như thế nào.

Ở bài trước chúng ta đã được học 3 kiểu để access vào một class là: private, protected, public. Cho nên chúng ta sẽ thực hiện encapsulation thông qua các loại access này. Thông thường thì tất cả các biến (member) trong một class ở chế độ private. Và những member function ở chế độ public thì người ta gọi là interface. Và khi bạn cần sử dụng các biến trong class, bạn có thể sử dụng chúng thông qua các public member function này để truy cập và các biến private.

Lợi ích của encapsulation

Khi một class được encapsulation thì bạn rất dễ dàng sử dụng trong một chương trình phức tạp

Trong một class như vậy, bạn chỉ cần quan tâm tới, biết tới các hàm public, và những hàm này cần những tham số đầu vào (argument) nào và return cái gì, vậy là đủ để bạn tạo và sử dụng object rồi. Bạn không phải quan tâm về các biến private, cho nên việc gặp lỗi sẽ giảm đi rất nhiều và làm giảm đi độ phức tạp của chương trình.
Tất cả các class trong C++ đều được encapsulation.

Giúp bảo vệ và hạn chế dùng sai dữ liệu

Các bạn có biết biến toàn cục không? Các biến toàn cục (global variable) rất nguy hiểm bởi vì bạn không thể điều khiển chính xác được biến này, vì ai cũng có thể sử dụng và thay đổi giá trị của nó.
Chúng ta lấy một ví dụ:

class MyString
{
    char *m_string; // we'll dynamically allocate our string here
    int m_length; // we need to keep track of the string length
};

Biến m_length mô tả chiều dài của chuỗi được chứa trong biến m_string và cả 2 biến này đều public. Các bạn có thể thấy rằng bất cứ ai cũng có thể thay đồi biến m_length trong khi biến m_string không thay đổi (Vậy là chiều dài của chuỗi đã được thay đổi sai). Gây mâu thuẫn phải không? Từ đó có thể dẫn tới gây ra những vấn đề kì lạ hay có thể gặp lỗi. Bởi vậy, 2 biến trên phải được private, và chúng chỉ được truy cập thông qua các hàm public để đảm bảo 2 biến trên làm việc chính xác (chiều dài trong chuỗi m_string phải bằng với biến m_length).

Chúng ta xem ví dụ tiếp

class IntArray
{
public:
    int m_array[10];
};

Trong class trên, mảng m_array là public, và nếu người dùng truy cập vào mảng này một cách trực tiếp, người dùng có thể mắc lỗi sai, có thể đưa vào một chỉ số mảng (index) sai. Ví dụ:

int main()
{
    IntArray array;
    array.m_array[16] = 2; // invalid array index, now we overwrote memory 
                           // that we don't own
}

Bạn đã set phần tử thứ 16 của mảng, và sẽ gây ra lỗi vì mảng chỉ tối đa 10 phần tử. Tuy nhiên, khi bạn cho mảng m_array này private, bạn sẽ bắt người dùng dùng một hàm để có thể set giá trị cho mảng.

class IntArray
{
private:
    int m_array[10]; // user can not access this directly any more
 
public:
    void setValue(int index, int value)
    {
        // If the index is invalid, do nothing
        if (index < 0 || index >= 10)
            return;
 
        m_array[index] = value;
    }
};

Trong trường hợp này, bạn có thể bảo vệ được “tính trung thực” của chương trình (người dùng chỉ được cho phép set các phần tử mảng với các chỉ số từ 0 tới 10. Khi người dùng đưa vào chỉ số sai, thì chương trình sẽ return luôn và không set giá trị cho mảng).

Encapsulation giúp class dễ dàng thay đổi

Xem ví dụ sau đây:

#include <iostream>
 
class Something
{
public:
    int m_value1;
    int m_value2;
    int m_value3;
};
 
int main()
{
    Something something;
    something.m_value1 = 5;
    std::cout << something.m_value1 << '\n';
}

Trong khi chương trình đã chạy chính xác, chuyện gì sẽ xảy ra khi bạn muốn đổi tên biến m_value1 hoặc bạn muốn thay đổi kiểu biến của nó? Đương nhiên là bạn sẽ phải sửa nó ở dòng 16 và 6, tuy nhiên nếu chương trình rất lớn (biến m_value1 được sử dụng cả 100 lần) không lẽ bạn phải sửa nó 100 lần?
Rất khó khăn phải không? Encapsulation giải quyết tốt điều này, xem ví dụ:

#include <iostream>
 
class Something
{
private:
    int m_value1;
    int m_value2;
    int m_value3;
 
public:
    void setValue1(int value) { m_value1 = value; }
    int getValue1() { return m_value1; }
};
 
int main()
{
    Something something;
    something.setValue1(5);
    std::cout << something.getValue1() << '\n';
}

Khi bạn muốn thay đổi tên hay giá trị của biến m_value1, bạn chỉ cần thay đổi duy nhất 1 chổ là dòng thứ 6 và trong 2 function trong class. Hết! Và bạn không phải sửa bất kì chổ nào khác.
Bây giờ chúng ta thử thay đổi chương trình phía trên thành:

#include <iostream>
 
class Something
{
private:
    int m_value[3]; // note: we changed the implementation of this class!
 
public:
    // We have to update any member functions to reflect the new implementation
    void setValue1(int value) { m_value[0] = value; }
    int getValue1() { return m_value[0]; }
};
 
int main()
{
    // But our program still works just fine!
    Something something;
    something.setValue1(5);
    std::cout << something.getValue1() << '\n';
}

Chúng ta vẫn giữ nguyên tất cả hàm main và chỉ thay đổi bên trong class.

Giúp cho chương trình dễ dảng debug

Và lợi ích cuối cùng là giúp cho chương trình dễ dàng debug tìm lỗi. Thông thường khi một chương trình gặp lỗi, thường là do một trong những biến trong class mang giá trị không đúng. Bạn thường đặt breakpoint vào biến đó để chương trình chạy tới đó và bạn xem giá trị của biến đó là bao nhiêu, nhưng bạn biết đặt breakpoint này ở đâu, vì có quá nhiều function đã thay đổi giá trị của biến public? Trong trường hợp private, bạn chỉ cần đặt breakpoint vào một hàm (trong trường hợp trên là hàm getValue1()) thì bạn cho chương trình chạy, cho đến khi có một “lời gọi hàm” này và set giá trị sai cho nó thì bạn sẽ tìm được lỗi.

Access functions

Thường thì trong một class sẽ có 2 function: set và get có nhiệm vụ thiết lập giá trị cho các biến private (set) và xuất giá trị đó ra (get), giống như các ví dụ trước của chúng ta.
Bây giờ chúng ta xem một ví dụ:

class MyString
{
private:
    char *m_string; // we'll dynamically allocate our string here
    int m_length; // we need to keep track of the string length
 
public:
    int getLength() { return m_length; } // access function to get value of m_length
};

Hàm getLength() trả về biến giá trị của biến m_length. Đây là ví dụ để truy cập một biến private trong một class.

Một ví dụ tiếp theo:

class Date
{
private:
    int m_month;
    int m_day;
    int m_year;
 
public:
    int getMonth() { return m_month; } // getter for month
    void setMonth(int month) { m_month = month; } // setter for month
 
    int getDay() { return m_day; } // getter for day
    void setDay(int day) { m_day = day; } // setter for day
 
    int getYear() { return m_year; } // getter for year
    void setYear(int year) { m_year = year; } // setter for year
};

Trong class Date, chúng ta đã sử dụng các hàm get và set để có thể tạo giá trị cho các biến private và lấy giá trị đó ra cho chúng ta. Đây là một cách thông thường nhất để access các biến private. Các bạn hãy ghi nhớ cách này.

Rule

1. Chỉ cung cấp các hàm như trong ví dụ trên để truy cập và sử dụng các biến private.
2. Các hàm get nên được return bởi giá trị của biến hoặc tham chiếu hằng (const reference), không được return non-const reference (khi học đủ nhiều bạn sẽ hiểu được câu này).

Summary

Như bạn thấy, encapsulation có rất nhiều lợi ích, và bạn không phải tốn nhiều nổ lực và thời gian khi xử lý những chương trình phức tạp. Lợi ích tốt nhất của encapsulation là chúng ta có thể sử dụng class mà không cần biết nó được thực thi như thế nào. Hẹn gặp lại bạn ở bài sau: Constructors

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s