Mục lục bài họcĐang ở d08-b3
← IB Computer Science
0/32 bài đã học xong
Chương 8 · Option: Object-Oriented Programming and Databases · Bài 3/4 của chương · bài 31/32 của IB Computer Science

Classes in Detail: Constructors, Static Members, UML and Program Structure

Lớp chi tiết: hàm khởi tạo, thành viên tĩnh, UML và cấu trúc chương trình
← Mục lục bài học
Lý thuyết · English

Reading and writing UML class diagrams. A class box has three compartments: the name, the attributes and the methods. A minus sign means private, a plus sign public; attributes are almost always private (encapsulation) and are reached through public getters and setters. An underlined member is static: it belongs to the class rather than to any object, so there is one copy shared by all instances — a counter of how many accounts have been created, or a constant such as the maximum overdraft. A static method can be called without an object and can only use static data. An arrow with a hollow triangle points from a subclass to its superclass and reads "is a": SavingsAccount is an Account. A plain line with multiplicities (1, 0..*) is association: a Customer has many Accounts. The exam gives a diagram and asks for code, or gives code and asks for the diagram; the mapping must be exact, including types and visibility.

Constructors. A constructor has the class's name and no return type, and runs once when an object is created, to put it into a valid starting state. It usually takes the values that cannot have sensible defaults (the account number) and sets the rest (balance starts at 0). A class may have several constructors with different parameter lists (overloading). A subclass constructor first calls the superclass constructor to set the inherited part, then sets its own attributes: SavingsAccount(number, rate) calls Account(number) and then stores rate. Objects created without going through the constructor — or with a constructor that forgets a field — are the source of many null-reference errors.

Overriding and calling the parent. A subclass may replace an inherited method with its own version of the same signature: SavingsAccount overrides withdraw to refuse going below zero, CurrentAccount overrides it to allow an overdraft. The version that runs is chosen by the object's actual class at run time — polymorphism — so a loop over an array of Account references calls the right withdraw for each element without testing its type. An override may call the parent's version to reuse the common part and add to it.

Program structure and good practice. Split code into classes by responsibility; keep each method short and doing one thing; name things for what they represent; use constants instead of magic numbers; keep data private and expose behaviour. Libraries provide tested classes (collections, dates, files) so that you do not rewrite them. The advantages examiners expect: reuse, easier maintenance because change is local, teams can work on separate classes, and code that models the problem so that it can be read by someone who knows the problem but not the code. The costs: more design effort up front, and performance overhead from layers of objects — which is why embedded systems often avoid heavy object-oriented designs.

Giải thích tiếng Việt

Đọc và viết sơ đồ lớp UML. Một hộp lớp có ba ngăn: tên, thuộc tính và phương thức. Dấu trừ là riêng tư, dấu cộng là công khai; thuộc tính gần như luôn riêng tư (đóng gói) và được truy cập qua getter và setter công khai. Thành viên gạch chân là tĩnh: nó thuộc về lớp chứ không thuộc đối tượng nào, nên có một bản sao duy nhất dùng chung cho mọi thể hiện — bộ đếm số tài khoản đã tạo, hoặc một hằng như hạn mức thấu chi tối đa. Phương thức tĩnh gọi được mà không cần đối tượng và chỉ dùng được dữ liệu tĩnh. Mũi tên với tam giác rỗng trỏ từ lớp con lên lớp cha và đọc là "là một": SavingsAccount là một Account. Đường thẳng thường với bội số (1, 0..*) là liên kết: một Customer có nhiều Account. Đề thi cho sơ đồ và yêu cầu mã, hoặc cho mã và yêu cầu sơ đồ; phép ánh xạ phải chính xác, gồm cả kiểu và mức truy cập.

Hàm khởi tạo. Hàm khởi tạo mang tên lớp và không có kiểu trả về, chạy một lần khi đối tượng được tạo, để đưa nó vào trạng thái ban đầu hợp lệ. Nó thường nhận các giá trị không có mặc định hợp lý (số tài khoản) và đặt phần còn lại (số dư bắt đầu bằng 0). Một lớp có thể có nhiều hàm khởi tạo với danh sách tham số khác nhau (nạp chồng). Hàm khởi tạo của lớp con gọi hàm khởi tạo của lớp cha trước để đặt phần kế thừa, rồi đặt thuộc tính riêng: SavingsAccount(number, rate) gọi Account(number) rồi lưu rate. Đối tượng được tạo mà không qua hàm khởi tạo — hoặc với hàm khởi tạo quên một trường — là nguồn của nhiều lỗi tham chiếu rỗng.

Ghi đè và gọi lớp cha. Lớp con có thể thay một phương thức kế thừa bằng phiên bản riêng có cùng chữ ký: SavingsAccount ghi đè withdraw để từ chối xuống dưới 0, CurrentAccount ghi đè nó để cho phép thấu chi. Phiên bản chạy được chọn theo lớp thật của đối tượng lúc chạy — đa hình — nên một vòng lặp qua mảng tham chiếu Account gọi đúng withdraw cho mỗi phần tử mà không cần kiểm kiểu. Phương thức ghi đè có thể gọi phiên bản của cha để tái dùng phần chung và thêm vào.

