Các nguyên lý cơ bản trong thiết kế hệ điều hành

Lớp Square được kế thừa từ lớp Rectangle, và do đặc điểm của hình vuông là hai cạnh bằng nhau, nên khi đặt chiều rộng thì chúng ta cũng phải đặt chiều dài và ngược lại. Bỏ qua lý do tốn bộ nhớ (do phải lưu cả chiều dài và chiều rộng), chúng ta xét về logic thực hiện chương trình. Chúng ta thử tạo ra hai đối tượng hình vuông và hình chữ nhật và gọi hàm f thao tác trên hai đối tượng này theo hàm main như sau:

doc19 trang | Chia sẻ: maiphuongdc | Lượt xem: 2061 | Lượt tải: 1download
Bạn đang xem nội dung tài liệu Các nguyên lý cơ bản trong thiết kế hệ điều hành, để tải tài liệu về máy bạn click vào nút DOWNLOAD ở trên
dùng, chúng ta chỉ tập trung đến thao tác vẽ (draw) của các đối tượng. Giả sử yêu cầu ban đầu của chương trình Draw là chỉ thao tác trên hai loại đối tượng là hình tròn và hình vuông. Sử dụng phương pháp lập trình cấu trúc (structured programming) (hay còn gọi phương pháp lập trình hướng thủ tục – procedural programming), chương trình phác thảo sơ lược sẽ có dạng như sau (sử dụng ngôn ngữ C++): PHP Code: enum ShapeType {circle, square}; struct Shape {   ShapeType itsType; }; struct Circle {   ShapeType itsType;   double itsRadius;   Point itsCenter; }; struct Square {   ShapeType itsType;   double itsSide;   Point itsTopLeft; }; // không cần quan tâm chi tiết đến cài đặt hai hàm này void DrawSquare(struct Square*); void DrawCircle(struct Circle*); typedef struct Shape *ShapePointer; void DrawAllShapes(ShapePointer list[], int n) {   int i;   for (i=0; iitsType)     {       case square:         DrawSquare((struct Square*)s);         break;       case circle:         DrawCircle((struct Circle*)s);         break;     }   } }  Đoạn mã ở trên có thể dễ dàng đọc hiểu. Trong đó hàm DrawAllShapes có nhiệm vụ vẽ các đối tượng hình học (hình tròn, hình vuông) ra màn hình. Tham số của hàm DrawAllShapes là một mảng các con trỏ chứa địa chỉ của các đối tượng hình học cần được vẽ. Để đạt được điều đó, hàm DrawAllShapes đến phiên nó lại cần sự trợ giúp của hai hàm vẽ cụ thể cho hai loại đối tượng hình học là hàm DrawCircle và DrawSquare. Để gọi được hai hàm này, hàm DrawAllShapes cần phải xác định đối tượng hiện tại đang thao tác là đối tượng nào thông qua biến thành viên itsType của từng đối tượng. Có vẻ chương trình Draw đã được hoàn thành và đúng với yêu cầu đề ra. Vấn đề sẽ xuất hiện khi chúng ta muốn vẽ thêm một đối tượng khác, như hình tam giác chẳng hạn. Lúc này, hàm DrawAllShapes cần phải xử lý thêm một trường hợp nữa là hình tam giác. Đoạn mã thêm vào và sửa đối sẽ như sau: PHP Code: enum ShapeType {circle, square, triangle};//kiểu dữ liệu liệu kê struct Triangle {   ShapeType itsType;   Point itsVertices[3]; }; // không cần quan tâm chi tiết đến cài đặt hàm này void DrawTriangle(struct Triangle*); void DrawAllShapes(ShapePointer list[], int n) {   int i;   for (i=0; iitsType)     {       case square:         DrawSquare((struct Square*)s);         break;       case circle:         DrawCircle((struct Circle*)s);         break;       // thêm vào       case triangle:         DrawTriangle((struct Triangle*)s);         break;     }   } }  Để ý trong trường hợp này, khi một yêu cầu mới phát sinh (vẽ hình tam giác), thì đoạn mã của hàm DrawAllShapes đã bị thay đổi. Bản thiết kế chương trình Draw của chúng ta đã vi phạm nguyên lý đóng mở. Vậy bản thiết kế chương trình Draw nên như thế nào? Hai kỹ thuật chính để đạt được nguyên lý Đóng - Mở là sự trừu tượng (abstraction) và tính đa hình (đa xạ : polymorphism). Các bạn có thể tự tìm hiểu hai kỹ thuật trừu tượng hóa và đa hình, vốn là hai kỹ thuật mà bất cứ một ngôn ngữ lập trình hướng đối tượng, bao gồm C++, phải hỗ trợ. Tôi không trình bày chi tiết hai kỹ thuật trên mà chỉ trình bày sơ lược theo ví dụ thiết kế Draw mà chúng ta đang hướng đến. Chương trình Draw ở trên có thể mô hình như sau: Nghĩa là hàm DrawAllShapes sử dụng trực tiếp (được thể hiện bằng đoạn thẳng có dấu mũi tên mảnh) hai đối tượng (hai lớp) Circle và Square, tương ứng là hình tròn và hình vuông. Chúng ta sẽ trừu tượng hóa quan hệ này bằng cách tạo ra một đối tượng gọi là hình (Shape). Một cách cảm tính chúng ta có thể thấy một đối tượng hình tròn hoặc hình vuông hoặc hình tam giác đều là một đối tượng hình. Hàm DrawAllShapes thay vì thao tác trực tiếp trên các đối tượng hình tròn và hình vuông sẽ thao tác trên các đối tượng hình chung chung mà chúng ta đã trừu tượng hóa. Mô hình chương trình Draw sẽ trở thành như sau: Các lớp Circle, Square sẽ được kế thừa (được thể hiện bằng đoạn thẳng có dấu mũi tên đậm) từ lớp Shape. Đoạn mã chương trình Draw cho mô hình thiết kế mới sẽ như sau: PHP Code: class Shape { public:     // hàm thuần ảo (pure virtual)         virtual void Draw() const=0; }; class Square : public Shape {         protected:                 double itsSide;                 Point itsTopLeft;     public:         // không cần quan tâm chi tiết cài đặt hàm này         virtual void Draw() const; }; class Circle : public Shape {         protected:                 double itsRadius;                 Point itsCenter;     public:         // không cần quan tâm chi tiết cài đặt hàm này         virtual void Draw() const; }; void DrawAllShape(set& list) {     for(iterator i(list); i; i++)         (*i) ->Draw(); };  Qua đoạn mã chương trình Draw mới, có thể thấy hàm DrawAllShapes không quan tâm chi tiết đến từng đối tượng hình cụ thể như là hình tròn hay hình vuông (không có câu lệnh if), mà nó chỉ quan tâm đến sự trừu tượng của các đối tượng hình này – Shape. Nhờ cơ chế đa hình (đa xạ) mà hàm Draw của lớp Shape sẽ được liên kết với hàm Draw của lớp Circle hoặc Square tùy thuộc vào đối tượng hiện tại thuộc lớp Circle hay Square. Trong đoạn chương trình trên cũng xuất hiện một khái niệm mà các bạn ít quen thuộc là iterator và set, các bạn có thể tự tìm hiểu thêm trong thư viện STL đi kèm với C++. Quay trở lại với chương trình Draw của chúng ta, nếu muốn chương trình vẽ thêm đối tượng tam giác thì chúng ta chỉ việc thêm vào lớp Triangle, được dẫn xuất (thừa kế) từ lớp Shape. PHP Code: class Shape {         public:     // hàm thuần ảo (pure virtual)         virtual void Draw() const=0; }; class Square : public Shape {         protected:                 double itsSide;                 Point itsTopLeft;     public:         // không cần quan tâm chi tiết cài đặt hàm này         virtual void Draw() const; }; class Circle : public Shape {         protected:                 double itsRadius;                 Point itsCenter;     public:         // không cần quan tâm chi tiết cài đặt hàm này         virtual void Draw() const; }; class Triangle : public Shape {         protected:                 Point vertices[3];     public:         // không cần quan tâm chi tiết cài đặt hàm này         virtual void Draw() const; }; void DrawAllShape(set& list) {     for(iterator i(list); i; i++)         (*i) ->Draw(); };  Có thể thấy, chúng ta chỉ cần thêm mới vào lớp Triangle, hàm DrawAllShapes, cũng như tất cả các thành phần đoạn mã đã có của chương trình Draw, không hề thay đổi. Bản thiết kế mới của chương trình Draw thỏa mãn nguyên lý Đóng – Mở. Open-closed là nguyên li trung tâm, rất quan trọng trong thiết kế hướng đối tượng vì chính nguyên lí này làm cho lập trình hướng đối tượng có tính tái sử dụng (reusability) và dễ bảo trì (maintainability). Tham khảo thêm ở đây và ở đây. Như đã đề cập ở trên hai kỹ thuật quan trọng để đạt được nguyên lý đóng mở là trừu tượng hóa và tính đa hình. Trong C++, tính đa hình được thể hiện thông qua sự thừa kế (inheritance). Vậy khi nào thì một lớp A nào đó nên được thừa kế từ lớp B đã có? Nguyên lý thay thế Liskov Nếu xem nguyên lý Mở - Đóng là nguyên lý cơ sở quan trọng nhất của lập trình và thiết kết theo hướng đối tượng, thì nguyên lý Thay thế Liskov là một phương tiện để chúng ta kiểm tra xem chương trình hoặc bản thiết kế của chúng ta có thoả nguyên lý Mở - Đóng hay không. Nếu nguyên lí này bị vi phạm, function có sử dụng reference hay pointer tới object của lớp cha phải kiểm tra kiểu của object để đảm bảo chương trình có thể chạy đúng, và việc này vi phạm nguyên lí open-closed nhắc đến ở trên. Tham khảo thêm ở đây và ở đây. Trước khi đi vào nguyên lý chúng ta xét một chương trình ví dụ, cũng liên quan đến các đối tượng hình vẽ như chúng ta đã đề cập trong chương trình Draw, nhưng được giản lược đi nhiều chỉ để đủ cho việc minh họa nguyên lý Thay thế Liskov. Cụ thể chúng ta xét lớp Rectangle mô tả đối tượng hình chữ nhật và một hàm f thao tác trên đối tượng lớp Rectangle. PHP Code: class Rectangle { public:   void SetWidth(double w) {itsWidth=w;}   void SetHeight(double h) {itsHeight=w;}   double GetHeight() const {return itsHeight;}   double GetWidth() const {return itsWidth;} private:   double itsWidth;   double itsHeight; }; // hàm thao tác trên đối tượng Rectangle& void f(Rectangle& r) {      r.SetWidth(32); }  Đoạn mã đã giải thích rõ ràng công dụng của lớp Rectangle, cũng như của hàm f. Phát biểu nguyên lý: Các hàm mà sử dụng con trỏ hoặc tham chiếu đến các (đối tượng) lớp cơ sở cũng phải có thể sử dụng các đối tượng của các lớp dẫn xuất mà không cần biết chúng. Nguyên văn tiếng Anh: FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES WITHOUT KNOWING IT. Để hiểu nguyên lý này chúng ta thử xét những ví dụ vi phạm. Giả sử chương trình Draw mở rộng thao tác trên những đối tượng hình vuông, lớp Square. Theo bạn, chúng ta nên tạo mới lớp hình vuông hay kế thừa từ lớp Rectangle. Để trả lời câu hỏi này thì theo như phần lớn các bạn đã được học, các bạn cần trả lời câu hỏi là “Một đối tượng hình vuông có phải là một (IS A) đối tượng hình chữ nhật hay không?” Nếu câu trả lời là có, thì lớp hình vuông là lớp kế thừa từ lớp hình chữ nhật và ngược lại. Trong trường hợp này, dĩ nhiên câu trả lời là có. Vậy chúng ta sẽ cho lớp hình vuông kế thừa từ lớp hình chữ nhật và xem điều gì sẽ xảy ra. Đoạn mã của lớp Square có dạng như sau: PHP Code: class Square: public Rectangle { public:   void Square::SetWidth(double w)   {     Rectangle::SetWidth(w);     Rectangle::SetHeight(w);   }   void Square::SetHeight(double h)   {     Rectangle::SetHeight(h);     Rectangle::SetWidth(h);   } };  Lớp Square được kế thừa từ lớp Rectangle, và do đặc điểm của hình vuông là hai cạnh bằng nhau, nên khi đặt chiều rộng thì chúng ta cũng phải đặt chiều dài và ngược lại. Bỏ qua lý do tốn bộ nhớ (do phải lưu cả chiều dài và chiều rộng), chúng ta xét về logic thực hiện chương trình. Chúng ta thử tạo ra hai đối tượng hình vuông và hình chữ nhật và gọi hàm f thao tác trên hai đối tượng này theo hàm main như sau: PHP Code: int main() {   Rectangle r;   Square s;   f(r); // thực hiện đúng   f(s); // thực hiện sai vì hàm SetWidth là hàm của hình chữ nhật   …   return 0; }  Nếu chúng ta truyền vào một đối tượng Rectangle (r) thì hàm f thực hiện đúng như mong đợi. Nhưng nếu chúng ta truyền vào một đối tượng Square (s) thì hàm f thực hiện sai vì câu lệnh r.SetWidth(32) sẽ gọi hàm SetWidth của lớp Rectangle và do đó gây ra vi phạm ràng buộc là chiều dài và chiều rộng của đối tượng s phải bằng nhau. Trong trường hợp này, hàm f đã vi phạm nguyên lý Thay thế Liskov. Nó họat động tốt trên đối tượng truyền vào thuộc lớp cơ sở (lớp Rectanlge) nhưng không họat động tốt trên đối tượng truyền vào thuộc lớp dẫn xuất (lớp Square). Giải pháp khắc phục rất đơn giản chúng ta sẽ thay đổi hai hàm thuộc lớp Rectangle thành hàm ảo (virtual function) và sử dụng cơ chế đa xạ. Để ý rằng, khi chương trình Draw vi phạm nguyên lý Thay thế Liskov thì nó cũng vi phạm nguyên lý Mở - Đóng (vì phải chỉnh sửa đoạn mã các thực thể đã có). PHP Code: class Rectangle{ public:    // đổi thành hàm ảo (virtual)    virtual void SetWidth(double w)       {itsWidth=w;}    // đổi thành hàm ảo (virtual)    virtual void SetHeight(double h)       {itsHeight=h;}    double GetHeight() const       {return itsHeight;}    double GetWidth() const       {return itsWidth;} private:    double itsHeight;    double itsWidth; }; class Square: public Rectangle { public:   void Square::SetWidth(double w)   {     Rectangle::SetWidth(w);     Rectangle::SetHeight(w);   }   void Square::SetHeight(double h)   {     Rectangle::SetHeight(h);     Rectangle::SetWidth(h);   } };  Vấn đề đã được giải quyết xong, hàm f bây giờ có thể hoạt động tốt cho cả đối tượng truyền vào thuộc lớp Rectangle lẫn đối tượng truyền vào thuộc lớp Square. Chương trình bây giờ thỏa mãn nguyên lý Thay thế Liskov. Để minh họa tiếp tục, chúng ta xét hàm g như sau. Hàm g có vai trò như một hàm test bảo đảm rằng thao tác thực hiện hai hàm SetWidth và SetHeight của lớp Rectangle phải hoàn toàn đúng đắn. PHP Code: void g(Rectangle& r) {   r.SetWidth(5);   r.SetHeight(4);   assert(r.GetWidth()*r.GetHeight())==20); }  Dễ thấy rằng, hàm g hoạt động tốt nếu chúng ta truyền vào một đối tượng Rectangle (r), assert thành công, nhưng sẽ không hoạt động tốt nếu chúng ta truyền vào một đối tượng Square (s), assert không thành công. (Các bạn tham khảo thêm hàm assert, nó được dùng chủ yếu cho mục đích debug và test chương trình). Trong trường hợp này, chúng ta kết luận chương trình không thỏa mãn nguyên lý Thay thế Liskov. Vì hàm g hoạt động tốt trên các đối tượng lớp cơ sở (Rectangle) nhưng không hoạt động tốt trên các đối tượng lớp dẫn xuất (Square). Vậy nguyên nhân là do đâu? Lý do chính ở đây, là lớp Square không nên kế thừa từ lớp Rectangle. Và việc trả lời cho câu hỏi: “Đối tượng hình vuông có phải là một đối tượng hình chữ nhật hay không?” cho đáp án là “có” chỉ là điều kiện cần cho việc quyết định lớp Square (hình vuông) có nên kế thừa từ lớp Rectangle (hình chữ nhật) hay không. Điều kiện đủ cần phải xét là nó có thỏa nguyên lý Liskov hay không. Lưu ý rằng việc bảo đảm nguyên lý Thay thế Liskov cho mọi hàm, mọi thực thể trong phần mềm là rất khó. Tuy nhiên việc cố gắng thực hiện đúng theo nguyên lý Thay thế Liskov sẽ giúp ích cho việc mở rộng và bảo trì phần mềm. Bởi vì nếu vi phạm nguyên lý Thay thế Liskov thì tất yếu sẽ vi phạm nguyên lý Mở - Đóng (cụ thể là tính Đóng). Nguyên lý đảo phụ thuộc (Dependency Inversion Principle) Phát biểu nguyên lý: A. Các đơn thể cấp cao không nên phụ thuộc vào các đơn thể cấp thấp. Cả hai nên phụ thuộc vào những cái trừu tượng. B. Cái trừu tượng không nên phụ thuộc vào cái chi tiết. Cái chi tiết nên phụ thuộc vào cái trừu tượng. Nguyên văn tiếng Anh: A. HIGH LEVEL MODULES SHOULD NOT DEPEND UPON LOW LEVEL MODULES. BOTH SHOULD DEPEND UPON ABSTRACTIONS. B. ABSTRACTIONS SHOULD NOT DEPEND UPON DETAILS. DETAILS SHOULD DEPEND UPON ABSTRACTIONS. Thực hiện một bằng cách dùng abstract layer như hình dưới. Tham khảo thêm ở chỗ này, chỗ này và chỗ này. Để làm rõ nguyên lý Đảo Phụ thuộc chúng ta xét một ví dụ đơn giản sau: giả sử chúng ta cần viết một chương trình nhằm đọc các ký tự được nhập từ bàn phím sau đó xuất ra máy in. Dễ dàng thấy rằng, chương trình của chúng ta cần ba chức năng tương ứng với ba hàm: ReadKeyboard để đọc một ký tự từ bàn phím; WritePrinter để xuất ký tự ra máy in; và hàm Copy để kết hợp hai hàm trên lại được chức năng như chương trình mong muốn. Thiết kế và đoạn mã chương trình như sau: PHP Code: // không quan tâm chi tiết cài đặt hai hàm này int ReadKeyboard(); void WritePrinter(int c); void Copy() {     int c;     while (c = ReadKeyboard()) != EOF)         WritePrinter(c); }  Đoạn mã chương trình ngắn gọn và dễ dàng đọc hiểu. Lưu ý rằng chúng ta bỏ qua chi tiết cài đặt của hai hàm ReadKeyboard và WritePrinter. Có vẻ chương trình đã được hoàn thành một cách tốt đẹp. Phương pháp mà chúng ta đang áp dụng để thiết kế ra chương trình này gọi là phương pháp top-down, thường được áp dụng trong lập trình cấu trúc (structured programming) – hay còn gọi là lập trình hướng thủ tục (procedural programming). Các bạn có thể tự tìm hiểu phương pháp thiết kế top-down. Chúng ta sẽ không đi chi tiết về nó. Chỉ nhắc lại ý tưởng chính của nó là chia để trị, chia chức năng cần hoàn thành thành các chức năng nhỏ hơn và tiếp tục cho đến khi nó đủ nhỏ để có thể dễ lập trình và dễ kiểm soát. Để ý hơn nữa, các bạn có thể thấy hàm copy sử dụng hai hàm ReadKeyboard và WritePrinter. Chúng ta nói hàm Copy ở cấp cao, còn hai hàm ReadKeyboard và WritePrinter ở cấp thấp. Giả sử yêu cầu của chương trình được thay đổi, chương trình được yêu cầu đọc ký tự từ bàn phím và xuất ra hoặc máy in hoặc đĩa cứng (tập tin). Đoạn mã được thay đổi như sau để phù hợp với yêu cầu của chương trình. PHP Code: // không quan tâm chi tiết cài đặt ba hàm này int ReadKeyboard(); void WritePrinter(int c); void WriteDisk(int c); // hàm copy có thay đổi enum outputDevice {printer, disk}; void Copy(outputDevice dev) {     int c;     while (c = ReadKeyboard()) != EOF)         if (dev == printer)             WritePrinter(c);         else             WriteDisk(c); }  Ở đây xuất hiện một vấn đề chức năng hàm Copy phải được thay đổi để phù hợp với yêu cầu mới. Lý do hàm Copy, hàm cấp cao, đã bị phụ thuộc vào các hàm ReadKeyboard, WritePrinter, và WriteDisk, vốn là các hàm cấp thấp. Thiết kế chương trình của chúng ta đã bị vi phạm nguyên lý Đảo Phụ thuộc (xem vế A của nguyên lý). Để sửa chữa chúng ta phải để các hàm cấp cao không phụ thuộc vào các hàm cấp thấp, mà phải để cả các hàm cấp cao (Copy) và các hàm cấp thấp (ReadKeyboard, WritePrinter, và WriteDisk) phụ thuộc vào những cái trừu tượng. Thiết kế chương trình và đoạn mã chương trình được sửa đổi như sau: PHP Code: class Reader { public:   virtual int Read() = 0; }; class KeyboardReader: public Reader { public:   // không quan tâm đến chi tiết cài đặt   virtual int Read(); }; class Writer { public:   virtual void Write(char) = 0; }; class PrinterWriter: public Writer { public:   // không quan tâm đến chi tiết cài đặt   virtual void Write(char); }; void Copy(Reader& r, Writer& w) {   int c;   while((c=r.Read()) != EOF)     w.Write(c); }  Để ý với bản thiết kế mới của chương trình, đơn thể cấp cao (hàm Copy) không phụ thuộc và các đơn thể cấp thấp (lớp KeyboardReader, PrinterWriter). Tất cả chúng phụ thuộc và những cái trừu tượng (lớp trừu tượng Reader và Writer). Hàm main() của chương trình chính nếu có sẽ có dạng như sau: PHP Code: int main() {     KeyboardReader keyboard;     PrinterWriter printer;     Copy(keyboard, printer);     return 0; }  Giả sử chương trình yêu cầu thay vì xuất ra máy in thì xuất ra đĩa cứng (tập tin). Bản thiết kế và đoạn mã chương trình sẽ thêm vào lớp DiskWriter, dẫn xuất từ lớp Writer. Hàm Copy và các lớp khác không hề thay đổi. Nguyên lý Đóng – Mở đã được bảo đảm. Hoặc thay vì đọc từ bàn phím thì đọc từ các thiết bị khác như đĩa thì sự thay đổi đơn thuần chỉ là sự thêm vào các lớp cấp thấp mới. Bản thiết kế này cũng giải thích vế B trong phát biểu nguyên lý: “Cái trừu tượng không phụ thuộc vào cái chi tiết. Cái chi tiết phải phụ thuộc vào cái trừu tượng.” Rõ ràng, khi lớp trừu tượng Reader thay đổi thì các lớp dẫn xuất (lớp con) từ nó phải thay đổi nhưng chiều ngược lại thì không. Tóm lại nếu như nguyên lý Đóng – Mở đưa ra mục tiêu thì nguyên lý thay thế Liskov là một phương tiện để kiểm tra mục tiêu đó có đạt được hay không và nguyên lý Đảo Phụ thuộc là một phương tiện để đạt được mục tiêu đó. Nguyên lý chia tách giao diện (Interface Segregation Principle) Trong 3 nguyên lý trước, kỹ thuật trừu tượng hóa với sự xuất hiện của khái niệm lớp trừu tượng đã xuất hiện rất nhiều và giúp cho chương trình thỏa mãn nguyên lý Mở - Đóng. Nguyên lý chia tách giao diện (Interface Segragation Principle) sẽ đóng vai trò định hướng trong việc thiết kế các lớp trừu tượng này. Phát biểu nguyên lý: Không nên buộc các thực thể (phần mềm) khách phụ thuộc vào các giao diện mà chúng không hề sử dụng. Nguyên văn tiếng Anh: CLIENTS SHOULD NOT BE FORCED TO DEPEND UPON INTERFACES THAT THEY DO NOT USE. Khi một client bị ép phải phụ thuộc vào những interface mà nó không sử dụng thì nó sẽ bị lệ thuộc vào những thay đổi của interface đó. Chúng ta cần phải tránh điều này nhiều nhất có thể bằng cách chia nhỏ interface. Tham khảo thêm ở đây. Trước hết chúng ta đi làm sáng tỏ một khái niệm mới xuất hiện là giao diện (interface). Xét lại ví dụ chương trình vẽ hình Draw ở nguyên lý Mở - Đóng mà chúng ta đã dùng. Để cho dễ minh họa khái niệm giao diện, tôi sẽ thêm vào một chức năng trong chương trình Draw ngoài chức năng vẽ hình là chức năng tịnh tiến một hình theo một vector cho trước. Đây là hai chức năng cơ bản trong các chương trình đồ họa vector. Đồng thời chúng ta sẽ thêm vào một đối tượng hình học được thao tác nữa là đối tượng đoạn thẳng (Line). Lớp Line sẽ được thừa kế từ lớp Shape chúng ta đã có. Thiết kế chương trình và mã nguồn các lớp sẽ có dạng như sau: PHP Code: class Shape { public:     // hàm thuần ảo (pure virtual)     virtual void Draw() const=0;     virtual void Transfer(double dx, double dy) = 0; }; class Square : public Shape { protected:         double itsSide;         Point itsTopLeft; public:     // không cần quan tâm chi tiết cài đặt các hàm này     virtual void Draw() const;     virtual void Transfer(double dx, double dy); }; class Circle : public Shape { protected:         double itsRadius;         Point itsCenter; public:     // không cần quan tâm chi tiết cài đặt các hàm này     virtual void Draw() const;     virtual void Transfer(double dx, double dy); }; class Line : public Shape { protected:         Point itsStartPoint, itsEndPoint; public:     // không cần quan tâm chi tiết cài đặt các hàm này     virtual void Draw() const;     virtual void Transfer(double dx, double dy); };  Câu hỏi đặt ra rất đơn giản: ý nghĩa của “lớp” Shape – “lớp” Hình là gì? Chúng ta vẫn gọi nó là lớp trừu tượng. Thật sự đó là một cách nói khỏa lấp. Lớp và đối tượng là hai khái niệm đi kèm nhau và liên quan chặt chẽ với nhau. Lớp dùng để tạo ra đối tượng, ngược lại nếu không phải vậy nó không phải là lớp. Shape không phải là một lớp, bởi nó không có khả năng tạo ra các đối tượng. Shape được gọi là một giao diện (interface). Một cách nôm na: giao diện là tập hợp các thành phần (thường là hàm) của một đối tượng mà các đối tượng khác có thể thấy. Một công thức mà các bạn thường hay gặp trong các bài giảng về lớp (đối tượng) là: Đối tượng = các hàm + các biến hay đối tượng = các phương thức + các dữ liệu. Những “các hàm” hay “các phương thức” ở đây chính là giao diện của đối tượng. Rất tiếc trong C++ (không như các ngôn ngữ hiện đại hơn như Java hay .NET) không có khái niệm giao diện một cách trực tiếp, mà nó được biểu diễn thông qua khái niệm lớp, gọi là lớp trừu tượng. Do đó khi chúng ta tạo ra một lớp trừu tượng thì chúng ta sẽ gọi nó là tạo ra một giao diện. Trong ví dụ trên chúng ta có giao diện Shape gồm 2 chức năng: Draw - vẽ và Transfer - tịnh tiến. Lớp Circle, Square và Line được gọi là sử dụng (hay thực hiện) giao diện Shape. Lưu ý rằng khái niệm giao diện = lớp trừu tượng được sử dụng rộng rãi trong các ngôn ngữ lập trình như Java hay .NET. Để minh họa sự vi phạm và hậu quả của sự vi phạm nguyên lý Chia tách Giao diện, chúng ta hãy thêm vào chương trình Draw chức năng tô màu – Fill. Một cách đơn giản và có tính minh họa, chúng ta sẽ đưa hàm Fill vào trong giao diện Shape. PHP Code: enum ColorType {red, green, blue}; enum PatternType {solid, vertical, horizontal}; class Shape { public:     // hàm thuần ảo (pure virtual)     virtual void Draw() const=0;     virtual void Transfer(double dx, double dy) = 0;     virtual void Fill(ColorType color, PatternType pattern) = 0; }; class Square : public Shape { protected:         double itsSide;         Point itsTopLeft; public:     // không cần quan tâm chi tiết cài đặt các hàm này     virtual void Draw() const;     virtual void Transfer(double dx, double dy);     virtual void Fill(ColorType color, PatternType pattern); }; class Circle : public Shape { protected:         double itsRadius;         Point itsCenter; public:     // không cần quan tâm chi tiết cài đặt các hàm này     virtual void Draw() const;     virtual void Transfer(double dx, double dy);     virtual void Fill(ColorType color, PatternType pattern); }; class Line : public Shape { protected:         Point itsStartPoint, itsEndPoint; public:     // không cần quan tâm chi tiết cài đặt các hàm này     virtual void Draw() const;     virtual void Transfer(double dx, double dy);     // cài đặt hàm Fill như thế nào     virtual void Fill(ColorType color, PatternType pattern); };  Hàm Fill có hai tham số chỉ ra màu dùng để tô và mẫu dùng để tô (đặc, dọc, ngang,….). Hai kiểu ColorType và PatternType chỉ có tính minh họa. Một cách tự nhiên, các lớp sử dụng giao diện Shape sẽ phải thực hiện (implement) hay định nghĩa các chức năng được mô tả trong giao diện Shape, trong đó có chức năng Fill. Với hai lớp Circle và Square, điều này là rõ ràng và có ý nghĩa. Nhưng đối với Line thì chức năng Fill sẽ làm gì? Ở đây xuất hiện một hiện tượng mà chúng ta gọi là “giao diện bị ô nhiễm” (polluted interface). Chúng ta đã bắt buộc lớp Line phải định nghĩa (hay phụ thuộc) vào hàm Fill mà nó không hề muốn sử dụng. Nguyên lý Chia tách Giao diện đã bị vi phạm. Vậy thiết kế phần mềm nên được thay đổi như thế nào? Chúng ta sẽ chia tách giao diện Shape thành các giao diện khác và bảo đảm không có lớp nào bắt buộc phải sử dụng các giao diện mà chúng không mong muốn. Giao diện Shape được tách ra thành hai giao diện: giao diện Shape mới, dành cho các các đối tượng hình vẽ không tô được, chứa hàm Draw và Transfer và FilledShape chứa hàm Fill kế thừa từ giao diện Shape, dành cho các đối tượng hình vẽ có tô được và. (Lẽ ra nên có 3 giao diện UnfilledShape thì có lý hơn. Tuy nhiên với ví dụ này chỉ cần chia tách giao diện Shape thành 2 giao diện mới

Các file đính kèm theo tài liệu này:

  • doccac_nguyen_ly_co_ban_trong_thiet_ke_hdt.doc
Tài liệu liên quan