Chương 15: Lập trình hướng đối tượng

Trở về Mục lục cuốn sách

15.1  Các ngôn ngữ và phong cách lập trình

Có nhiều ngôn ngữ lập trình khác nhau, và không kém mấy về số lượng là các phong cách lập trình (còn gọi là mẫu hình). Những chương trình mà ta đã viết đến giờ đều thuộc phong cách thủ tục, bởi chú ý được dồn vào việc quy định các thủ tục tính toán.

Đa số các chương trình Java đều hướng đối tượng, có nghĩa là tập trung về những đối tượng và tương tác giữa chúng. Sau đây là một số đặc tính của lập trình hướng đối tượng:

  • Đối tượng thường biểu diễn cho những thực thể ngoài đời. Trong chương trước, việc tạo nên lớp Deck là một bước hướng tới lập trình hướng đối tượng.
  • Đa số các phương thức là phương thức đối tượng (như những phương thức mà ta kích hoạt lên các Strings) thay vì các phương thức lớp (như các phương thức Math). Những phương thức mà đến giờ ta đã viết vẫn là phương thức lớp. Ở chương này ta sẽ viết một số phương thức đối tượng.
  • Các đối tượng cô lập khỏi nhau bằng cách hạn chế những cách thức tương tác giữa chúng, đặc biệt bằng cách ngăn không cho chúng truy cập các biến thực thể mà không kích hoạt các phương thức.
  • Các lớp được tổ chức trong cây gia đình, ở đó những lớp mới thì mở rộng từ lớp cũ, qua việc bổ sung những phương thức mới và thay thế phương thức sẵn có.

Ở chương này tôi chuyển chương trình Card ở chương trước, từ phong cách thủ tục sang hướng đối tượng. Bạn có thể tải về mã lệnh từ chương này tại http://thinkapjava.com/code/Card3.java.

15.2  Các phương thức đối tượng và phương thức lớp

Có hai kiểu phương thức trong Java, gọi là phương thức lớp và phương thức đối tượng. Phương thức lớp dễ nhận thấy bởi có từ khóa static ngay trên dòng đầu. Còn bất kì phương thức nào không có từ khóa static này thì đều là phương thức đối tượng.

Dù chưa viết được phương thức đối tượng nào, song ta đã kích hoạt một số phương thức như vậy. Mỗi khi bạn kích hoạt một phương thức “lên” một đối tượng, thì đó chính là phương thức đối tượng. Chẳng hạn, charAt và những phương thức khác mà ta kích hoạt lên những đối tượng String đều là các phương thức đối tượng.

Bất cứ thứ gì viết được là phương thức lớp cũng có thể được viết thành phương thức đối tượng, và ngược lại. Song đôi khi, sẽ có một cách viết tự nhiên hơn cách kia.

Chẳng hạn, sau đây là printCard viết dưới dạng phương thức lớp:

  public static void printCard(Card c) { 
    System.out.println(ranks[c.rank] + " of " + suits[c.suit]); 
  }

Còn sau đây, nó được viết lại thành phương thức đối tượng:

  public void print() { 
    System.out.println(ranks[rank] + " of " + suits[suit]); 
  }

Sau đây là những thay đổi:

  1. Tôi đã xóa từ static.
  2. Tôi thay đổi tên của phương thức để giống với giọng đọc Java hơn.
  3. Tôi bỏ tham số đi.
  4. Bên trong một phương thức đối tượng, bạn có thể tham chiếu đến các biến thực thể như thể chúng là những biến địa phương, vì vậy tôi đổi c.rank thành rank, và tương tự với suit.

Đây là cách kích hoạt phương thức mới này:

    Card card = new Card(1, 1); 
    card.print();

Khi bạn kích hoạt một phương thức lên một đối tượng thì đối tượng đó trở thành đối tượng hiện hành, còn được gọi là this. Bên trong print, từ khóa this tham chiếu đến lá bài mà phương thức được kích hoạt lên đó.