Cấu trúc chương trình và thực hành tốt. Chia mã thành các lớp theo trách nhiệm; giữ mỗi phương thức ngắn và làm một việc; đặt tên theo thứ nó đại diện; dùng hằng thay cho số ma thuật; giữ dữ liệu riêng tư và lộ hành vi. Thư viện cung cấp các lớp đã kiểm thử (collection, ngày tháng, tệp) để bạn không viết lại. Các ưu điểm giám khảo mong đợi: tái dùng, bảo trì dễ hơn vì thay đổi là cục bộ, nhóm làm việc được trên các lớp riêng, và mã mô hình hoá bài toán nên người biết bài toán mà không biết mã vẫn đọc được. Cái giá: tốn công thiết kế trước, và chi phí hiệu năng từ các tầng đối tượng — đó là lý do hệ nhúng thường tránh thiết kế hướng đối tượng nặng.

SƠ ĐỒ LỚP UML: ĐỌC ĐƯỢC LÀ VIẾT ĐƯỢC MÃ Account - number: String - balance: double - count: int (tĩnh) + Account(number: String) + deposit(amount: double): void + getBalance(): double + getCount(): int (tĩnh) SavingsAccount - rate: double + SavingsAccount(number, rate) + addInterest(): void + withdraw(amount): boolean (ghi đè) CurrentAccount - overdraft: double + CurrentAccount(number, overdraft) + withdraw(amount): boolean (ghi đè) + setOverdraft(limit): void − riêng tư · + công khai · gạch chân = tĩnh, thuộc về lớp không thuộc đối tượng · tam giác rỗng = kế thừa, con LÀ MỘT cha hàm khởi tạo trùng tên lớp, không có kiểu trả về · withdraw ở hai lớp con khác nhau = đa hình; balance riêng tư nên con phải dùng deposit hoặc getter
Hình là một sơ đồ lớp UML cho ba lớp tài khoản ngân hàng, và mục tiêu của bài là đọc được sơ đồ này thành mã và ngược lại. Mỗi hộp có ba ngăn: tên lớp, thuộc tính, phương thức. Dấu trừ là riêng tư và dấu cộng là công khai; thuộc tính đều riêng tư theo nguyên tắc đóng gói, nên các lớp con không đọc thẳng balance mà phải qua deposit hoặc getter. Dòng gạch chân là thành viên tĩnh, thuộc về lớp chứ không thuộc từng đối tượng: count đếm số tài khoản đã tạo, và getCount trả về nó mà không cần một đối tượng nào. Dòng Account(number: String) là hàm khởi tạo: trùng tên lớp, không có kiểu trả về, chạy khi tạo đối tượng để đặt trạng thái ban đầu. Mũi tên tam giác rỗng trỏ từ hai lớp con lên Account là kế thừa, đọc là SavingsAccount là một Account; lớp con nhận mọi thuộc tính và phương thức công khai của cha, thêm thuộc tính riêng như rate hay overdraft, và ghi đè withdraw theo quy tắc riêng — tài khoản tiết kiệm từ chối rút quá số dư, tài khoản vãng lai cho phép tới hạn mức thấu chi. Cùng một lời gọi withdraw cho hành vi khác nhau tuỳ đối tượng thật là đa hình. Hàm khởi tạo của lớp con nhận thêm tham số riêng và gọi hàm khởi tạo của cha để đặt phần chung.
Ví dụ — Ví dụ 1 — từ sơ đồ UML sang mã, kèm hàm khởi tạo và thành viên tĩnh

Từ sơ đồ trong hình, viết lớp Account với bộ đếm tĩnh count tăng mỗi khi tạo tài khoản, và lớp SavingsAccount với withdraw ghi đè từ chối rút quá số dư. Giải thích vì sao count phải tĩnh và balance phải riêng tư.

Giải.

Lớp Account.

class Account
  private String number
  private double balance
  private static int count = 0
  public Account(String number)
    this.number = number
    this.balance = 0
    count = count + 1
  end constructor
  public void deposit(double amount)
    if amount > 0 then balance = balance + amount
  end method
  public boolean withdraw(double amount)
    balance = balance - amount
    return true
  end method
  public double getBalance() return balance
  public static int getCount() return count
end class

Lớp SavingsAccount.

class SavingsAccount extends Account
  private double rate
  public SavingsAccount(String number, double rate)
    super(number)
    this.rate = rate
  end constructor
  public boolean withdraw(double amount)
    if amount > getBalance() then return false
    return super.withdraw(amount)
  end method
  public void addInterest() deposit(getBalance() * rate)
end class

