Do là nguyên lý nên nó có tính trừu tượng cao chứ không đi vào chi tiết cách thức giải quyết vấn đề cụ thể việc hiện thực hóa những nguyên lý lập trình hướng đối tượng đòi hỏi chúng ta p
Trang 1TRƯỜNG ĐẠI HỌC KINH TẾ QUỐC DÂN VIỆN CÔNG NGHỆ THÔNG TIN – KINH TẾ
-
TIỂU LUẬN
SVTH: PHẠM ANH TÚ MSV: 11134279
MÔN: NGUYÊN LÝ NGÔN NGỮ LẬP TRÌNH
ĐỀ TÀI: Nghiên cứu các vấn đề về nguyên lý ngôn ngữ lập trình hướng đối tượng và cài đặt thử nghiệm bài toán tự chọn bằng ngôn ngữ lập trình hướng đối tượng C++
HÀ NỘI THÁNG 4 NĂM 2015
Trang 33
Nguyên Lí Của Lập Trình Hướng Đối Tượng
Phương pháp lập trình hướng đối tượng đã được nghiên cứu và phát triển từ lâu nhưng việc vận dụng nó như thế nào cho hiệu quả trong việc xây dựng phần mềm là điều vẫn còn khá mơ hồ đối với nhiều người Thế nào là một phần mềm hướng đối tượng ? Đâu là những cơ sở nền tảng để xây dựng được phần mềm theo tư tưởng hướng đối tượng đúng nghĩa ? Bài viết này trình bày về các nguyên lý lập trình hướng đối tượng Đó là những quy tắc phân tích thiết kế hướng đối tượng cơ bản, mang tính chất khái quát Do là nguyên lý nên nó có tính trừu tượng cao chứ không đi vào chi tiết cách thức giải quyết vấn đề cụ thể (việc hiện thực hóa những nguyên lý lập trình hướng đối tượng đòi hỏi chúng ta phải xem xét đến Design Patterns)
I, Nguyên lý Open-Closed (The Open-Closed Principle)
1 Phát biểu
Các thực thể phần mềm (hàm, đơn thể, đối tượng, …) nên được xây dựng theo hướng mở cho việc mở rộng (be opened for extension) nhưng đóng đối với việc sửa đổi (be closed for modification)
2 Nội dung
Các thực thể trong một phần mềm không đứng riêng lẻ mà có sự gắn kết chặt chẽ với nhau Chúng phối hợp hoạt động để cùng nhau thực hiện các chức năng của phần mềm
Do đó, việc nâng cấp, mở rộng một thực thể nào đó sẽ ảnh hưởng đến những thực thể liên quan Điều này có thể dẫn đến việc phải nâng cấp, mở rộng cả những thực thể liên quan đó Và trong thời đại đầy biến động hiện nay, việc phải thường xuyên nâng cấp, mở rộng các thực thể trong phần mềm là điều khó tránh khỏi
Để làm cho quá trình bảo trì, nâng cấp, mở rộng phần mềm diễn ra dễ dàng
và hiệu quả hơn, các thực thể phần mềm nên được xây dựng tuân theo nguyên lý Open-Closed Điều này có nghĩa là các thực thể phần mềm nên
Trang 44
được xây dựng sao cho việc nâng cấp, mở rộng đồng nghĩa với việc thêm vào những cái mới chứ không phải là thay đổi những cái hiện có, từ đó tránh được việc phải thay đổi các thực thể liên quan
Xét ví dụ một đoạn chương trình vẽ đường thẳng và hình chữ nhật bằng C# public enum ShapeType
Trang 5public void drawRectangle()
Trang 6Đoạn chương trình trên hoạt động rất tốt cho đến khi có sự nâng cấp, mở rộng Giả
sử chúng ta cần nâng cấp, mở rộng đoạn chương trình trên để nó có thể vẽ thêm được hình tròn Lúc bấy giờ ta phải chỉnh sửa lại hàm “draw”, thêm vào một trường hợp vẽ hình tròn Và trong nhiều tình huống, việc chỉnh sửa hàm “draw” sẽ dẫn đến việc chỉnh sửa những hàm khác liên quan Hàm “draw” được viết theo cách này được nói là không tuân thủ nguyên lý Open-Closed
Để đoạn chương trình trên tuân thủ nguyên lý Open-Closed, chúng ta sử dụng tính đa hình của lập trình hướng đối tượng
public abstract class Shape
{
public abstract void draw();
}
public class Line: Shape
{ public override void draw()
{ // Draws the line
}
}
public class Rectangle: Shape
{
Trang 77
public override void draw()
{ // Draws the rectangle }
}
class Circle: Shape
{
public override void draw()
{ // Draws the circle }
}
public void draw(ArrayList shapeList)
{ foreach (Shape s in shapeList) s.draw(); }
Với đoạn chương trình trên, khi thêm một hình mới vào, chúng ta chỉ việc thêm lớp đối tượng cho hình đó (kế thừa từ Shape) mà không cần phải chỉnh sửa lại hàm “draw”
Nó vẫn hoạt động tốt với những hình mới thêm vào
Ghi chú
i) Không phải lúc nào tất cả các thực thể trong phần mềm đều có thể tuân thủ
nguyên lý Open-Closed Nhưng mục tiêu của phân tích thiết kế hướng đối tượng là phải làm sao cho số lượng các thực thể tuân thủ nguyên lý là lớn nhất, trong đó ưu tiên các thực thể thường xuyên phải nâng cấp, mở rộng thỏa nguyên lý
ii) Việc tuân thủ nguyên lý Open-Closed của một thực thể phần mềm chỉ mang tính tương đối, phụ thuộc vào ngữ cảnh Có thể trong ngữ cảnh này, thực thể thỏa
nguyên lý, nhưng trong một ngữ cảnh khác, thực thể này không còn tuân thủ nguyên
lý nữa Mục tiêu của phân tích thiết kế hướng đối tượng là phải làm sao cho có nhiều thực thể phần mềm nhất tuân thủ nguyên lý trong ngữ cảnh thường xảy ra nhất của phần mềm, trong đó ưu tiên các thực thể thường xuyên phải nâng cấp, mở rộng thỏa nguyên lý
Trang 88
Ví dụ trường hợp hàm “draw” như trong đoạn chương trình vẽ hình trên
public void draw(ArrayList shapeList)
iii) Một tính chất quan trọng trong lập trình hướng đối tượng giúp cho các thực thể phần mềm tăng khả năng tuân thủ nguyên lý Open-Closed là tính đóng gói
(encapsulation) Đối tượng nắm giữ thông tin và chịu trách nhiệm trên thông tin mình nắm giữ Điều này giúp hạn chế sự kết dính (coupling) giữa các lớp đối tượng với nhau Trường hợp lý tưởng là tất cả thuộc tính của đối tượng được đặt tầm vực private việc thay đổi trên thuộc tính chỉ có thể được thực hiên thông qua những xử
lý của phương thức Những phương thức của đối tượng khác, kể cả đối tượng kế thừa không thể truy xuất được đến những thuộc tính này
iv) Việc hạn chế sử dụng ép kiểu động (runtime type-casting) trong các thực thể phần mềm cũng sẽ giúp làm tăng khả năng tuân thủ nguyên lý Open-Closed của chúng Vì bản chất của việc ép kiểu động là làm việc với một kiểu dữ liệu cụ thể Khi muốn nâng cấp, mở rộng thực thể để nó có thể làm việc với những kiểu dữ liệu khác, đoạn chương trình sử dụng ép kiểu động phải được thay đổi để có thể làm việc được với các kiểu dữ liệu khác này
public void doSomething(Vehicle vehicle)
{
Car car = (Car)vehicle; car.run(); car.stop();
}
Trang 99
Khi cần nâng cấp, mở rộng để đoạn chương trình trên có thể làm việc được với các lớp đối tượng khác kế thừa từ “Vehicle”, chúng ta phải chỉnh sửa lại nó
Ý nghĩa: Nguyên lý Open-Closed là nguyên lý cốt lõi và là một trong bốn nguyên
lý cơ bản làm nền tảng cho phân tích thiết kế hướng đối tượng Nó giúp cho phần mềm dễ bảo trì, nâng cấp và mở rộng
II, Nguyên lý Nghịch đảo phụ thuộc (The Dependency Inversion Principle)
1 Phát biểu:
Các thành phần trong phần mềm không nên phụ thuộc vào những cái riêng,
cụ thể (details) mà ngược lại nên phụ thuộc vào những cái chung, tổng quát (abstractions) của những cái riêng, cụ thể đó
Những cái chung, tổng quát (abstractions) không nên phụ vào những cái riêng, cụ thể (details) Sự phụ thuộc này nên được đảo ngược lại
2 Nội dung
Những cái chung, tổng quát là tập hợp của những đặc tính chung nhất từ những cái riêng, cụ thể Những cái riêng, cụ thể dù khác nhau thế nào đi nữa cũng đều tuân theo các quy tắc chung mà cái chung, tổng quát của nó đã định nghĩa Những cái chung, tổng quát là những cái ít thay đổi và ít biến động Trong khi đó, sự thay đổi lại thường xuyên xảy ra ở những cái riêng, cụ thể Việc phụ thuộc vào những cái chung, tổng quát sẽ giúp cho các thành phần trong phần mềm trở nên linh động (flexible) và thích ứng tốt với sự thay đổi thường xuyên diễn ra ở những cái riêng, cụ thể Khi phụ thuộc vào những cái chung, tổng quát, các thành phần trong phần mềm vẫn có thể hoạt động tốt
mà không cần phải sửa đổi một khi cái riêng, cụ thể được thay thế bằng một cái riêng, cụ thể khác cùng loại
Lấy ví dụ đ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();
Trang 10Keyboard keyboard = new Keyboard();
Printer printer = new Printer();
File file = new File();
Để đoạn chương trình trên tuân thủ Nguyên lý Nghịch đảo phụ thuộc, từ đó tuân thủ Nguyên lý Open-Closed, chúng ta phải cho nó làm việc với thiết bị đọc ghi tổng quát
Trang 11develops, this flexibility is essential.”
Chú ý
i) Nguyên lý Nghịch đảo phụ thuộc có mối liên hệ mật thiết với nguyên lý
OpenClosed Một khi nguyên lý Nghịch đảo phụ thuộc bị vi phạm, có nghĩa là những thành phần trong phần mềm phụ thuộc vào những cái riêng, cụ thể, việc nâng cấp, mở rộng ở những cái riêng, cụ thể (điều này rất thường xảy ra) buộc những thành phần phụ thuộc vào nó bị thay đổi theo Điều này dẫn đến vi phạm nguyên lý Open-Closed
ii) Sự nghịch đảo được đề cập đến ở đây nhằm nhấn mạnh đến việc cần phải thay đổi quan điểm trong phân tích thiết kế phần mềm Theo lối suy nghĩ “chia để trị” của lập trình hướng cấu trúc, những công việc lớn, phức tạp, mang tính trừu tượng cao thường được phân ra thành những công việc nhỏ, đơn giản và cụ thể hơn Khi đó, cấu trúc phần mềm có xu hướng theo dạng những thành phần lớn (trừu tượng) gọi đến những thành phần nhỏ (cụ thể) hơn để yêu cầu chúng thực hiện công việc Điều này thường làm cho những thành phần trong phần mềm phụ thuộc vào những cái riêng, cụ thể Trong phân tích thiết kế hướng đối tượng, sự phụ thuộc này nên được đảo ngược lại
iii) Một thành phần trong phần mềm vi phạm nguyên lý Nghịch đảo phụ thuộc sẽ có tính tái sử dụng (reusability) không cao Việc mang những thành phần này sử dụng vào
Trang 12public void doSomething(Car car)
Nguyên lý Nghịch đảo phụ thuộc có mối liên hệ mật thiết với nguyên lý Open-Closed và
là một trong bốn nguyên lý cơ bản làm nền tảng cho phân tích thiết kế hướng đối tượng
Nó giúp cho phần mềm có tính tái sử dụng cao, linh động và bền vững (robustness) trước những sự thay đổi
III, Nguyên lý Thay thế Liskov (The Liskov Substitution Principle)
1 Phát biểu
Lớp B chỉ nên kế thừa từ lớp A khi và chỉ khi với mọi hàm F thao tác trên các đối tượng của A, cách cư xử (behaviors) của F không thay đổi khi ta thay thế (substitute) các đối tượng của A bằng các đối tượng của B
Trang 1313
2 Nội dung:
Kế thừa (inheritance) là một trong những tính chất cơ bản của lập trình hướng đối tượng Đó là khả năng định nghĩa một lớp đối tượng dựa trên các lớp đối tượng đã được định nghĩa trước đó Các đối tượng của lớp kế thừa có khả năng cư xử (behave) như các đối tượng của lớp cơ sở Điều này có nghĩa
là các đối tượng của lớp kế thừa hoàn toàn có thể thay thế các đối tượng của lớp cơ sở trong những hàm thao tác trên các đối tượng của lớp cơ sở
Chính vì tính chất này mà chúng ta không thể sử dụng kế thừa một cách tùy tiện Giả sử ta có lớp A và hàm F thao tác trên các đối tượng của A Để nâng cấp, mở rộng phần mềm, ta cần thêm vào lớp B kế thừa từ A Nhưng việc thay thế các đối tượng của A bằng các đối tượng của B lại làm cho F cư xử sai lệch so với trước khi thực hiện việc thay thế Lúc này, để F có thể cư xử không đổi so với trước, ta phải chỉnh sửa lại F Điều này làm cho F vi phạm nguyên lý Open-Closed
Đoạn chương trình sau cho thấy việc kế thừa tùy tiện chỉ với mục đích tái sử dụng nguy hiểm như thế nào
public class Stack
{ private ArrayList data;
// More data members of stack P
ublic virtual void push(int n)
{ // Pushes n to stack }
public virtual int pop()
{ // Pops value from stack }
}
public class Queue: Stack
{
// Data members of Queue
public override void push(int n)
Trang 14Với mục đích tái sử dụng là một số thuộc tính và phương thức trong “Stack”, chúng
ta cho “Queue” kế thừa từ Stack Xét hàm “func” thao tác trên đối tượng của “Stack”, do
“Queue” kế thừa từ “Stack” nên chúng ta hoàn toàn có thể truyền đối tượng của “Queue” vào hàm này Nhưng cách cư xử của hàm “func” khi thao tác trên các đối tượng của
“Stack” và “Queue” là khác nhau Với các đối tượng của “Stack” hàm func luôn trả về chính xác tích của hai số 7 và 6 Nhưng với các đối tượng của “Queue” hàm func lại luôn gây ra một exception Để hàm “func” có thể cư xử trên các đối tượng của “Stack” và
“Queue” như nhau, chúng ta phải viết lại nó Điều này làm cho hàm “func” vi phạm nguyên lý Open-Closed Khi đó ta nói hàm “func” vi phạm nguyên lý Thay thế Liskov
Chú ý
i) Nguyên lý Thay thế Liskov có mối liên hệ mật thiết với Nguyên lý Open-Closed
Sự vi phạm nguyên lý Thay thế Liskov sẽ dẫn đến sự vi phạm nguyên lý Open-Closed
Trang 1515
Một thực thể phần mềm vi phạm nguyên lý Thay thế Liskov sẽ cư xử khác nhau trên các đối tượng của lớp cơ sở và lớp kế thừa Để thực thể phần mềm này vẫn có thể làm việc tốt trên các đối tượng của cả lớp cơ sở và lớp kế thừa, chúng ta phải chỉnh sửa lại nó Điều này dẫn đến vi phạm nguyên lý Open-Closed
ii) Không phải lúc nào tất cả các thực thể trong phần mềm đều có thể tuân thủ nguyên lý Thay thế Liskov Nhưng mục tiêu của phân tích thiết kế hướng đối tượng là phải làm sao cho số lượng các thực thể tuân thủ nguyên lý là lớn nhất, trong đó ưu tiên các thực thể thường xuyên phải nâng cấp, mở rộng thỏa nguyên lý
iii) Việc tuân thủ nguyên lý Thay thế Liskov của một thực thể phần mềm chỉ mang tính tương đối, phụ thuộc vào ngữ cảnh Có thể trong ngữ cảnh này, thực thể thỏa nguyên
lý, nhưng trong một ngữ cảnh khác, thực thể này không còn tuân thủ nguyên lý nữa Mục tiêu của phân tích thiết kế hướng đối tượng là phải làm sao cho có nhiều thực thể phần mềm nhất tuân thủ nguyên lý trong ngữ cảnh thường xảy ra nhất của phần mềm, trong đó
ưu tiên các thực thể thường xuyên phải nâng cấp, mở rộng thỏa nguyên lý
iv) Quan hệ “IS-A” thường được dùng để phát hiện kế thừa Khi lớp đối tượng B về mặt ngữ nghĩa là một trường hợp đặc biệt của lớp đối tượng A thì ta có thể cho B kế thừa
từ A Nhưng thực tế cho thấy, trong một số ngữ cảnh của phần mềm, một lớp đối tượng
có quan hệ “IS-A” với những lớp đối tượng khác nhưng việc để nó kế thừa những lớp đối tượng này sẽ dẫn đến việc vi phạm nguyên lý Thay thế Liskov Xét đoạn chương trình sau
public class Rectangle
{
// Data members of rectangle
// Member functions of rectangle
}
public class Square: Rectangle
{
Trang 1616
// Data members of square
// Member functions of square
}
public double doSomething(Rectangle obj)
{ obj.setWidth(5);
obj.setHeight(6);
if (obj.Area == 30) return obj.Area;
throw new ArgumentException();
đã vi phạm nguyên lý Thay thế Liskov Để hàm “doSomething” có thể làm việc được trên
cả “Rectangle” và “Square” chúng ta phải chỉnh sửa lại nó Như vậy việc vi phạm nguyên
lý Thay thế Liskov đã làm cho hàm “doSomething” vi phạm nguyên lý Open-Closed v) Nguyên lý Thay thế Liskov có mối liên hệ mật thiết với kỹ thuật “Design by Contract” được đề cập bởi Bertrand Meyers Kỹ thuật này chỉ ra rằng: mỗi phương thức trong một lớp đối tượng, khi được định nghĩa, đã hàm chứa trong nó tiền điều kiện (pre-condition) và hậu điều kiện (post-condition) Tiền điều kiện là những điều kiện cần để phương thức có thể thực hiện được Hậu điều kiện là những ràng buộc phát sinh sau khi thực hiện phương thức Khi thực hiện việc kế thừa, phương thức được định nghĩa lại trong lớp kế thừa phải có tiền điều kiện lỏng lẻo hơn (weaker) và hậu điều kiện chặt chẽ hơn (stronger) Điều này có nghĩa là trước khi thực hiện, phương thức được định nghĩa lại trong lớp kế thừa không được đòi hỏi nhiều hơn như khi nó được định nghĩa trong lớp cơ
sở Và sau khi thực hiện, phương thức được định nghĩa lại trong lớp kế thừa phải đảm bảo tất cả những ràng buộc phát sinh như khi nó được định nghĩa trong lớp cơ sở Chỉ khi nào những điều trên được đáp ứng cho mọi phương thức trong lớp kế thừa thì lớp kế thừa