15.3  Phương thức toString

Từng kiểu đối tượng đều có một phương thức mang tên toString để trả lại một chuỗi biểu diễn cho đối tượng đó. Khi bạn in ra đối tượng bằng lệnh print hoặc println, Java sẽ kích hoạt phương thức toString của đối tượng này.

Phiên bản mặc định của toString trả lại một chuỗi có chứa kiểu của đối tượng và một số nhận diện duy nhất (xem Mục 11.6). Khi bạn định nghĩa một kiểu đối tượng mới, bạn có thể sửa đè lên hành vi mặc định này bằng cách cung cấp một phương thức mới chứa hành vi mà bạn muốn.

Chẳng hạn, sau đây là một phương thức toString đối với Card:

  public String toString() { 
    return ranks[rank] + " of " + suits[suit]; 
  }

Kiểu trả lại là String, theo lẽ tự nhiên; và phương thức này không nhận tham số nào. Bạn có thể kích hoạt toString theo lối thông thường:

    Card card = new Card(1, 1); 
    String s = card.toString();

hoặc bạn cũng có thể kích hoạt gián tiếp nó thông qua println:

    System.out.println(card);

15.4  Phương thức equals

Ở Mục 13.4 ta đã nói về hai hình thức cân bằng: sự giống hệ, nghĩa là hai biến cùng tham chiếu tới một đối tượng, và sự tương đương, tức là hai biến có cùng giá trị.

Toán tử == kiểm tra sự giống hệt, nhưng không có toán tử nào để kiểm tra sự tương đồng, bởi “tương đồng” thế nào thì còn phụ thuộc vào kiểu của đối tượng nữa. Thay vì vậy, các đối tượng lại cung cấp một phương thức có tên equals để định nghĩa sự tương đồng này.

Các lớp trong Java cung cấp những phương thức equals để làm điều đúng đắn. Nhưng với những kiểu do người dùng định nghĩa thì cách ứng xử mặc định của phương thức này cũng chẳng khác gì sự giống hệt; đây không phải là điều bạn mong muốn.

Đối với Card ta đã có một phương thức để kiểm tra sự tương đồng:

  public static boolean sameCard(Card c1, Card c2) { 
    return (c1.suit == c2.suit && c1.rank == c2.rank); 
  }

Bởi vậy tất cả những điều ta cần làm là viết lại nó dưới dạng một phương thức cho đối tượng:

  public boolean equals(Card c2) { 
    return (suit == c2.suit && rank == c2.rank); 
  }

Một lần nữa, tôi đã bỏ đi từ khóa static cũng như thông số đầu, c1. Sau đây là cách kích hoạt phương thức mới này:

    Card card = new Card(1, 1); 
    Card card2 = new Card(1, 1); 
    System.out.println(card.equals(card2));

Bên trong equalscard là đối tượng hiện hành còn card2 là tham số, c2. Đối với những phương thức hoạt động trên hai đối tượng có cùng kiểu, đôi khi tôi dùng hẳn từ khóa this đồng thời gọi tham số kia là that:

  public boolean equals(Card that) { 
    return (this.suit == that.suit && this.rank == that.rank); 
  }

Tôi nghĩ rằng theo cách này, mã lệnh sẽ dễ đọc hơn.

15.5  Những điều kì quặc và lỗi sai

Nếu bạn có các phương thức đối tượng và lớp đối tượng ở bên trong cùng một lớp, thì thật dễ nhầm lẫn. Một cách thông thường để tổ chức lời định nghĩa lớp là đặt tất cả những constructor ở đầu, theo sau là tất cả những phương thức đối tượng và tiếp theo là phương thức lớp.

Bạn có thể có một phương thức đối tượng trùng tên với phương thức lớp, miễn là chúng không có cùng số lượng cũng như kiểu các tham số. Giống các hình thức quá tải (overloading) khác, Java quyết định xem cần kích hoạt dạng nào bằng cách nhìn vào những tham số mà bạn cung cấp.