Vì sao count tĩnh. Số tài khoản đã tạo là thuộc tính của lớp, không của một tài khoản cụ thể nào; nếu count là thuộc tính thường, mỗi đối tượng có bản riêng và không bản nào biết tổng. Tĩnh nghĩa là một bản duy nhất, tăng trong hàm khởi tạo, đọc qua phương thức tĩnh mà không cần đối tượng.

Vì sao balance riêng tư. Nếu công khai, mã bên ngoài có thể đặt số dư âm hoặc bỏ qua kiểm tra của withdraw; riêng tư buộc mọi thay đổi đi qua deposit và withdraw, nơi quy tắc được kiểm. Hệ quả nhìn thấy trong mã: SavingsAccount là lớp con nhưng vẫn phải dùng getBalance và super.withdraw thay vì chạm thẳng balance — đóng gói áp dụng cả với lớp con.

Ví dụ — Ví dụ 2 — vì sao đa hình làm mã ngắn hơn và an toàn hơn khi thêm lớp mới

Một ngân hàng có mảng ACCOUNTS chứa cả SavingsAccount lẫn CurrentAccount và cần cuối tháng xử lý tất cả: cộng lãi cho tiết kiệm, tính phí thấu chi cho vãng lai. Hai cách viết: (A) kiểm kiểu từng phần tử rồi gọi phương thức tương ứng; (B) khai báo phương thức monthEnd() ở Account, ghi đè ở mỗi lớp con, và gọi ACCOUNTS[I].monthEnd() trong một vòng lặp. So sánh, rồi xét việc thêm lớp StudentAccount sau một năm.

Giải.

Cách A. loop I from 0 to N − 1: if ACCOUNTS[I] is SavingsAccount then addInterest, else if CurrentAccount then chargeOverdraft. Nó chạy đúng hôm nay, nhưng logic loại tài khoản nào làm gì nằm ở nơi gọi, và mọi nơi khác có vòng lặp tương tự — in sao kê, tính thuế — đều lặp lại chuỗi if đó.

Cách B. Account khai báo monthEnd() (có thể rỗng hoặc trừu tượng); SavingsAccount ghi đè để cộng lãi; CurrentAccount ghi đè để tính phí. Vòng lặp chỉ còn ACCOUNTS[I].monthEnd(), và phiên bản đúng được chọn lúc chạy theo lớp thật của đối tượng. Logic của mỗi loại nằm trong lớp đó, đúng nơi người bảo trì tìm.

Thêm StudentAccount sau một năm. Với A, phải tìm mọi vòng lặp có chuỗi if trong toàn chương trình và thêm một nhánh; bỏ sót một chỗ là lỗi im lặng — tài khoản sinh viên bị bỏ qua cuối tháng mà không ai báo. Với B, viết lớp mới với monthEnd() riêng, và mọi vòng lặp hiện có tự động xử lý nó mà không sửa một dòng nào. Đây là lý do thật của đa hình: không phải mã ngắn hơn, mà là thay đổi được khoanh vùng trong lớp mới thay vì rải khắp chương trình.

Cái giá cần nêu. Cách B đòi thiết kế trước — quyết định monthEnd() thuộc về Account từ đầu — và khi đọc mã, người ta không thấy ngay điều gì xảy ra ở dòng ACCOUNTS[I].monthEnd() mà phải mở từng lớp con. Với chương trình hai loại tài khoản không bao giờ đổi, cách A không sai; đa hình trả công khi hệ thống lớn lên.

Bẫy hay mất điểm — Bẫy 1 — viết hàm khởi tạo có kiểu trả về hoặc tên khác lớp. Hàm khởi tạo trùng tên lớp, không có kiểu trả về, và lớp con gọi hàm khởi tạo của cha trước.
Bẫy 2 — đặt bộ đếm số đối tượng làm thuộc tính thường. Mỗi đối tượng có bản riêng; thứ thuộc về lớp phải tĩnh.
Bẫy 3 — lớp con chạm thẳng thuộc tính riêng tư của cha. Đóng gói áp dụng cả với lớp con: dùng getter và phương thức của cha.
Phải nhớ — Chốt bài 3. Bốn thứ mang vào phòng thi. Một, UML ba ngăn, trừ riêng cộng công, gạch chân tĩnh, tam giác rỗng kế thừa. Hai, hàm khởi tạo trùng tên lớp, không kiểu trả về, con gọi cha trước. Ba, ghi đè cùng chữ ký, chọn theo lớp thật lúc chạy, và lợi ích thật là khoanh vùng thay đổi. Bốn, cấu trúc theo trách nhiệm, hằng thay số ma thuật, dữ liệu riêng tư.

Đọc xong rồi — làm thử ngay

Bài tập của chương Option: Object-Oriented Programming and Databases gồm 14 câu trắc nghiệm và 5 đề tự luận. Đáp án hiện ngay khi chọn, miễn phí.

Làm bài tập chương →