Chương 1: Cơ sở hướng đối tượng
Tác giả: Sưu tầm
Khái quát
Chương này sẽ giới thiệu với các bạn về lập trình hướng đối tượng. Những ai đã quen với lập trình hướng đối tượng rồi thì có thể muốn bỏ qua chương này.
Có rất nhiều cách tiếp cận với thiết kế hướng đối tượng, điều này được chứng minh bằng số lượng quyển sách viết về chúng. Phần giới thiệu sau sẽ đem đến cho bạn một cách tiếp cận thực dụng và không tốn nhiều thời gian trong thiết kế, nhưng cách tiếp cận hướng thiết kế này có thể là hữu ích hơn đối với những người mới học.
Đối tượng là gì?
Đối tượng đơn thuần là một tập hợp những thông tin và chức năng liên quan với nhau. Một đối tượng có thể là một cái gì đó tương ứng với sự biểu hiện thế giới thực (ví dụ như đối tượng employee), hay một cái gì đó có ý nghĩa thực sự (như một cửa sổ trên màn hình máy tính), hay chỉ là một vài khái niệm trừu tượng trong một chương trình (như một danh sách các công việc sẽ làm).
Đối tượng là bao gồm dữ liệu mô tả và những thao tác có thể tác động lên nó. Ví dụ, thông tin chứa bên trong một đối tượng employee có thể là thông tin về nhận dạng (tên, địa chỉ), thông tin về việc làm (công việc, lương), vân vân. Các thao tác có thể bao gồm việc tạo một đối tượng nhân viên hay thăng chức một nhân viên.
Trong việc thiết kế hướng đối tượng, bước đầu tiên là phải xác định cái gì là đối tượng. Việc thiết kế đối tượng thực thường không phức tạp, nhưng đối với thế giới ảo thì các ranh giới trở nên không rõ ràng. Đó là lúc mà nghệ thuật thiết kế xuất hiện, và đó cũng chính là đáp án cho câu hỏi tại sao người ta lại đòi hỏi nhiều kỹ sư giỏi.
Thừa kế
Thừa kế là một đặc tính cơ bản của hệ thống hướng đối tượng, và nó chỉ đơn giản là khả năng thừa kế dữ liệu và chức năng từ một đối tượng cha. Với tính thừa kế này thì ta không cần phải thiết kế một đối tượng mới từ đống tạp nham mà có thể dựa trên một đối tượng có sẵn của người lập trình khác, rồi chỉ việc thêm vào các đặc tính ta cần. Đối tượng cha được coi là lớp cơ sở, và đối tượng con được coi là lớp dẫn xuất.
Thừa kế là rất được chú trọng trong những bài giảng về thiết kế hướng đối tượng, nhưng trong thực tế thì không phải thiết kế nào cũng sử dụng chúng. Có vài lý do cho điều này.
Trước hết, thừa kế được biết đến trong lập trình hướng đối tượng như là một mối quan hệ “là-một”. Nếu trong một hệ thống có một đối tượng animal và một đối tượng cat, thì đối tượng cat có thể thừa kế từ đối tượng animal, bởi vì cat “là-một” animal. Trong thừa kế, lớp cơ sở luôn tổng quát hơn so với lớp dẫn xuất. Lớp cat trên thừa kế hàm thành phần eat từ lớp cơ sở animal, và có thể có thêm một hàm thành phần riêng là sleep. Trong việc thiết kế thế giới thực, những quan hệ như vậy không đặc biệt phổ biến.
Thứ hai, để sử dụng thừa kế, lớp cơ sở cần được thiết kế cùng với vấn đề thừa kế ngay từ đầu. Điều này là quan trọng vì vài lý do. Nếu các đối tượng không có một cấu trúc thích hợp, thì thừa kế không thể thật sự làm việc tốt. Quan trọng hơn nữa là việc thiết kế thừa kế cũng làm cho chương trình trở nên sáng sủa và điều này có thể giúp cho người lập trình có thể hỗ trợ các lớp khác thừa kế từ lớp đã có. Nếu một lớp mới được thiết kế từ một lớp mà không phải như trên, thì khi lớp cơ sở có một vài thay đổi, nó sẽ làm hỏng lớp thừa kế. Một vài lập trình viên ít kinh nghiệm tin tưởng một cách sai lầm rằng thừa kế “được hỗ trợ” rộng rãi trong lập trình hướng đối tượng, và do đó sử dụng nó quá thường xuyên. Thừa kế chỉ cần được sử dụng khi những lợi thế mà nó mang lại là cần thiết. Phần “Đa hình và hàm ảo” sẽ nói rõ hơn về điều này.
Trong môi trường thực thi CLR (Common Language Runtime – thời gian chạy ngôn ngữ chung) của .NET, tất cả các đối tượng được thừa kế từ lớp cơ sở tận cùng có tên là object, và chỉ có sự thừa kế đơn giữa các đối tượng (ví dụ, một đối tượng chỉ có thể được dẫn xuất từ một lớp cơ sở). Do đó chúng ta không thể sử dụng một số thành ngữ thông dụng của hệ thống đa thừa kế như C++, nhưng đồng thời nó cũng loại bỏ sự lạm dụng đa thừa kế và cung cấp cho ta sự rõ ràng. Trong hầu hết các trường hợp, điều này tạo sự cân bằng tốt. Thời gian chạy .NET cho phép đa thừa kế thông qua giao diện (interface), mà không bao hàm sự thực thi. Giao diện sẽ được bàn đến trong Chương 10, “Giao diện”.
Sự chứa đựng
Như vậy, nếu thừa kế không phải là chọn lựa đúng, vậy là cái gì?
Câu trả lời là sự chứa đựng, cũng được biết như là sự tập hợp. Thay vì nói rằng một đối tượng là mẫu của một đối tượng khác, thì một thể hiện của đối tượng khác sẽ được chứa đựng trong một đối tượng. Như vậy, thay vì việc có một lớp trông như một chuỗi, lớp sẽ chứa đựng một chuỗi (hoặc mảng, hoặc bảng băm).
Lựa chọn thiết kế mặc định cần phải là sự chứa đựng, và bạn chỉ cần chuyển sang sự thừa kế khi cần (ví dụ, nếu thật sự có một quan hệ “là-một”).
Đa hình và hàm ảo
Hồi trước khi tôi đang viết một chương trình âm nhạc, tôi đã quyết định hỗ trợ cả Winamp và Windows Media Player như các động cơ phát lại, nhưng tôi không muốn tất cả mã của tôi phải biết động cơ nào đang được sử dụng. Do đó tôi định nghĩa một lớp trừu tượng, để định nghĩa những hàm sẽ được những lớp dẫn xuất cài đặt, và đôi khi cung cấp các hàm hữu ích cho cả hai lớp.
Trong trường hợp này, lớp trừu tượng là MusicServer, và có các hàm như Play(), NextSong(), Pause(), vân vân. Các hàm này được khai báo trừu tượng, nên mỗi lớp dẫn xuất phải tự mình thực thi những hàm này.
Những hàm trừu tượng tự động là những hàm ảo, cho phép người lập trình sử dụng tính đa hình để làm cho chương trình của họ đơn giản hơn. Khi có một hàm ảo, người lập trình có thể chuyển một tham chiếu đến lớp trừu tượng hơn là lớp dẫn xuất, và trình biên dịch sẽ gọi phiên bản thích hợp của hàm trong thời gian chạy.
Để rõ hơn hãy xem ví dụ sau. Một hệ thống âm nhạc hỗ trợ cả Winamp và Windows Media Player. Sau đây là phát thảo cơ bản cho các lớp trong chương trình:
using System;
public abstract class MusicServer {
public abstract void Play();
}
public class WinampServer: MusicServer {
public override void Play() {
Console.WriteLine(“WinampServer.Play()”);
}
}
public class MediaServer: MusicServer {
public override void Play() {
Console.WriteLine(“MediaServer.Play()”);
}
}
class Test {
public static void CallPlay(MusicServer ms) {
ms.Play();
}
public static void Main() {
MusicServer ms = new WinampServer();
CallPlay(ms);
ms = new MediaServer();
CallPlay(ms);
}
}
Chương trình cho kết quả:
WinampServer.Play()
MediaServer.Play()
Đa hình và hàm ảo là được sử dụng nhiều nơi trong hệ thống thời gian chạy .NET. Ví dụ, đối tượng cơ sở object có hàm ảo ToString() được sử dụng để chuyển đổi một đối tượng thành một chuỗi. Nếu bạn gọi hàm ToString() trên một đối tượng mà nó không có phiên bản riêng của ToString() thì phiên bản hàm ToString() của lớp object sẽ được gọi, mà nó đơn giản trả lại tên của lớp đó. Nếu bạn đã chồng hàm ToString() rồi – viết một phiên bản khác cho hàm ToString() trong lớp dẫn xuất – thì thay vào đó hàm này sẽ được gọi, và bạn có thể làm gì đó theo ý bạn, như viết ra tên của nhân viên chứa trong đối tượng nhân viên. Đối với hệ thống âm nhạc trên, điều này có nghĩa là chồng các hàm Play(), Pause(), NextSong(), vân vân.
Đóng gói và nhìn thấy được
Khi thiết kế các đối tượng, người lập trình được phép quyết định những phần nào của đối tượng là người sử dụng thấy được, và phần nào là riêng tư. Những gì mà người sử dụng không nhìn thấy được gọi là được đóng gói trong một lớp.
Nói chung, mục đích khi thiết kế một đối tượng là để đóng gói lớp nếu có thể. Những lý do quan trọng nhất để làm điều này là:
- Người sử dụng không thể thay đổi thành phần private của đối tượng, điều này giảm đi cơ hội của người sử dụng sẽ hoặc là thay đổi hoặc là phụ thuộc vào các chi tiết như vậy trong mã của họ. Nếu người sử dụng phụ thuộc vào những chi tiết này, thì những thay đổi trên đối tượng có thể làm hỏng chương trình người sử dụng.
- Những sự thay đổi trên phần public của một đối tượng phải thích hợp với phiên bản trước. Hầu hết đó là những thành phần mà người sử dụng thấy được, số ít thành phần có thể được thay đổi mà không gây hỏng chương trình của người sử dụng.
- Những giao diện lớn hơn tăng thêm sự phức tạp của toàn bộ hệ thống. Các trường private có thể chỉ được truy nhập từ bên trong lớp; các trường public có thể được truy nhập qua bất kỳ thể hiện nào của lớp. Những thành phần public thường khó gỡ lỗi hơn.
Vấn đề này sẽ được tìm hiểu hơn nữa trong Chương 5, “Lớp 101”