Bây giờ khi đã biết ý nghĩa của từ khóa static, có lẽ bạn đã hình dung ra được rằng main là một phương thức lớp, nghĩa là không có một “đối tượng hiện thời” nơi nó được kích hoạt.  Vì không có đối tượng hiện thời trong một phương thức lớp, nên sẽ có lỗi khi dùng từ khóa this. Nếu bạn thử thì sẽ nhận được một thông báo lỗi kiểu như “Undefined variable: this.”

Đồng thời, bạn cũng không thể tham chiếu đến những biến thực thể mà không dùng kí pháp dấu chấm lẫn cung cấp một tên đối tượng. Nếu thử làm, bạn sẽ nhận một thông báo lỗi như “non-static variable… cannot be referenced from a static context.” Nói “non-static variable” nghĩa là biến thực thể (“instance variable.”)

15.6  Thừa kế

Đặc điểm ngôn ngữ thường gắn với lập trình hướng đối tượng nhất là tính thừa kế. Thừa kế là khả năng định nghĩa được một lớp mới là phiên bản sửa đổi từ một lớp sẵn có. Mở rộng hình ảnh ví von này, lớp sẵn có đôi khi còn được gọi là lớp cha mẹ và lớp mới được gọi là lớp con.

Ưu điểm cơ bản của đặc điểm này là bạn có thể bổ sung được những phương thức và biến thực thể mà không cần sửa đổi lớp cha mẹ. Điều này đặc biệt hữu ích đối với các lớp Java, vì bạn có muốn cũng chẳng thể sửa đổi được chúng.

Nếu bạn đã làm các bài tập GridWorld rồi (ở các Chương 5 và 10), bạn sẽ thấy một số ví dụ về thừa kế:

public class BoxBug extends Bug { 
  private int steps; 
  private int sideLength; 
  public BoxBug(int length) { 
    steps = 0; 
    sideLength = length; 
  } 
}

BoxBug extends Bug nghĩa là BoxBug là một loại Bug mới đựa kế thừa những phương thức và biến thực thể của Bug. Ngoài ra:

  • Lớp con có thể có thêm các biến thực thể khác. Trong ví dụ này, các BoxBug có steps và sideLength.
  • Lớp con có thể có thêm các phương thức khác. Trong ví dụ này, các BoxBug có thêm một constructor nhận vào tham số nguyên.
  • Lớp con có thể ghi đè lên một phương thức thừa hưởng từ lớp cha mẹ. Trong ví dụ này, lớp con cung cấp phương thức act (không chỉ ra ở đây), để ghi đè lên phương thức act của lớp cha mẹ.

Nếu bạn đã làm các bài tập về đồ họa ở Phụ lục A, bạn còn thấy một ví dụ nữa:

public class MyCanvas extends Canvas { 
  public void paint(Graphics g) { 
    g.fillOval(100, 100, 200, 200); 
  } 
}

MyCanvas là một kiểu mới của Canvas, chẳng có thêm phương thức hay biến thực thể nào, song nó ghi đè lên paint.

Nếu bạn chưa từng làm bài nào trong số đó thì giờ đã là lúc rồi!

15.7  Cấu trúc thừa kế lớp

Trong Java, tất cả mọi lớp đều mở rộng từ một lớp nào đó khác. Lớp cơ bản nhất được gọi là Object. Nó không chứa biến thực thể nào, nhưng có cung cấp các phương thức equals và toString, cùng những phương thức khác.

Nhiều lớp mở rộng Object, gồm cả hầu hết những lớp ta đã viết và nhiều lớp Java khác, như java.awt.Rectangle. Bất kì lớp nào không nói rõ tên lớp cha mẹ ra, thì đều mặc định là thừa hưởng từ Object.

