Nếu nguyên lý đóng mở đƣợc xem là nguyên lý quan trọng nhất của thiết kế hướng đối tượng, thì nguyên lý thay thế Liskov được xem như một phương tiện để chúng ta có thể kiểm tra xem thiết kế hoặc chương trình có thỏa mãn nguyên lý đóng mở hay không.
Các thành phần phần mềm không nên phụ thuộc vào những cái riêng, cụ thể, mà ngƣợc lại nên phụ thuộc vào những cái chung, tổng quát của những cái riêng cụ thể đó.
Ngƣợc lại, những cái chung, tổng quát không nên phụ thuộc vào những cái riêng, cụ thể. Sự phụ thuộc này nên đảo ngƣợc lại.
Cái chung, tổng quát bao gồm những đặc tính chung nhất của những cái riêng, cụ thể. Còn những cái riêng, cụ thể thì tuân theo những quy tắc mà cái chung, tổng quát của nó định nghĩa. Những cái chung, tổng quát thường ít biến đổi. Sự thay đổi thường diễn ra ở những cái riêng, cụ thể. Do đó sự phụ thuộc vào những cái chung, tổng quát sẽ giúp các thành phần phần mềm linh động và thích ứng tốt hơn với các thay đổi. Khi phụ thuộc vào những cái chung, tổng quát, các thành phần phần mềm sẽ không bị ảnh hưởng và không cần sửa đổi để thích nghi bởi việc thay thế cái riêng, cụ thể bằng một cái riêng, cụ thể khác.
Xét đoạn chương trình đọc dữ liệu từ bàn phím và xuất ra máy in:
public void copy(){
Keyboard keyboard = new Keyboard();
Printer printer = new Printer();
char c;
while ((c = keyboard.read()) != „q‟) printer.write(c);
}
Khi nâng cấp, mở rộng đoạn chương trình trên để nó có thể xuất dữ liệu ra máy in hoặc tập tin, ta phải điều chỉnh lại nhƣ sau:
public void copy(OutputType type){
Keyboard objKeyboard = new Keyboard();
Printer objPrinter = new Printer();
File objFile = new File();
char c;
while ((c = objKeyboard.read()) != „q‟) if (type == OutputType.PRINTER)
objPrinter.write(c);
else if (type == OutputType.FILE) objFile.write(c);
}
Hàm copy trên đã vi phạm nguyên lý đóng mở. Vì khi thêm một thiết bị mới, ta phải chỉnh lại nó. Nguyên nhân của việc này là vì đã làm việc với các thiết bị đọc ghi cụ thể. Khi thêm một thiết bị mới, ta phải thêm vào đoạn mã nội dung làm việc với thiết bị mới đó. Khi đó người ta nói rằng, hàm copy đã vi phạm nguyên lý nghịch đảo phụ thuộc.
Để đoạn chương trình trên có thể tuân theo nguyen lý nghịch đảo phụ thuộc, ta phải chỉnh sửa cho nó làm việc với thiết bị tổng quát. Với đoạn mã chương trình trên, ta có thể chỉnh sửa lại nhƣ sau:
public void copy(Reader objReader, Writer objWriter){
char c;
while ((c = objReader.read()) != „q‟) objWriter.write(c);
}
Một số chú ý khi tuân theo nguyên lý nghịch đảo phụ thuộc:
Nguyên lý nghịch đảo phụ thuộc có mối liên hệ chặt chẽ với nguyên lý đóng mở. Việc vi phạm nguyên lý nghịch đảo phụ thuộc sẽ dẫn đến vi phạm nguyên lý đóng mở. Khi nguyên lý nghịch đảo phụ thuộc bị vi phạm, tức các thành phần phần mềm đã phụ thuộc vào những cái riêng, cụ thể. Do đó, việc nâng cấp, mở rộng sẽ dẫn đến các thành phần phần mềm mà phụ thuộc vào nó sẽ yêu cầu đƣợc sửa đổi, thay đổi. Điều này là đã vi phạm nguyên lý đóng mở.
Một đặc trưng của thiết kế hướng đối tượng mà giúp cho các thành phần phần mềm tăng khả năng tuân thủ nguyên lý nghịch đảo phụ thuộc, đó là việc truy xuất đối tƣợng thông qua giao diện của nó. Việc truy xuất qua giao diện sẽ làm cho các thành phần có tính linh động cao hơn, không phải sửa đổi khi ta thay thế
năng của đối tƣợng này. Khi đó các đối tƣợng của lớp thừa kế sẽ có những hành vi tương tự như các đối tượng của lớp cơ sở. Do đó, các đối tượng của lớp thừa kế có thể thay thế các đối tƣợng của lớp cơ sở để thực hiện các thao tác trên các đối tƣợng của lớp cơ sở.
Vì lý do trên mà ta không nên sử dụng tính chất thừa kế một cách tùy ý. Giả sử ta có lớp A và phương thức F. Khi nâng cấp, mở rộng phần mềm, ta thêm vào một lớp mới, lớp B thừa kế lớp A. Tuy nhiên, việc thay thế đối tƣợng lớp A bằng đối tƣợng lớp B sẽ làm cho phương thức F thực hiện những ứng xử sai khác so với trước khi thực hiện thay thế. Do đó, ta phải chỉnh sửa lại hàm F để đảm bảo cách ứng xử không thay đổi. Điều này dẫn đến việc vi phạm nguyên lý đóng mở.
Ví dụ: ta hãy xem xét ví dụ sau để thấy đƣợc những hậu quả của việc thừa kế tùy tiện:
public class Stack{
private ArrayList data;
public virtual void push(int n){
// Đưa số nguyên n vào đỉnh ngăn xếp.
}
public virtual int pop(){
// Lấy giá trị ở đỉnh ngăn xếp.
} }
public class Queue: Stack{
public override void push(int n){
// Thêm số nguyên n vào hàng đợi.
}
public override int pop(){
// Lấy số nguyên từ hàng đợi.
} }
public int function1(Stack p){
p.push(2);
p.push(3);
p.push(4);
int a = p.pop();
int b = p.pop();
if (a == 4 && b == 3)
return a * b;
throw new ArgumentException();
}
Để có thể sử dụng lại một số thuộc tính và phương thức trong lớp Stack, ta cho lớp Queue thừa kế lớp Stack. Khi đó ta có thể truyền đối tƣợng Queue vào hàm function1. Tuy nhiên, cách cƣ xử của hàm này trên các đối tƣợng của lớp Stack và lớp Queue là khác nhau. Với đối tƣợng của lớp Stack, hàm function1 sẽ trả về tích của 2 số 4 và 3. Nếu đối tƣợng truyền vào là đối tƣợng của lớp Queue, hàm sẽ gây ra một ngoại lệ. Do đó ta phải chỉnh sửa lại hàm function1 để đảm bảo tính cƣ xử nhất quán.
Điều này dẫn đến việc vi phạm nguyên lý đóng mở. Khi đó ta nói, hàm function1 vi phạm nguyên lý thay thế Liskov.
Một số chú ý khi xác định nguyên lý thay thế Liskov:
Nguyên lý thay thế Liskov có mối liên hệ chặt chẽ với nguyên lý đóng mở. Khi một thực thể phần mềm vi phạm nguyên lý thay thế Liskov, nó sẽ có những hành vi khác nhau trên các lớp cơ sở và lớp dẫn xuất. Để nó có thể có những thao tác đúng trên cả lớp cơ sở và lớp dẫn xuất, ta phải chỉnh sửa lại nó. Điều này dẫn đến việc vi phạm nguyên lý đóng mở.
Việc tuân thủ nguyên lý thay thế Liskov có tính chất tương đối. Trong tình huống này, thực thể xem xét thỏa mãn nguyên lý, nhƣng trong tình huống cụ thể khác, nó lại không còn tuân thủ nguyên lý nữa. Tuy nhiên, khi thực hiện phân tích thiết kế hướng đối tượng, ta phải làm cho số lượng thực thể mà tuân thủ theo nguyên lý là nhiều nhất trong các tình huống thường xảy ra, đặc biệt cho các thực thể hay đƣợc nâng cấp, mở rộng.
2.1.4. Nguyên lý Phân tách giao diện
Không nên để các thực thể phần mềm phụ thuộc vào những giao diện mà chúng không sử dụng đến.
Khi xây dựng một lớp, nhất là lớp trừu tượng, người ta thường có xu hướng đưa thật nhiều các thuộc tính và phương thức vào lớp đó. Những lớp như vậy được gọi là lớp có giao diện bị ô nhiễm. Khi một lớp có giao diện bị ô nhiễm, nó sẽ trở lên phức tạp. Một thực thể khi muốn thực hiện một số việc đơn giản, nó buộc phải thực hiện với toàn bộ giao diện của lớp đó. Một lớp trừu tƣợng có giao diện bị ô nhiễm, khi một lớp muốn thừa kế nó với mong muốn chỉ thực hiện những nội dung mà nó quan tâm, tuy nhiên nó phải cài đặt cả phần giao diện mà không cần thiết. Điều này dẫn đến sự dƣ thừa không cần thiết, đồng thời tăng sự kết dính giữa các thực thể. Và khi thực hiện
public:
virtual void Draw() const;
virtual void Transfer(double dx, double dy);
};
class Circle : public Shape{
protected:
double radius;
Point center;
public:
virtual void Draw() const;
virtual void Transfer(double dx, double dy);
};
class Line : public Shape{
protected:
Point startPoint, endPoint;
public:
virtual void Draw() const;
virtual void Transfer(double dx, double dy);
};
Hình 2.1. Mô hình thừa kế lớp
Để minh họa sự vi phạm nguyên lý phân tách giao diện, ta thêm vào đoạn chương trình trên chức năng tô màu, đưa hàm Fill vào trong giao diện Shape.
enum ColorType {red, green, blue};
enum PatternType {solid, vertical, horizontal};
class Shape{
public:
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 side;
Point topLeft;
public:
virtual void Draw() const;
virtual void Transfer(double dx, double dy);
virtual void Fill(ColorType color, PatternType pattern);
};
Các lớp con Circle, Line, Square sẽ phải cài đặt cụ thể cho phương thức ảo Fill.
Với các lớp Circle, Square thì điều này có ý nghĩa thực tế. Tuy nhiên với lớp Line, chức năng Fill sẽ không có ý nghĩa. Ta đã bắt buộc lớp Line phải định nghĩa hàm Fill mà không có ý nghĩa đối với nó. Khi đó ta nói nó đã vi phạm nguyên lý phân tách giao diện.
Để không bị vi phạm nguyên lý, ta có thể thực hiện thiết kế lại nhƣ sau:
Hình 2.2. Mô hình thừa kế lớp cải tiến Một số chú ý khi xem xét nguyên lý phân tách giao diện:
Việc vi phạm nguyên lý phân tách giao diện sẽ dẫn đến vấn đề: nếu chấp nhận giao diện Shape bị ô nhiễm, lúc đó trong lớp Line, chúng ta cài đặt hàm Fill và một giải pháp đặt ra là cài đặt phương thức rỗng. Hoặc trong giao diện Shape, hàm Fill không phải đặt là thuần ảo mà là hàm rỗng. Tuy nhiên cả hai giải pháp này sẽ có nguy cơ vi phạm nguyên lý thay thế Liskov.
Nguyên lý phân tách giao diện có mối liên hệ với nguyên lý đóng mở. Sự vi phạm nguyên lý phân tách giao diện có khả năng dẫn đến vi phạm nguyên lý đóng mở.
trình xây dựng các mẫu thiết kế nhƣ sau:
2.2.1. Tình huống phát sinh mẫu thiết kế là từ nguyên lý thiết kế và thực tiễn
Trong quá trình phân tích thiết kế hướng đối tượng, để đảm bảo tuân theo các nguyên lý thiết kế, đồng thời xây dựng lên kiến trúc tối ƣu cho hệ thống, ta thấy có sự lặp đi lặp lại của những kiến trúc. Trên cơ sở đó, ta trừu tƣợng, tổng quát vấn đề đó thành mẫu thiết kế để sử dụng cho những tình huống tương tự. Do đó có thể nói rằng, mẫu thiết kế đƣợc bắt nguồn từ những tình huống, những vấn đề cụ thể.
Khi thực hiện việc viết các mẫu thiết kế, ta cố gắng dựa trên các nguyên lý thiết kế và thực tiễn một cách triệt để nhất. Các thiết kế ngầm định và tường minh trong mẫu thiết kế cần dựa vào các nguyên lý xuất phát từ một sự hiểu biết sâu rộng về thực tiễn. Khi đó cũng sẽ hữu ích cho việc trích dẫn nguyên lý kèm theo, bởi nó có thể giúp cho đọc giả nguồn gốc cho việc thực hiện quyết định trên.
Để bắt đầu cho việc viết một mẫu thiết kế ta nên nhận dạng qua một tập các mẫu thiết kế đã đƣợc ứng dụng rộng rãi. Đó là việc hệ thống hóa mẫu bao gồm nhận dạng và ghi lại một cách hệ thống các luồng thông dụng và các nguyên lý thiết kế tốt đƣợc biểu diễn trong các mẫu thiết kế. Điều đó sẽ giúp nhận dạng cũng nhƣ hệ thống mẫu thiết kế dự định trong hệ thống các mẫu đã có, đồng thời tạo ra một cái nhìn tổng thể và hệ thống để trừu tƣợng hóa, tổng quát lên mẫu cần diễn đạt.
Các mẫu có thể đƣợc xây dựng từ các mẫu khác. Các mẫu có thể là kết hợp của các một vài mẫu thiết kế khác. Tính chất phức hợp của một mẫu phải đƣợc xem xét một cách cẩn thận. Bởi nó có thể bị phá vỡ thành các mẫu thiết kế thành phần nhỏ hơn.
Liệu mẫu mà ta đang xây dựng là nguyên tử hay phức hợp.
2.2.2. Mẫu thiết kế là giải pháp cụ thể
Mẫu thiết kế đƣợc coi nhƣ là giải pháp cho vấn đề cụ thể. Hay nói cách khác, trọng tâm của mẫu thiết kế là về các giải pháp. Điều này là rất quan trọng, bởi chắc chắn có một chút gì đó đặc biệt có khuynh hướng dẫn đến các mẫu. Nét tổng thể của mẫu thiết kế là sự miêu tả một sự lặp đi lặp lại và giải pháp hữu ích cho vấn đề đó. Các nội dung khác trong mẫu thiết kế thường là phần phụ trợ cho giải pháp khi giải pháp đó đƣợc đƣa ra phù hợp cho tình huống bắt gặp. Việc viết các mẫu thiết kế có thể đƣợc theo các dịnh dạng khác nhau, và do đó có các nội dung khác nhau. Tuy nhiên tất cả
đều có xu hướng hướng tới một mục tiêu, đó là tìm ra và mô tả giải pháp cho những vấn đề đang tranh cãi.
Bất kỳ giải pháp tốt nào cũng là một giải pháp cho một vấn đề cụ thể. Làm thế nào để có một giải pháp mà không có vấn đề tương ứng. Việc hiểu vấn để hay một nhóm các vấn đề là một phần chính của việc hiểu và đƣa ra giải pháp. Nghĩ về các vấn đề giúp tập trung vào lõi của giải pháp. Nó cũng giúp tránh khỏi việc chệch hướng theo một thảo luận nào đó. Vì vậy việc hiểu vấn đề là quan trọng, và giải pháp sẽ vẫn là trọng tâm của các mẫu.
Khi nói về giải pháp, sẽ dễ hiểu hơn khi tập trung vào bản thân giải pháp và làm thế nào để áp dụng giải pháp đó. Ngƣợc lại việc xem xét giải pháp nào là phù hợp và những điều kiện nào là phù hợp hay không phù hợp với nó sẽ gây ra những khó khăn hơn cho việc viết mẫu thiết kế tương ứng. Đây là lý do tại sao người viết mẫu thiết kế thường nhấn mạnh vào vấn đề, bởi vì điều đó tập trung trí óc của chúng ta vào việc khởi nguồn cho các mẫu thiết kế.
Khi nghĩ rằng đã có một mẫu, hãy cố gắng không nghĩ về nó và tập trung nghĩ về một hướng khác. Điều này thường dẫn đến với các mẫu luân phiên. Do đó các mẫu thiết kế thường có một nhóm luân phiên. Điều này có thể giúp dẫn đến với một số mẫu khác phù hợp hơn, hoặc giúp ta hiểu chính mẫu thiết kế mà ta đã có ý định đƣa ra đƣợc sâu sắc hơn.
Ngữ cảnh của ứng dụng là yếu tố then chốt để đạt đƣợc thành công của bất kỳ giải pháp bắt nguồn cho một mẫu. Mọi giải pháp đƣợc mô tả trong các mẫu có thể thành công trong một ngữ cảnh nhƣng lại có thể thất bại trong ngữ cảnh khác. Một khó khăn là hiểu tại sao và sự phụ thuộc nhƣ thế nào vào ngữ cảnh sử dụng cụ thể.
2.2.3. Mục tiêu thiết kế mẫu là hướng tới người dùng
Trong quá trình viết mẫu thiết kế, cần tập trung vào khía cạnh người dùng, đặt người dùng vào trung tâm trong sự mô tả của nó. Mọi thứ trong mẫu cần là về những vấn đề mà người dùng biết, hiểu được ý nghĩa, hiểu và có ý định, cũng như các hoạt động thực hiện trong sự phản hồi đối với cách cƣ xử của hệ thống. Tác giả của mẫu thiết kế cũng cần xem xét người dùng cuối như là một phần quan trọng của khán giả.
Một trong các mục tiêu của việc viết mẫu thiết kế tốt nên là trợ giúp người thiết kế trong việc giao tiếp với người dùng cuối.
Một mẫu không phải là một thiết kế. Một điều quan trọng là phân biệt giữa một mẫu thiết kế và một thiết kế. Một mẫu thiết kế phản ánh sự thực thi tốt nhất đã đƣợc
Một thuật ngữ đƣợc sử dụng trong một mẫu phải có cùng nghĩa khi sử dụng nó trong mẫu khác.
Một mẫu thiết kế tốt mô tả một phần của chức năng đƣợc tham chiếu bởi tên mẫu mà không có sự giải thích kèm theo. Điều này cung cấp một nhóm các khái niệm làm giàu vốn từ vựng và thảo luận một cách dễ dàng giữa những người phát triển và người thiết kế. Do đó việc lựa chọn đặt tên mẫu là rất quan trọng. Tên của một mẫu sẽ phản ánh mục đích của nó.
Các mẫu đóng góp vào một ngôn ngữ chia sẻ, vì các tên của các mẫu đƣợc hệ thống hóa, chúng như là một dạng của giao tiếp giữa các người phát triển và người thiết kế, phân tích nghiệp vụ, và thậm chí các chuyên gia và các người dùng cuối.
Theo cách này, các mẫu trở thành bộ phận của một ngôn ngữ chia sẻ cho việc thảo luận thiết kế giao diện người dùng trong số tât cả những người mà thực hành nó và người bị tác động bởi nó.
Lựa chọn phép ẩn dụ cẩn thận. Nếu có thể, lựa chọn các phép ẩn dụ cần rõ ràng và quen thuộc từ kinh nghiệm thế giới thực mà không dựa trên các thuật ngữ. Phép ẩn dụ được lựa chọn có thể là trong hiểu biết về thế giới thực của người dùng. Điều này có thể cung cấp đòn bẩy cho việc đạt đƣợc sự hiểu biết nhanh chóng của những vấn đề đang xảy ra. Cho ví dụ, ảnh của một file di chuyển giữa các thƣ mục trên màn hình có thể là một phép ẩn dụ tốt cho quá trình thay đổi con trỏ file gắn với cây thƣ mục, thậm chí khi không có dữ liệu nào được di chuyển. Đây là một cách tốt cho người dùng để hiểu quá trình theo một cách thông dụng và tiện lợi.
2.2.5. Lựa chọn các tình huống áp dụng điển hình
Tìm kiếm các ví dụ điển hình: một mô tả mẫu nên đƣợc hỗ trợ bởi các ví dụ đáng tin cậy của các ứng dụng thành công. Để minh họa tính tổng quát, cần đƣa ra các ví dụ thể hiện rõ các nguyên lý thiết kế tốt. Trong khi thường được biểu diễn theo ngôn ngữ của các mẫu, các ví dụ cũng có thể đƣợc biểu diễn nhƣ là các sự xác định hay các sự thực thi thực tế.
Nhiều người có vẻ lo lắng về các ví dụ trong mẫu và các mã ví dụ nhất định.
Bởi khi xem xét mẫu thiết kế trong ví dụ cụ thể sẽ dẫn đến sự khó khăn khi áp dụng cho những vấn đề tương tự trong tầm bao quát của mẫu thiết kế. Đó là một lý do xác