Tính đóng gói trong lập trình hướng đối tượng C++
1.2 ENCAPSULATION ĐÓNG GÓI
Lập trình hướng đối tượng (OOP) xoay quanh khái niệm về đối tượng. Tuy nhiên, các đối tượng được tạo bằng cách sử định nghĩa lớp. Một lớp là một khuôn mẫu phù hợp với các đối tượng được tạo ra. Lớp là một phần của phần mềm bao gồm dữ liệu và các chức năng hoạt động trên những dữ liệu này và có thể trên dữ liệu thuộc các lớp khác. Các hàm được định nghĩa trong một lớp được có là methods, và các biến được sử dụng trong lớp được gọi là dữ liệu. Sự kết hợp dữ liệu và các hoạt động liên quan được gọi là đóng gói dữ liệu. Một đối tượng là một thực thể của lớp, một thực thể được tạo bằng định nghĩa của lớp.
Sự khác biệt so với các hàm trong ngôn ngữ lập trình không phải là hướng đối tượng, đối tượng làm cho sự kết nối giữa dữ liệu và các hàm thành viên chặt chẽ và ý nghĩa hơn. Trong ngôn ngữ không phải hướng đối tượng, khai báo dữ liệu và định nghĩa các hàm có thể xen kẻ trong toàn bộ chương trình, và chỉ mã lệnh của chương trình thể hiện được mối liên hệ giữa chúng. Trong ngôn ngữ hướng đối tượng, sự kết nối sẽ được thiết lập ngay từ đầu. Thực tế, chương trình dựa trên sự kết nối này. Một đối tượng được định nghĩa bởi dữ liệu và các hoạt động liên quan, và bởi vì có thể có nhiều đối tượng được sử dụng trong cùng một chương trình, các đối tượng giao biết bằng cách trao đổi thông điệp, tiết lộ rất ít chi tiết về cấu trúc bên trong của chúng khi cần thiết để trao đổi thông tin đầy đủ. Cấu trúc chương trình theo hướng đối tượng cho phép chúng ta hoàn thành một số mục tiêu.
Đầu tiên, sự kết hợp chặt chẽ của dữ liệu và các hoạt động có thể sử dụng tốt hơn nhiều trong việc mô hình hóa một phần của thế giới, đặt biệt được nhấn mạnh bởi các kĩ thuật phần mềm. Không đáng ngạc nhiên, lập trình hướng đối tượng có nguồn gốc từ mô phỏng, nghĩa là, trong mô hình hóa các sự kiện trong thế giới thực. Đầu tiên, ngôn ngữ hướng đối tượng được gọi là Simula, và nó được phát triển vào những năm 1960 ở Nauy.
Thứ hai, đối tượng cho phép tìm lỗi dễ dàng hơn, bởi vì các hoạt động cục bộ được giới hạn của các đối tượng. Ngay cả khi có các tác dụng phụ xảy ra chúng ta cũng dễ theo dõi.
Thứ ba, các đối tượng cho phép chúng che giấu một số chi tiết nhất điện về hoạt động của chúng đối với các đối tượng khác để các đối tượng này không bị ảnh hưởng bất lợi bởi các đối tượng khác, điều này được gọi là nguyên tắc che giấu dữ liệu. Trong ngôn ngữ không phải hướng đối tượng, nguyên tắc này có thể tìm thấy ở một mức độ nào đó trong trường hợp các biến cục bộ, hoặc như trong Pascal, trong các hàm hoặc các thủ tục cục bộ, chỉ có thể sử dụng và truy cập bởi các hàm định nghĩa chúng. Tuy nhiên, đây là một sự che giấu chặt chẽ hoặc không có sự che giấu nào cả. Đôi khi, chúng ta có thể cần sử dụng (lặp lại, như trong Pascal) một hàm f2
được định nghĩa trong f1
mà không thể sử dụng bên ngoài f1, nhưng chúng ta không thể làm điều đó. Đôi khi, chúng ta cần truy cập vào dữ liệu cục bộ đến f1
mà không biết chính xác cấu trúc của dữ liệu này, nhưng chúng ta không thể. Do đó, một số sửa đổi là cần thiết, và nó được hoàn thành trong ngôn ngữ hướng đối tượng.
Một đối tượng trong ngôn ngữ lập trình hướng đối tượng giống như một chiếc đồng hồ. Là người dùng, chúng ta quan tâm đến những gì mà kim hiển thị, nhưng không quan tâm đến cách làm việc bên trong của đồng đồ. Chúng ta biết rằng có bánh răng và lò xo bên trong đồng hồ, nhưng vì chúng ta thường biết rất ít về lý do tại sao tất cả chúng lại nằm trong một cấu hình cụ thể, chúng ta không nên can thiệp vào cơ cấu máy móc của chúng để tránh làm hỏng chúng, có thể vô tình hoặc cố ý. Cơ chế hoạt động của nó được che giấu, chúng ta không nên can thiệp vào nó, và đồng hồ được bảo vệ và hoạt động tốt hơn so với khi cấu tạo của nó được được mở ra cho mọi người xem.
Một đối tượng giống như một chiếc hộp đen có hành vi được xác định rõ ràng, và chúng ta sử dụng đối tượng bởi vì chúng ta biết nó làm gì, không phải vì chúng ta có cái nhìn sâu sắc về cách nó hoạt động. Sự mập mờ của các đối tượng vô cùng hữu ích cho việc duy trì chúng độc lập với nhau. Nếu các kênh thông tin liêc lạc giữa các đối tượng được định nghĩa rõ ràng, sau đó những thực hiện thay đổi bên trong đối tượng chỉ có thể ảnh hưởng đến các đối tượng khác khi những thay đối đó ảnh hưởng đến kênh giao tiếp. Biết loại dữ liệu được gửi và nhận bởi một đối tượng, đối tượng có thể được thay thế dễ dàng bằng một đối tượng khác ở trong một trường hợp cụ thể; một đối tượng mới có thể thực hiện cùng một tác vụ theo cách khác nhưng nhanh hơn trong một môi trường phần cứng nhất định. Một đối tượng chỉ tiết lộ ở mức vừa đủ để người dùng sử dụng nó. Nó có một phần công khai có thể truy cập được bởi một số người dùng khi người dùng gửi tin nhắn khớp với bất kì hàm thành viên nào được tiết lộ bởi đối tượng. Trong phần công khai này, đối tượng hiển thị các nút cho người dùng để có gọi các hoạt động của đối tượng. Người dùng chỉ biết tên các hoạt động và hành vi mong đợi.
Che giấu thông tin có xu hướng làm mờ ranh giới giữa dữ liệu và hoạt động. Trong các ngôn ngữ giống như Pascal, sự khác biệt giữa dữ liệu và hàm hoặc các thủ tục là rõ ràng. Chúng được định nghĩa khác nhau và có vai trò rất khác biệt. Các ngôn ngữ lập trình hướng đối tượng đặt dữ liệu và phương thức lại với nhau, và đối với người dùng đối tượng, sự khác biệt này ít được đáng chú ý. Đến một mức độ nào đó, điều này kết hợp các tính năng của ngôn ngữ chứ năng. LISP, một trong những ngôn ngữ lập trình sớm nhất, cho phép người dùng xử lý dử liệu và các chức năng tương tự, bởi vì cấu trúc của cả hai đều giống nhau.
Chúng ta đã phân biệt được sự khác nhau giữ các đối tượng củ thể và các loại đối tượng hoặc các lớp. Chúng ta biết các hàm sử dụng các biến khác nhau, và bằng cách tương tự, chúng ta không muốn bị ép buộc viết nhiều khai bao đối tượng bằng số đối tượng mà chương trình yêu cầu. Một số đối tượng cùng loại và chúng tôi muốn sử dụng tham chiếu đến đối tượng chung. Đối với một biến đơn, chúng ta phân biệt giữa khai báo kiểu và khai báo biến. Trong trường hợp các đối tượng, chúng ta có khai báo một lớp và khởi tạo một đối tượng. Ví dụ, trong khai báo lớp sau. C là một lớp, object1
thông qua object3
là một đối tượng.
class C{
public:
C(char *s = "", int i =0, double d = 1){
strcpy(dataMember1,s);
dataMember2 = i;
dataMember3 = d;
}
void memberFunction1(){
cout<<dataMember1<<" "<<dataMember2<<" "<<dataMember3<<"\n";
}
void memberFunction2(int i, char *s = "unknown"){
dataMember2 = i;
cout << i << " received from " << s << endl;
}
protected:
char dataMember1[20];
int dataMember2;
double dataMember3;
};
C object1("object1",100,2000), object2("object2"), object3;
Truyền thông điệp (message passing) tương đương với gọi hàm trong các ngôn ngữ truyền thống. Tuy nhiên, để nhấn mạnh tính thực tế trong các ngôn ngữ lập trình hướng đối tượng, các hàm thành viên có liên quan đến các đối tượng, một thuật ngữ mới được sử dụng. Ví dụ, lời gọi đến hàm public.memberFunction1()
trong object1
object1.memberFunction1()
được xem như thông điệp memberFunction1() được gửi đến object1
. Khi nhận được thông báo, đối tượng sẽ gọi các hàm thành viên và hiển thị các thông tin liên quan. Thông điệp bao gồm các tham số như
object1.memberFunction1(123)
Một tính năng đặc biệt của C++ là có thể khai báo các lớp chung bằng cách sử dụng các kiểu tham số trong khai báo lớp. Ví dụ, nếu chúng ta cần khai báo một lớp sử dụng một mảng để lưu trữ một số mục, thì chúng ta có thể khai báo lớp này là
class intClass{
int storage[50];
};
Tuy nhiên, ở đây chúng ta giới hạn khả năng sử dụng của lớp này chỉ với các số nguyên. Nếu chúng ta cần một lớp thực hiện các hoạt động tương tự intClass ngoại trừ việc nó hoát động trên số thực, sau đó cần khai báo mới, chẳng hạn như
class floatClass{
float storage[50];
};
Nếu storage
là giữ các cấu trúc, hoặc các con trỏ chỉ đến ký tự, sau đó thêm hai lớp nữa phải được khai báo. Nó sẽ tốt hơn nhiều nếu khai báo một lớp chung và quyết định những mục mà đối tượng đề cập đến chỉ khi xác định các đối tượng. Thật may mắn, C++ cho phép chúng ta khai báo lớp bằng cách như thế này, và xem ví dụ khai báo dưới đây:
template <class genType>
class genClass{
genType storage[50];
};
Sau đó, chúng tôi đưa ra quyết định về cách khởi tạo genType
genClass <int> intObject;
genClass <float> floatObject;
Lớp chung này là nền tàng để tạo thành hai lớp mới, genClass
thuộc kiểu số nguyên và genClass
thuộc kiểu số thực, và sau đó có hai lớp được dùng để tạo ra hai đối tượng, intObject
và floatObject
. Trong cách này, lớp chung thể hiện dưới các hình thức khác nhau tùy thuộc vào cách khai báo cụ thể. Một khai báo chung đủ để kích hoạt các biểu mẫu khác nhau.
Chúng ta có thể có thể đi xa hơn bằng cách không cam kết với 50 ô nhớ trong storage
và bằng cách trì hoãn cho đến giai đoạn xác định đối tượng. Nhưng ở trong trường hợp, chúng ta có thể để một giá trị mặc định khi khai báo lớp bây giờ là
template <class genType, int size = 50>
class genClass{
genType storage[size];
};
Bây giờ các đối tượng được định nghĩa là
genClass<int> intObject1; //sử dụng kích thước mặc định
genClass<int,100> intObject2;
genClass<float,123> floatObject;
Phương pháp này sử dụng các kiểu chung không chỉ giới hạn ở các lớp, chúng ta có thể dùng chúng trong khai báo hàm. Ví dụ, các hoạt động hoán đối hai giá trị có thể được định nghĩa bởi hàm:
template<class genType>
void swap(genType& el1, genType& el2) {
genType tmp = el1; el1 = el2; el2 = tmp;
}
Ví dụ này cũng chỉ ra sự cần thiết của việc điều chỉnh các toán tử cài sẵn cho các tình huống riêng biệt. Nếu genType
là một số, một kí tự hoặc một cấu trúc, thì gán toán tử =
thực hiện đúng chức năng của nó. Nhưng nếu genType
là một mảng, sau đó chúng ta có thể gặp vấn để ở trong hàm swap()
. Vấn đề có thể được giải quyết bằng cách nạp thêm các toán tử bằng cách thêm nó vào chức năng yêu cầu của một kiểu dữ liệu cụ thể.
Sau đó một hàm chung đã được khai báo. Một hàm thích hợp có thể được tạo ra tại thời điểm biên dịch. Ví dụ, nếu trình biên dịch thấy 2 lệnh gọi:
swap(n,m); // đổi chổ 2 số nguyên
swap(x,y); // đổi chổ 2 số thực
nó tạo ra hai hàm đổi chổ được sử dụng trong quá trình thực thi chương trình.