Tuy vậy, một số chuỗi thừa kế thì dài hơn. Chẳng hạn, javax.swing.JFrame mở rộng java.awt.Frame, đến lượt nó lại mở rộng Window, đến lượt nó mở rộng Container, đến lượt nó mở rộng Component, đến lượt nó mở rộng Object. Bất kể chuỗi này có dài như thế nào thì Object vẫn là “tổ tiên” chung của tất cả các lớp.

“Cây gia đình” của các lớp được gọi là thừa kế lớp. Object thường xuất hiện ở trên cùng, và tất cả những lớp “con” thì được xếp dưới. Chẳng hạn, nếu bạn nhìn vào tài lệu của JFrame, bạn sẽ thấy rằng phần của sự thừa kế cho ra JFrame.

15.8  Thiết kế hướng đối tượng

Thừa kế là một đặc điểm quan trọng. Có những chương trình sẽ trở nên rất phức tạp nếu không diễn đạt được một cách gọn gàng, đơn giản bằng đặc điểm nói trên. Hơn nữa, thừa kế có thể giúp tận dụng lại mã lệnh, vì bạn có thể chỉnh lại theo ý thích cách ứng xử của những lớp sẵn có mà không cần sửa đổi chúng.

Mặt khác, thừa kế có thể làm cho chương trình rất khó đọc. Khi bạn thấy một lời kích hoạt phương thức, thật khó để hình dung ra phương thức nào được kích hoạt.

Ngoài ra, nhiều thứ có thể thực hiện bằng cách thừa kế cũng có thể làm được hoặc thậm chí tốt hơn mà không dùng cách này. Cách làm thay thế thường gặp là tổng hợp, trong đó các đối tượng được kết hợp từ những đối tượng có sẵn, qua đó bổ sung thêm tính năng mà không cần thừa kế.

Việc thiết kế nên những đối tượng và mối liên hệ giữa chúng là chủ đề nghiên cứu của thiết kế hướng đối tượng, một lĩnh vực nằm ngoài phạm vi cuốn sách này. Song nếu bạn quan tâm, tôi sẽ gợi ý bạn đọc quyển Head First Design Patterns, của nhà xuất bản O’Reilly Media.

15.9  Thuật ngữ

phương thức đối tượng:
Một phương thức được kích hoạt lên một đối tượng, đồng thời hoạt động trên đối tượng đó. Các phương thức đối tượng thì không có chứa từ khóa static.
phương thức lớp:
Một phương thức có từ khóa static. Phương thức lớp không được kích hoạt trên đối tượng và chúng không có đối tượng hiện hành.
đối tượng hiện hành:
Đối tượng mà trên đó một phương thức đối tượng được kích hoạt. Bên trong phương thức, đối tượng hiện hành được tham chiếu đến bằng this.
ngầm:
Thứ được lướt qua không nói đến, hay được ngụ ý. Bên trong một phương thức đối tượng, bạn có thể tham chiếu đến những biến thực thể một cách ngầm (nghĩa là không nhắc đến tên đối tượng).
tường minh:
Thứ được ghi rõ ra. Bên trong một phương thức lớp, tất cả những tham chiếu đến biến thực thể phải được viết tường minh.

15.10  Bài tập

Bài tập 1  Tải về các file http://thinkapjava.com/code/CardSoln2.java và http://thinkapjava.com/code/CardSoln3.java. File CardSoln2.java chứa lời giải những bài tập của chương trước. Nó chỉ dùng các phương thức lớp (trừ các constructor). CardSoln3.java cũng chứa chương trình này, nhưng đa số các phương thức đều là phương thức đối tượng. Tôi vẫn để nguyên merge mà không thay đổi vì tôi nghĩ nó là phương thức lớp thì sẽ dễ đọc hơn. Hãy chuyển  merge thành một phương thức đối tượng, và chuyển mergeSort một cách tương ứng. Bạn thích phiên bản merge nào hơn?
Bài tập 2  Hãy chuyển phương thức lớp sau đây thành phương thức đối tượng.

  public static double abs(Complex c) { 
    return Math.sqrt(c.real * c.real + c.imag * c.imag); 
  }
