Tài liệu công nghệ thông tin - Các nguyên lý cơ bản trong thiết kế HĐT - Pdf 18

Các nguyên lý cơ bản trong thiết kế HĐT
Các nguyên lý cơ bản trong thiết kế HĐT (basic object-oriented principles)
1.Vai trò của thiết kế
Thiết kế là 1 công đoạn quan trọng trong qui trình phát triển phần mềm.
Thiết kế là bước chuyển tiếp của giai đoạn phân tích và là bước chuẩn bị trước khi chúng ta tiến
hành xây dựng phần mềm.
Thiết kế là tiến trình mà ở đó xuất hiện mô hình các kiểu mẫu của phần mềm. Các mô hình này
chính là những nét phác thảo nên phần mềm. Nó cho chúng ta biết phần mềm chúng ta đang xây
dựng là gì, đã có, đang có và sẽ có những gì.
Thiết kế là nơi mà ta có thể trả lời câu hỏi “Liệu phần mềm này có thể chạy được không?” ,
“Phần mềm có thể đáp ứng được các yêu cầu của khách hàng hay không?” mà không cần đợi đến
công đoạn phát triển.
2.Các nguyên lý thiết kế hướng đối tượng
- Nguyên lý ‘đóng mở’: một moudle cần “mở” đối với việc phát triển thêm tính năng nhưng phải
“đóng” đối với việc sửa đổi mã nguồn
- Nguyên lý thay thế Liskov: Các chức năng của hệ thống vẫn thực hiện đúng đắn nếu ta htay bất
kì một lớp đối tượng nào bằng đối tượng kế thừa.
- Nguyên lý nghịch đảo phụ thuộc: phụ thuộc vào mức trừu tượng, không phụ thuộc vào mức chi
tiết.
- Nguyên lý phân tách giao diện: nên có nhiều giao diện đặc thù với bên ngoài hơn là chỉ có một
giao diện dùng chung cho một mục đích.
Theo tác giả thì mọi nguyên lý trong lập trình hướng đối tượng đều quy vào một nguyên lý duy
nhất là nguyên lý đóng mở (Open-Closed Principle). Do đó đầu tiên sẽ giới thiệu với các bạn về
nguyên lý đóng mở. Các nguyên lý sau sẽ làm rõ hơn làm cách nào để đạt được yêu cầu như
nguyên lý đóng mở đề ra.
Phát biểu nguyên lý Đóng - Mở:
“Các thực thể phần mềm (lớp, đơn thể, hàm, …) nên (được xây dựng theo hướng) mở cho
việc mở rộng và đóng cho việc sửa đổi”.
Nguyên văn tiếng Anh:
“SOFTWARE ENTITIES(CLASSES,MODULES,FUNCTIONS,ETC.)SHOULD BE OPEN
FOR EXTENSION, BUT CLOSED FOR MODIFICATION.”

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; i<n; i++)
{
struct Shape* s = list[i];
switch (s->itsType)
{
case square:
DrawSquare((struct Square*)s);
break;
case circle:
DrawCircle((struct Circle*)s);
break;
}
}

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:

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;

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.

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)

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.

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:

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.

{
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

{
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

// 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 FilledShape
{
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);
};
Đoạn mã chương trình thể hiện những điều đã nói ở trên nên không cần phải giải thích. Bản thiết
kế này thỏa nguyên lý Chia tách Giao diện.
Các bạn có thể đặt câu hỏi nếu vi phạm nguyên lý Chia tách Giao diện thì sẽ gây ra hậu quả gì
ảnh hưởng đến phần mềm? Có hai hậu quả chính:
1) xét giao diện Shape như cũ (tức có 3 chức năng: Draw, Transfer và Fill) bị ô nhiễm thì dễ


Nhờ tải bản gốc

Tài liệu, ebook tham khảo khác

Music ♫

Copyright: Tài liệu đại học © DMCA.com Protection Status