I. QUÁ TRÌNH THIẾT KẾ HỆ THỐNG SỐ.
Sự khác nhau giữa hàm thành viên,
hàm thành viên, hàm không thành viên và hàm friend Việt Thanh Sự khác nhau lớn nhất giữa hàm thành viên và hàm không thành viên là các hàm thành viên có thể là hàm ảo trong khi hàm không thành viên thì không. Như là một hệ quả, nếu bạn có một hàm ảo thì hàm ảo đó phải là thành viên của một lớp nào đó.
Xét một lớp thể hiện các số hữu tỉ:
class Rational { public:
Rational(int numerator = 0, int denominator = 1);
int numerator() const; int denominator() const; private:
...
};
Nhưđã thấy, hiện giờ lớp này là vô dụng. Bạn cần phải thêm các phép toán như: cộng, trừ, nhân, chia, … nhưng bạn chưa biết chắc phải hiện thực chúng dưới dạng hàm thành viên, hàm không thành viên hay hàm friend. Khi ngờ vực, hãy dùng hướng đối tượng. Bạn biết rằng phép nhân các số hữu tỉ sẽ liên quan đến lớp Rational, do đó hãy thêm thao tác này vào lớp như là một hàm thành viên:
class Rational { public:
...
const Rational operator*(const Rational& rhs) const; };
Bây giờ bạn có thể nhân 2 số hữu tỉ một cách dễ dàng: Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; result = result * oneEighth;
Nhưng bạn vẫn chưa thỏa mãn. Bạn cũng muốn hỗ trợ các thao tác hòa trộn giữa các chếđộ (mixed-mode operations). Ví dụ như nhân một đối tượng Rational với một số kiểu int. Khi bạn cố gắng làm điều này bạn
sẽ thấy rằng nó chỉ thực hiện được một nửa: result = oneHalf * 2; // tốt
result = 2 * oneHalf; // lỗi!
Đây là một điềm gở. Phép nhân có tính giao hoán. Nguyên nhân gây ra lỗi sẽ trở nên rõ ràng hơn nếu bạn viết 2 câu lệnh trên ở dạng khác tương đương:
result = oneHalf.operator*(2); // tốt
result = 2.operator*(oneHalf); // lỗi!
Đối tượng oneHaft là thuộc lớp Rational có chứa hàm operator*, nên trình biên dịch sẽ gọi hàm này. Tuy nhiên, số nguyên 2 không có một lớp tương đương nên sẽ không có hàm operation*. Trình biên dịch sẽ tìm một hàm operator* không thành viên khác, ví dụ: result = operator*(2, oneHalf); // lỗi!
Nhưng dĩ nhiên là không có hàm không thành viên operator* nào nhận đối số là một số nguyên int và một đối tượng Rational nên việc tìm kiếm thất bại.
Xem lại lời gọi thành công, bạn sẽ thấy tham số thứ hai là một số nguyên 2, trong khi hàm Rational:: operator* chỉ nhận các đối tượng Rational làm tham số. Điều gì đang xảy ra? Tại sao số 2 lại làm việc ở vị trí này còn vị trí kia thì không?
Điều đang xảy ra chính xác là sự chuyển kiểu. Trình biên dịch biết rằng bạn đang truyền một số nguyên int và hàm thì lại đòi hỏi tham số là một đối tượng Rational. Nhưng nó cũng biết rằng nó có thể tạo ra một đối tượng Rational tương ứng bằng cách gọi hàm khởi dựng của Rational với đối số là số nguyên int bạn cung cấp nên nó đã làm như vậy. Nói cách khác nó xem lời gọi của bạn tương tự như sau:
const Rational temp(2); // tạo một đối tượng Rational tạm từ số 2
result = oneHalf * temp; // hay oneHalf. operator*(temp);
Dĩ nhiên trình biên dịch chỉ làm điều này khi có các hàm khởi dựng nonexplicit (không tường minh) bởi vì các hàm khởi dựng explicit (tường minh) không thể được dùng để chuyển kiểu ngầm. Nếu lớp Rational được định nghĩa như sau:
class Rational { public:
explicit Rational(int numerator = 0, // hàm khởi dựng bây giờ là explicit
int denominator = 1); ...
const Rational operator*(const Rational& rhs) const; ... }; Thì cả 2 dòng lệnh sau đều bị lỗi: result = oneHalf * 2; // lỗi! result = 2 * oneHalf; // lỗi! Việc hỗ trợ khả năng nhân giữa các tập số khác nhau là rất khó khăn khi thực hiện theo các này. Nhưng ít nhất thì cách hành xử của cả hai phát biểu là đồng nhất. Tuy nhiên, lớp Rational chúng ta đang xem xét được thiết kếđể cho phép chuyển kiểu ngầm từ các kiểu dựng sẵn sang Rational. Đó là lý do vì sao hàm khởi dựng của Rational không được khai báo là explicit. Trình biên dịch của bạn sẽ thực hiện việc chuyển kiểu ngầm trên mọi tham số của mọi hàm gọi. Nhưng chúng chỉ thực hiện điều đó đối với các tham sốđược liệt kê trong danh sách tham số chứ không phải cho đối tượng chứa hàm thành viên được gọi, nghĩa là đối tượng tương ứng với con trỏ *this bên trong một hàm thành viên. Đó là lý do vì sao lời gọi sau chạy được:
result = oneHalf.operator*(2); // chuyển đổi int -> Rational
trong khi lời gọi sau thì không:
result = 2.operator*(oneHalf); // không chuyển đổi int -> Rational
Trường hợp đầu tiên gọi một tham sốđược liệt kê trong khai báo hàm nhưng trường hợp thứ hai thì không.
Tuy nhiên, bạn vẫn muốn hỗ trợ khả năng thực hiện phép toán giữa các kiểu khác nhau. Và như vậy cách để thực hiện điều này đã trở nên rõ ràng: làm cho hàm operator* trở thành không thành viên. Điều đó cho phép trình biên dịch thực hiện việc chuyển kiểu ngầm trên tất cả các đối số:
class Rational {
... // không chứa hàm operator*
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator()); } Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // tốt! result = 2 * oneFourth; // tốt! Điều này dĩ nhiên là một kết cục có hậu. Nhưng chúng ta vẫn còn một khúc mắc. Hàm operator* có nên là hàm friend của lớp Rational không? Trong trường hợp này, câu trả lời là không. Bởi vì hàm operator* có thể được hiện thực hoàn toàn dưới dạng giao tiếp công cộng của một lớp. Đoạn
mã bên trên chỉ ra một cách để làm điều đó. Bạn nên tránh dùng hàm friend bất cứ khi nào có thể. Tuy nhiên, việc một hàm không phải là thành viên, dù về mặc khái niệm nó vẫn là một phần của giao tiếp của lớp, cần phải truy xuất đến các thành phần không phải là công cộng của một lớp cũng không phải là điều bất bình thường.
Ví dụ, trong trường hợp của lớp String, nếu bạn cố overload các hàm operator>> và operator<< đểđọc và ghi các đối tượng String thì bạn sễ nhanh chóng phát hiện ra rằng chúng không nên là hàm thành viên. Nếu không bạn phải đặt đối tượng String bên trái khi bạn gọi hàm:
// một lớp khai báo không đúng hàm operator>> // và hàm operator<< - vì cho chúng là hàm thành viên
class String { public:
String(const char *value); ...
istream& operator>>(istream& input); ostream& operator<<(ostream& output); private:
char *data; };
String s;
s >> cin; // hợp lệ nhưng trái với quy ước thông thường
s << cout; // như trên
Điều đó sẽ làm mọi người bối rối. Như là một hệ quả, những hàm này không nên là hàm thành viên. Chú ý rằng đây là một trường hợp khác với trường hợp chúng ta vừa thảo luận bên trên. Ởđây, mục tiêu là làm phù hợp với cú pháp tự nhiên của hàm trong khi
trường hợp trên lại liên quan đến vấn đề chuyển kiểu ngầm.
Nếu bạn đang thiết kế những hàm như vậy, bạn sẽđối diện với trường hợp sau:
istream& operator>>(istream& input, String& string)
{delete [] string.data; delete [] string.data; đọc từ luồng nhập vào bộ nhớ và tạo string.data trỏđến nó return input; }
ostream& operator<<(ostream& output,
const String& string) {
return output << string.data; }
Chú ý rằng cả hai hàm đều cần truy xuất đến trường data của lớp String, một trường private. Tuy nhiên, bạn cũng đã biết rằng phải làm cho những hàm này là không thành viên. Vậy là bạn đã bị dồn vào chân tường và bạn không còn lựa chọn nào khác: một hàm không thành viên với khả năng truy xuất đến các thành phần không công cộng phải là một hàm friend của lớp đó.
Giả sử bạn có hàm f là một hàm bạn cần phải khai báo thế nào đó cho đúng và lớp C là lớp mà hàm đó có liên quan về mặt khái niệm thì những gì chúng ta vừa thảo luận có thể tóm tắt lại như sau:
· Hàm ảo phải là hàm thành viên: Nếu f phải là hàm ảo, làm cho nó trở thành thành viên của lớp C. · Hàm operator>> và operator<< không bao giờ
là hàm thành viên: nếu f là hàm operator>> hay operator>>, làm cho f là một hàm không thành viên. Nếu thêm vào đó, f phải truy xuất đến các
thành viên không công cộng của C, làm cho f là một hàm friend của lớp C.
· Chỉ có hàm không thành viên thực hiện chuyển kiểu trên đối số bên trái nhất của nó: nếu f phải chuyển kiểu trên đối số trái nhất, nó phải là hàm không thành viên. Nếu thêm vào đó, f phải truy xuất đến các thành viên không công cộng của C, f phải là một hàm friend của lớp C.
· Những thứ khác nên là hàm thành viên: nếu không có trường hợp nào ở trên là đúng vào trường hợp của hàm f thì nó nên là thành viên của lớp C.
Thời gian gần đây, nhất là sau khi học môn “Xây dựng phần mềm hướng đối tượng” các bạn sinh viên dấy lên phong trào dùng UserControl trong các projects môn học. “Nhà nhà dùng UserControl, người người dùng UserControl”, “UserControl mọi lúc, mọi nơi”. Ai cũng cố gắng đưa UserControl vào trong projects của mình. Với sự hỗ trợ của IDE, việc xây dựng một UserControl trên nền .NET khá dễ dàng. Nhưng để có được một UserControl “có chất lượng”, hay nói tổng quát hơn là một GUI Component “có chất lượng”, thì thật ra không dễ chút nào! Nó đòi hỏi chúng ta phải có kiến thức vững về lập trình hướng đối tượng và am tường phân tích thiết kế hướng đối tượng.
Tất cả bắt đầu vào những năm 70 của thế kỷ 20, tại phòng thí nghiệm Xerox PARC ở Palo Alto. Sự ra đời của giao diện đồ họa (Graphical User Interface) và lập trình hướng đối tượng (Object Oriented Programming) cho phép lập trình viên làm việc với những thành phần đồ họa như những đối tượng đồ họa có thuộc tính và phương thức riêng của nó. Không dừng lại ởđó, những nhà nghiên cứu ở Xerox PARC còn đi xa hơn khi cho ra đời cái gọi là kiến trúc MVC (viết tắt của Model – View – Controller).
Trong kiến trúc MVC, một đối tượng đồ họa (GUI Component) bao gồm 3 thành phần cơ bản: Model, View, và Controller. Model có trách nhiệm đối với toàn bộ dữ liệu cũng như trạng thái của đối tượng đồ họa. View chính là thể hiện trực quan của Model, hay nói cách khác chính là giao diện của đối tượng đồ họa. Và Controller điều khiển việc tương tác giữa đối tượng đồ họa với người sử dụng cũng như những đối tượng khác.
Khi người sử dụng hoặc những đối tượng khác cần thay đổi trạng thái của đối tượng đồ họa, nó sẽ tương tác thông qua Controller của đối tượng đồ họa. Controller sẽ thực hiện việc thay đổi trên Model. Khi có bất kỳ sự thay đổi nào ở xảy ra ở Model, nó sẽ phát thông điệp (broadcast message) thông báo cho View và Controller biết. Nhận được thông điệp từ Model, View sẽ cập nhật lại thể hiện của mình, đảm bảo rằng nó luôn là thể hiện trực quan chính xác của Model. Còn Controller, khi nhận được thông điệp từ Model, sẽ có những tương tác cần thiết phản hồi lại người sử dụng hoặc các đối tượng khác.
Lấy ví dụ một GUI Component đơn giản là Checkbox. Checkbox có thành phần Model để quản
Kiến trúc