Bài tập 3   Hãy chuyển phương thức lớp sau đây thành phương thức đối lớp.

  public boolean equals(Complex b) { 
    return(real == b.real && imag == b.imag); 
  }
Bài tập 4  Bài tập này là sự tiếp nối theo Bài tập 3 của Chương 11. Mục đích là nhằm thực hành cú pháp của những phương thức đối tượng và làm quen với những thông báo lỗi có liên quan.

  1. Hãy chuyển các phương thức trong lớp Rational từ phương thức lớp sang phương thức đối tượng, đồng thời thực hiện những chuyển đổi cần thiết trong main.
  2. Cố ý mắc một số lỗi. Thử kích hoạt các phương thức lớp như thể chúng là phương thức đối tượng, và ngược lại. Hãy thử tìm hiểu xem thế nào là hợp lệ và thế nào không, và hiểu thông báo lỗi bạn nhận được khi mọi việc rối lên.
  3. Hãy nghĩ về ưu và nhược điểm của các phương thức lớp và phương thức đối tượng? Cách nào (thường) viết gọn hơn? Cách nào diễn đạt tính toán một cách tự nhiên hơn (hoặc xét công bằng, những kiểu phép tính nào có thể được diễn đạt một cách tự nhiên nhất theo mỗi phong cách)?
Bài tập 5   Mục đích của bài tập này là viết một chương trình để phát sinh ra những phần bài poker ngẫu nhiên rồi phân loại chúng, để ta ước tính được xác suất của các dạng phần bài khác nhau. Nếu bạn không chơi poker, bạn có thể đọc về nó ở đây http://en.wikipedia.org/wiki/List_of_poker_hands.

  1. Bắt đầu bằng http://thinkapjava.com/code/CardSoln3.java rồi đảm bảo chắc rằng bạn biên dịch và chạy được chương trình.
  2. Hãy viết lời định nghĩa cho một lớp có tên PokerHand (phần bài), mở rộng từ  Deck.
  3. Viết một phương thức trong Deck có tên deal để tạo ra một PokerHand, chuyển các lá bài từ cỗ bài vào phần bài, rồi trả lại phần bài này.
  4. Trong main, hãy dùng shuffle và deal để phát sinh và in ra bốn PokerHand, mỗi phần bài gồm 5 lá. Bạn có thu được kết quả tốt không?
  5. Viết một phương thức PokerHand có tên hasFlush để trả lại một giá trị boolean để chỉ định xem liệu phần bài này có một flush (5 lá đồng chất) hay không.
  6. Viết một phương thức có tên hasThreeKind để chỉ định xem liệu phần bài có bộ ba hay không.
  7. Viết một vòng lặp để phát sinh ra vài nghìn phần bài rồi kiểm tra xem chúng có chứa 5 lá đồng chất, hay bộ ba không. Ước tính xác suất để nhận được một trong hai dạng phần bài kể trên. Hãy so sánh kết quả thu được với các xác suất ở http://en.wikipedia.org/wiki/List_of_poker_hands.
  8. Viết các phương thức để kiểm tra cho những dạng phần bài khác. Có dạng dễ, có dạng khó. Đôi khi bạn sẽ thấy cần viết một vài phương thức trợ giúp phục vụ cho nhiều phép kiểm tra khác nhau.
  9. Có những trò chơi poker mà người chơi lấy 7 lá bài, rồi chọn ra 5 lá bài đẹp nhất. Hãy sửa lại chương trình của bạn để phát sinh ra các phần bài 7 lá rồi tính lại những xác suất nêu trên.

1 Phản hồi

Filed under Think Java

One response to “Chương 15: Lập trình hướng đối tượng

  1. Pingback: Think Java: Cách suy nghĩ như nhà khoa học máy tính | Blog của Chiến

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s