CHƯƠNG 1. CƠ SỞ CỦA PHÁT TRIỂN PHẦN MỀM HƯỚNG ĐỐI TƯỢNG Quan hệ kết hợp được biểu diễn trong UML bởi một đường thẳng có hình quả trám rỗng ở một đầu. Điều này xác định không có quan hệ sở hữu trong quan hệ này và các thể hiện của lớp được kết hợp sẽ được quản lý bên ngoài lớp kết hợp. Gian nhỏ chứa Customer chỉ ra một giới hạn, trong hàm khởi tạo của lớp OrderList có một tham số là Customer để giới hạn số lượng Order tương ứng với Customer đó trong Danh sách OrderList.
Hợp thành Quan hệ giữa Order và OrderLine thuộc kiểu hợp thành. Các OrderLine của một Order đều thuộc về Order và không có ý nghĩa bên ngoài Order đó. Order có trách nhiệm hoàn toàn trong việc tạo, quản lý và xóa bất kỳ OrderLine nào trong Order đó. Trong UML, quan hệ này được biểu diễn bởi đường thẳng với một đầu có hình quả trám màu đen.3: Quan hệ hợp thành Cài đặt: package relation; public class OrderLine { private Currency value; aggregation.Order orderLine; } package relation; public class Order { private Customer customer; private OrderLine[] orderLine; private Currency total; aggregation.OrderList unnamedOrderList_; public OrderLine addLine() { throw new UnsupportedOperationException(); } public void removeLine() { throw new UnsupportedOperationException(); } 12 CHƯƠNG 1.
CƠ SỞ CỦA PHÁT TRIỂN PHẦN MỀM HƯỚNG ĐỐI TƯỢNG } 1.2 Kế thừa Kế thừa (inheritance) là khái niệm đã được đề xuất và sử dụng rộng rãi trong xây dựng mô hình dữ liệu của nhưng năm 70, 80 thế kỷ trước. Nó cho phép ta thực hiện cách tư duy đặc biệt hóa (specialization) khi xây dựng một lớp mới bằng cách lấy lại một số đặc điểm, thuộc tính từ lớp cha và sau đó thêm vào một số đặc trưng riêng của nó. Kế thừa cũng cho phép ta thực hiện cách tư duy tổng quá hóa (generalization) bằng cách nhóm một số lớp thành một lớp tổng quát hơn để có thể đưa ra các đối tượng rộng hơn về thế giới mà ta đang sống. Để hiểu một cách đầy đủ hơn khái niệm kế thừa, chúng ta hãy xem xét một ví dụ thiết kế bộ sưu tập để lưu trữ các đối tượng cho sử dụng về sau này.
Thiết kế kiến trúc phân cấp lớp Sau khi phân tích, chúng ta thấy có bốn kiểu bộ sưu tập như sau: Danh sách (List): lưu giữ các đối tượng theo thứ tự mà nó được đưa vào. Túi (Bag): lưu đối tượng nhưng không theo thứ tự. Danh sách liên kết (LinkedList): lưu các đối tượng theo thứ tự bằng cách cài đặt một chuỗi các đối tượng và mỗi đối tượng chỉ tới một đối tượng khác trong chuỗi. Danh sách liên kết cho phép cập nhật dễ dàng, nhưng truy cập chậm vì ta phải duyệt toàn bộ danh sách.
Danh sách mảng (ArrayList): lưu các đối tượng theo thứ tự sử dụng như là một mảng, nghĩa là một chuỗi các ô nhớ liên tiếp. Mảng cho phép truy cập nhanh nhưng cập nhật chậm vì ta có thể phải dịch các phần tử hoặc tạo một mảng mới mỗi khi cập nhật. Làm thế nào thiết kế kiến trúc phân cấp lớp theo kiểu kế thừa? Điểm mấu chốt là cần phải xem xét sự tương đồng giữa các khái niệm với nhau. Rõ ràng, chúng đều là các bộ sưu tập, vì vậy lớp Collection sẽ đưa lên đầu.
Trong bốn bộ sưu tập trên, chỉ có Bag là không lưu các đối tượng theo thứ tự, còn lại đều lưu đối tượng theo thứ tự. Do đó, ta đặt Bag trực tiếp ngay bên dưới Collection trong một nhánh riêng. Ta thấy, List không có ràng buộc gì về cài đặt bên trong, trong khi LinkedList và ArrayList thì có. Vì vậy, List sẽ là lớp cha của LinkedList và ArrayList.
Như vậy, ta có cây phân cấp sau đây (Hình 1. CƠ SỞ CỦA PHÁT TRIỂN PHẦN MỀM HƯỚNG ĐỐI TƯỢNG Hình 1.4: Thiết kế kiến trúc phân cấp Trong thực tế, ta lại thường hay làm ngược lại. Trước hết, ta mô tả các lớp ở mức lá (Bag, ArrayList và LinkedList) và sau đó, tìm các khái niệm tổng quát hơn. Trong khi phát triển mô hình kế thừa, ta tìm các thông điệp để có thể chia sẻ, đưa chúng vào mô hình kế thừa càng ở các lớp trên càng tốt.
Ta sẽ tìm các thông điệp trước khi tìm các thành phần lớp khác vì các thông điệp biểu diễn giao tiếp giữa các đối tượng với thế giới bên ngoài. Xét các thông điệp trong mô hình phân cấp Collection: contains(:Object): boolean Tìm các đối tượng trong bộ sưu tập và trả về true nếu bộ sưu tập chứa tham số, ngược lại trả về false. elementAt(:int): Object trả về đối tượng ở vị trí được xác định bởi tham số truyền vào. numberOfElements(): int trả về số nguyên là số đối tượng trong bộ sưu tập.
Đặt các thông điệp này vào lớp nào? contains() có thể dùng đối với mọi bộ sưu tập, vì vậy đặt nó trong Collection. elementAt(: int) lấy đối tượng ở vị trí xác định nên phải đặt trong List, để tránh sự lặp lại nếu để trong ArrayList và LinkedList. numberOfElement() có thể dùng với mọi bộ sưu tập nên để nó trong Collection. Ta có cây phân cấp trình bày trong Hình 1.
Cài đặt các phương thức trong phân cấp lớp Vì thuật toán tìm kiếm sẽ xử lý khác nhau đối với bộ sưu tập theo thứ tự và không theo thứ tự, nên contains() không thể cài đặt trong Collection. Ở Bag và List ta sẽ cài đặt hàm contains() khác nhau. CƠ SỞ CỦA PHÁT TRIỂN PHẦN MỀM HƯỚNG ĐỐI TƯỢNG //Cài đặt hàm contains trong lớp List bolean contains(Object obj) { for (int i= 0; i< numberOfElements(); ++i) { if (elementAt(i) == 0) { return true; } } return false; } Hình 1.5: Đưa các thông điệp vào các lớp Phương thức elementAt được cài đặt khác nhau trong hai lớp ArrayList và LinkedList. Vì vậy, ta phải có hai phương thức elementAt riêng, một cho lớp ArrayList – truy cập các phần tử một cách trực tiếp, một cho lớp LinkedList – duyệt toàn bộ danh sách.
Cài đặt phương thức numberOfElements() phụ thuộc vào việc ta lưu số phần tử trong một trường hay tính số phần tử khi cần. Lưu số phần tử trong một trường: trường này sẽ tăng khi thêm phần tử và giảm khi xóa phần tử. Cách này cho phép ghi số phần tử một cách nhanh chóng tùy thuộc vào bộ nhớ nhưng chậm trong việc thêm và xóa đối tượng. Tính toán số phần tử khi cần: đối với LinkedList thì chậm vì phải duyệt toàn bộ số phần tử.
Đối với ArrayList và Bag, các đối tượng bên trong có thể lưu trữ số phần tử, do đó sẽ nhanh hơn. Cách này sẽ không tốn bộ nhớ và không bị chậm trong việc thêm và xóa đối tượng. CƠ SỞ CỦA PHÁT TRIỂN PHẦN MỀM HƯỚNG ĐỐI TƯỢNG Ta có thiết kế phan cấp lớp như sau: Hình 1.6: Đưa các thông điệp vào các lớp Các phương thức in nghiêng là các phương thức ảo, còn lại là các phương thức thực. Phương thức ảo chỉ có tên mà không có các dòng mã cài đặt, còn phương thức thực thì ngược lại.
Các lớp ảo Lớp ảo là lớp có ít nhất một phương thức ảo – nó có thể nằm trong lớp đó hoặc được kế thừa từ lớp cha. Khi thiết kế phân cấp lớp, ta nên hình dung trong đầu rằng lớp cha cao nhất là ảo. Định nghĩa lại các phương thức Hướng đối tượng cho phép ta định nghĩa lại các phần tử dựa trên kế thừa. Ở dạng đơn giản nhất, định nghĩa lại cho phép lớp con thay đổi việc cài đặt phương thức được kế thừa.
Tên phương thức vẫn như cũ nhưng các dòng code trong thân sẽ được thay thế hoặc tạo chuyển thông điệp từ private sang public hoặc đổi tên hoặc kiểu của một thuộc tính… Sau đây, ta sẽ tập trung bàn về định nghĩa lại nội dung của phương thức, vì đó là lý do quan trọng nhất cho việc định nghĩa lại. Có ba lý do chính giải thích tại sao ta phải định nghĩa lại: Phương thức được kế thừa là ảo và ta muốn biến nó thành hiện thực bằng cách thêm vào một số dòng mã. Ví dụ, contains là ảo trong Collection nhưng cần là thực trong Bag và List. Phương thức cần phải thực hiện thêm một số công việc khi nằm ở lớp con.
CƠ SỞ CỦA PHÁT TRIỂN PHẦN MỀM HƯỚNG ĐỐI TƯỢNG Ta có thể cung cấp một cài đặt tốt hơn cho lớp con. Ví dụ, nếu thêm một chỉ số vào lớp LinkedList, ta có thể định nghĩa lại contains để làm việc nhanh hơn thuật toán tuần tự được dùng với List. Khi ta thêm công việc, phải chắc chắn rằng định nghĩa lớp cha vẫn làm mọi thứ bình thường – để tăng việc chia sẻ mã nguồn và đơn giản hóa việc bảo trì (ví dụ, nếu sửa đổi định nghĩa của lớp cha, lớp con sẽ tự động có hành vi mới). Mỗi ngôn ngữ hướng đối tượng cho phép phương thức được định nghĩa lại có thể gọi phương thức trong lớp cha.
//Ví dụ trong Java: void initialize() { //invoke the inherited initialize method super.initialize(); … } Đa kế thừa Mỗi lớp con có nhiều lớp cha. Java có một dạng đa kế thừa đối với interface và lớp abstract (không cài đặt phương thức).4 SỬ DỤNG LẠI Sử dụng lại (reuse) là khái niệm đã được bàn cãi rất nhiều trong công nghiệp phần mềm. Nhiều nghiên cứu và thực tế phát triển phần mềm đã chỉ ra rằng sử dụng lại dẫn đến phát triển nhanh hơn, hiệu quả hơn và đáng tin cậy hơn vì mã đã được kiểm thử nhiều lần. Hơn nữa, việc bảo trì cũng sẽ dễ dàng hơn.
Chúng ta có thể liệt kê ra đây một số cách để sử dụng lại mã nguồn: Sử dụng lại các chức năng trong một hệ thống: Dạng đơn giản nhất là dùng lại mã nguồn (được sử dụng trong phát triển các hệ thống theo cách truyền thống) liên quan đến việc viết các hàm tiện ích được gọi từ nhiều nơi. Ví dụ, một số môdun trong hệ thống cần sử dụng chức năng tìm kiếm thông qua một danh sách tên khách hàng, do đó có thể viết một hàm tìm kiếm chung để có thể gọi từ các tình huống khác nhau. Sử dụng lại các phương thức trong một đối tượng: Các phương thức được đóng gói trong một đối tượng có thể được gọi từ các phương thức khác. Ví dụ, trong Java phương thức không public có thể sử dụng trong lớp nào thuộc cùng gói với nó.
Bạn nên nghĩ đến việc sử dụng lại các phương thức trong một đối tượng bất cứ khi nào cần. Sử dụng lại các lớp trong một hệ thống: Nhiều lớp đã định nghĩa có thể được dùng trong các phần khác nhau của hệ thống. Ví dụ, nếu bạn xây dựng lớp khách 17 CHƯƠNG 1.