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)
ù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 { // 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(); } Ở đoạn chương trình trên, mặc dù về mặt ngữ nghĩa, hình vuông là một trường hợp của hình chữ nhật. Điều này hoàn toàn đúng!!! Nhưng trong ngữ cảnh này, việc để “Square” kế thừa “Rectangle” là không phù hợp. Lúc này hàm “doSomething” cư xử khác nhau trên các đối tượng của “Rectangle” và “Square”. Như vậy hàm “doSomething” đã 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 mới được xem là cư xử như lớp cơ sở. Và khi đó, việc để nó kế thừa từ lớp cơ sở mới là đúng đắn trong ngữ cảnh phần mềm đang xét. vi) Nguyên lý Thay thế Liskov và kỹ thuật “Design by Contract” vô tình làm cho việc kế thừa trở nên rất khó thực hiện. Khi cần thêm vào một lớp kế thừa, chúng ta phải xem xét rất kỹ lưỡng lại tất cả hàm có thao tác trên lớp cơ sở xem chúng có vi phạm nguyên lý Thay thế Liskov hay không. Chúng ta cũng cần phải xem xét tất cả các phương thức của lớp kế thừa xem chúng có vi phạm những quy định của kỹ thuật “Design by Contract” hay không. Tất cả những điều này là do lớp kế thừa có một mối liên hệ mật thiết với lớp cơ sở. Lớp kế thừa bị kết dính (coupling) chặt chẽ với lớp cơ sở. Sự kết dính này rõ ràng làm cho phần mềm kém linh động (flexibility) một khi có sự thay đổi xảy ra. Do đó, để hạn chế sự kết dính này mà vẫn đảm bảo được tính tái sử dụng, chúng ta chỉ nên kế thừa interface và sử dụng composition thay cho việc kế thừa. Ý nghĩa Nguyên lý Thay thế Liskov 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 nâng cao tính tái sử dụng và bền vững của phần mềm trước những sự thay đổi. Nguyên lý Phân tách interface (The Interface Segregation) Phát biểu Không nên buộc các thực thể phần mềm phụ thuộc vào những interface mà chúng không sử dụng đến. Nội dung Khi xây dựng một lớp đối tượng, đặc biệt là những lớp trừu tượng (abstract class), nhiều người thường có xu hướng để cho lớp đối tượng thực hiện càng nghiều chức năng càng tốt, đưa thật nhiều thuộc tính và phương thức vào lớp đối tượng đó. Những lớp đối tượng như vậy được gọi là những lớp đối tượng có interface bị “ô nhiễm” (fat interface or polluted interface). Khi một lớp đối tượng có interface bị “ô nhiễm”, nó sẽ trở nên cồng kềnh. Một thực thể phần mềm nào đó chỉ cần thực hiện một công việc đơn giản mà lớp đối tượng này hỗ trợ buộc phải làm việc với toàn bộ interface của lớp đối tượng đó. Việc phải truyền đi truyền lại nhiều lần những đối tượng có interface bị “ô nhiễm” sẽ làm giảm hiệu năng của phần mềm. Đặc biệt đối với lớp trừu tượng có interface bị “ô nhiễm”, một số lớp kế thừa chỉ quan tâm đến một phần interface của lớp cơ sở nhưng bị buộc phải thực hiện việc cài đặt cho cả phần interface không hề có ý nghĩa đối với chúng. Điều này dẫn đến sự dư thừa không cần thiết trong các thực thể phần mềm. Quan trọng hơn nữa, việc buộc các lớp kế thừa phụ thuộc vào phần interface mà chúng không sử dụng đến sẽ làm tăng sự kết dính (coupling) giữa các thực thể phần mềm. Một khi sự nâng cấp, mở rộng diễn ra, đòi hỏi phần interface đó phải thay đổi, các lớp kế thừa này bị buộc phải chỉnh sửa theo. Điều này làm cho chúng vi phạm nguyên lý Open-Closed. Hình bên dươi là sơ đồ lớp cho đoạn chương trình tính điện trở mạch điện. “Resistor” và “Lamp” là những mạch điện đơn giản với điện trở là một thuộc tính của mạch. Trong khi “SeriesCircuit” và “ParallelCircuit” là những mạch điện phức hợp với điện trở của mạch được tính từ các mạch điện con. Để có thể cư xử như nhau trên các loại mạch điện này hay nói cách khác là truy xuất đến chúng một cách “trong suốt” (transparency), chúng ta có “Circuit” là lớp trừu tượng chung đại diện cho các mạch điện khác nhau. Lớp “Circuit” được thiết kế như trên được gọi là có interface bị “ô nhiễm”. “Resistor” và “Lamp” bị buộc phải thực hiện việc cài đặt cho các phương thức “add” và “remove” hoàn toàn chẳng có ý nghĩa gì với chúng. Điều này gây ra sự dư thừa code không cần thiết cũng như gây “khó chịu” cho những thực thể phần mềm khác sử dụng “Resistor” và “Lamp”. Nhưng vấn đề chỉ thật sự xảy ra khi chúng ta nâng cấp, mở rộng đoạn chương trình trên. Giả sử chúng ta cần thêm vào phương thức “removeAt” để hỗ trợ việc xóa mạch điện con tại vị trí nào đó trong mạch điện phức hợp. Lúc này, chúng ta phải thực hiện việc chỉnh sửa trên tất cả các lớp đối tượng kế thừa từ “Circuit”. Việc chỉnh sửa trên “SeriesCircuit” và “ParallelCircuit” xem ra còn có thể chấp nhận được. Nhưng việc phải chỉnh sửa trên “Resistor” và “Lamp” là không thể chấp nhận được vì phương thức “removeAt” chẳng hề có ý nghĩa gì đối với chúng. Điều này rõ ràng làm cho “Resistor” và “Lamp” vi phạm nguyên lý Open-Closed một cách “không chính đáng”. Chú ý i) Nguyên lý Phân tách interface có mối liên hệ với nguyên lý Open-Closed. Sự vi phạm nguyên lý Phân tách interface có khả năng dẫn đến sự vi phạm nguyên lý Open-Closed (xem phân tích ở trên). ii) Để tránh vi phạm nguyên lý Phân tách Inteface, chúng ta nên giữ cho interface của lớp đối tượng đơn giản và gọn nhẹ, nên làm theo tiêu chí “a class should do one thing and do it well”. Chúng ta không nên để cho lớp đối tượng đảm nhận quá nhiều trách nhiệm vì điều này dễ làm cho interface của nó bị “ô nhiễm”. iii) Interface bị “ô nhiễm” của lớp đối tượng nên được phân tách ngay khi có thể để tránh khả năng dẫn đến sự vi phạm nguyên lý Open-Closed. Việc phân tách interface bị “ô nhiễm” của một lớp cơ sở có thể được thực hiện thông qua việc tăng thêm mức độ trừu tượng trong cây kế thừa của nó. Lớp cơ sở ban đầu chỉ nên có interface đơn giản mà mọi lớp kế thừa của nó đều cần phải có. Sau đó, phần interface chung của một bộ phận lớp kế thừa được tổng hợp lại trong một lớp cơ sở. Và lớp cơ sở này lại kế thừa từ lớp cơ sở ban đầu. Như vậy những lớp kế thừa thuộc nhánh khác không bị phụ thuộc vào phần interface mà chúng không sử dụng đến của bộ phận lớp kế thừa kia. Với trường hợp đoạn chương trình tính điện trở mạch điện, để giải quyết vấn đề interface của “Circuit” bị “ô nhiễm”, chúng ta tăng thêm một mức độ trừu tượng trong cây kế thừa của nó. Khi đó, “Circuit” đóng vai trò là lớp trừu tượng cho các mạch điện khác nhau. Nó chỉ chứa phần interface chung nhất của tất cả các mạch điện này. Và trong ngữ cảnh bài toán tính điện trở đơn giản thì nó chỉ chứa phương thức “calcResistance”. Chúng ta sẽ có lớp “SingleCircuit” đại diện cho các mạch điện đơn giản và “ComplexCircuit” đại diện cho cách mạch điện phức hợp. “SingleCircuit” chứa phần interface chung của các mạch điện đơn giản như “Resistor” và “Lamp” trong khi “ComplexCircuit” chứa phần interface chung của các mạch điện phức hợp. Chúng ta sẽ có được cây kế thừa như hình bên dưới. Lúc này, khi cần thêm vào phương thức “removeAt” chúng ta chỉ việc nâng cấp phần interface của “ComplexCircuit”, nhánh kế thừa bên “SingleCircuit” sẽ không bị ảnh hưởng. iv) Trong một số trường hợp, sau khi phân tách interface, một số lớp kế thừa mới thêm vào muốn sử dụng những phần interface đã phân tách, chúng có thể thực hiện việc đa kế thừa từ những lớp đối tượng hỗ trợ những phần interface này hoặc cũng có thể kế thừa từ một lớp đối tượng hỗ trợ một phần interface chúng cần và thực hiện composition đối với những đối tượng hỗ trợ phần interface còn lại. Ý nghĩa Nguyên lý Phân tách interface có mối liên hệ 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 giảm sự cồng kềnh, dư thừa không cần thiết cho phần mềm và quan trọng hơn là giảm sự kết dính (copuling) làm hạn chế tính linh động (flexibility) của phần mềm. Tài liệu tham khảo - Robert C. Martin, The Open-Closed Principle, Object Mentor, 1996. - Robert C. Martin, The Dependency Inversion Principle, Object Mentor, 1996. - Robert C. Martin, The Liskov Substitution Principle, Object Mentor, 1996. - Robert C. Martin, The Interface Segregation Principle, Object Mentor, 1996. - Allen Holub, Why extends is evil?, Java World, 2003.
File đính kèm:
- NguyenLyOOP.pdf