1. Hệ phân cấp lớp là gì?
Trong lập trình (và không chỉ trong đó), hệ phân cấp là một cấu trúc dạng cây, nơi chúng ta có các thực thể “chung” ở đỉnh cây (các lớp cơ sở), và bên dưới là các thực thể cụ thể hơn (các lớp con). Trong Java, hệ phân cấp lớp được xây dựng bằng kế thừa: mỗi lớp con lại có thể là cha của các lớp con khác, v.v.
Tương tự:
- Động vật (Animal) — là lớp tổng quát.
- Động vật có vú (Mammal) — là trường hợp cụ thể của động vật.
- Chó (Dog) — là trường hợp cụ thể của động vật có vú.
- Còn “Sharik” cụ thể — chính là một đối tượng của lớp Dog.
Trong mã, nó trông như sau:
class Animal { }
class Mammal extends Animal { }
class Dog extends Mammal { }
Hệ phân cấp lớp cho phép mô tả các thuộc tính và hành vi chung “ở trên”, còn chi tiết — “ở dưới”. Điều này khiến mã logic hơn và loại bỏ sự trùng lặp.
Sơ đồ hệ phân cấp
Animal
├── Mammal
│ ├── Dog
│ └── Cat
└── Bird
└── Sparrow
2. Cách xây dựng hệ phân cấp: logic và thực hành
Xác định phần chung và phần riêng
Quy tắc chính: lớp cơ sở phải chứa những gì đặc trưng cho tất cả hậu duệ của nó. Còn mọi thứ độc nhất — hãy đưa xuống các lớp con.
Ví dụ:
- Tất cả động vật đều có thể thở và ăn — vậy các phương thức breathe() và eat() nên nằm ở lớp Animal.
- Chỉ có chim mới biết bay — vậy phương thức fly() sẽ ở lớp Bird, không phải ở Animal.
- Chỉ chó mới biết sủa — phương thức bark() sẽ ở lớp Dog.
Ví dụ: Động vật
// Lớp cơ sở
class Animal {
String name;
Animal(String name) {
this.name = name;
}
void eat() {
System.out.println(name + " ăn.");
}
void makeSound() {
System.out.println(name + " phát ra âm thanh.");
}
}
// Lớp con: Động vật có vú
class Mammal extends Animal {
Mammal(String name) {
super(name);
}
void feedMilk() {
System.out.println(name + " nuôi con bằng sữa.");
}
}
// Lớp con: Chó
class Dog extends Mammal {
Dog(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " sủa: Gâu gâu!");
}
void wagTail() {
System.out.println(name + " vẫy đuôi.");
}
}
// Lớp con: Mèo
class Cat extends Mammal {
Cat(String name) {
super(name);
}
@Override
void makeSound() {
System.out.println(name + " kêu: Meo!");
}
void purr() {
System.out.println(name + " gừ gừ.");
}
}
// Lớp con: Chim
class Bird extends Animal {
Bird(String name) {
super(name);
}
void fly() {
System.out.println(name + " bay.");
}
@Override
void makeSound() {
System.out.println(name + " hót: Chíp chíp!");
}
}
Gọi trong main:
public class ZooDemo {
public static void main(String[] args) {
Dog sharik = new Dog("Sharik");
Cat murka = new Cat("Murka");
Bird sparrow = new Bird("Chim sẻ");
sharik.eat(); // Sharik ăn.
sharik.makeSound(); // Sharik sủa: Gâu gâu!
sharik.feedMilk(); // Sharik nuôi con bằng sữa.
sharik.wagTail(); // Sharik vẫy đuôi.
murka.eat(); // Murka ăn.
murka.makeSound(); // Murka kêu: Meo!
murka.feedMilk(); // Murka nuôi con bằng sữa.
murka.purr(); // Murka gừ gừ.
sparrow.eat(); // Chim sẻ ăn.
sparrow.makeSound(); // Chim sẻ hót: Chíp chíp!
sparrow.fly(); // Chim sẻ bay.
}
}
Trực quan: cây lớp
| Lớp | Cha | Đặc điểm |
|---|---|---|
|
|
name, eat(), makeSound() |
|
|
feedMilk() |
|
|
makeSound(), wagTail() |
|
|
makeSound(), purr() |
|
|
fly(), makeSound() |
3. Thêm ví dụ đời thực
Các hình hình học
Hệ phân cấp lớp rất phù hợp để mô hình hóa hình học.
// Lớp cơ sở
class Shape {
void draw() {
System.out.println("Vẽ hình.");
}
}
// Hình tròn
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void draw() {
System.out.println("Vẽ hình tròn có bán kính " + radius);
}
}
// Hình chữ nhật
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
void draw() {
System.out.println("Vẽ hình chữ nhật " + width + "x" + height);
}
}
Sử dụng:
public class ShapeDemo {
public static void main(String[] args) {
Shape s1 = new Circle(5);
Shape s2 = new Rectangle(3, 4);
s1.draw(); // Vẽ hình tròn có bán kính 5.0
s2.draw(); // Vẽ hình chữ nhật 3.0x4.0
}
}
Phương tiện giao thông
class Vehicle {
void move() {
System.out.println("Phương tiện di chuyển.");
}
}
class Car extends Vehicle {
@Override
void move() {
System.out.println("Ô tô chạy trên đường.");
}
}
class Bicycle extends Vehicle {
@Override
void move() {
System.out.println("Người đi xe đạp đạp bàn đạp.");
}
}
main:
Vehicle v1 = new Car();
Vehicle v2 = new Bicycle();
v1.move(); // Ô tô chạy trên đường.
v2.move(); // Người đi xe đạp đạp bàn đạp.
Người dùng
class User {
String username;
User(String username) { this.username = username; }
void login() { System.out.println(username + " đã đăng nhập vào hệ thống."); }
}
class Admin extends User {
Admin(String username) { super(username); }
void deleteUser(String user) {
System.out.println(username + " đã xóa người dùng " + user);
}
}
class Customer extends User {
Customer(String username) { super(username); }
void buy() { System.out.println(username + " đã mua hàng."); }
}
4. Những lưu ý hữu ích
Đừng lạm dụng kế thừa. Kế thừa là công cụ cho quan hệ “is-a”. Nếu bạn muốn nói “Mèo là Động vật”, hãy dùng kế thừa. Còn nếu “Mèo có đuôi”, hãy dùng hợp thành (has-a).
Không tốt:
class Engine { /* ... */ }
class Car extends Engine { /* Ô tô là động cơ ư? Không, đây là hợp thành! */ }
Tốt:
class Car {
Engine engine; // Ô tô có động cơ
}
Đừng tạo hệ phân cấp quá sâu. Cấp càng nhiều — càng khó bảo trì và hiểu mã. Thường 2–3 cấp là tối đa cho phần lớn bài toán.
Đừng tạo hệ phân cấp quá “phẳng”. Nếu bạn có 20 lớp đều kế thừa trực tiếp từ một lớp cơ sở, có lẽ nên xem lại kiến trúc.
Kế thừa “cho tiện”. Đôi khi ta muốn “mượn” phương thức hay trường mà không xem xét logic. Đó là con đường dẫn tới hỗn loạn! Nếu class A và class B không liên quan về mặt logic, đừng làm class B extends A chỉ vì vài phương thức.
Vi phạm nguyên lý thay thế của Liskov. Nếu lớp con không thể được dùng thay cho lớp cha mà không gây bất ngờ — hệ phân cấp đã được xây sai.
Lặp lại mã. Nếu bạn nhận ra “ồ, phương thức này đang bị sao chép từ lớp này sang lớp khác”, có lẽ đã đến lúc đưa nó lên lớp cơ sở.
5. Các lỗi thường gặp khi xây dựng hệ phân cấp lớp
Lỗi số 1: Kế thừa mà không có logic “is-a”.
Nếu bạn dùng kế thừa chỉ để truy cập phương thức hoặc trường, chứ không phải vì lớp con thực sự “là” lớp cha, mã của bạn sẽ nhanh chóng thành mớ hỗn độn. Ví dụ, “Ô tô kế thừa Động cơ” — đó không phải is-a, mà là has-a.
Lỗi số 2: Bỏ qua đặc điểm riêng của lớp con.
Nếu mọi lớp con đều trông giống nhau và không thêm điều gì mới, có lẽ hệ phân cấp là không cần thiết — hãy dùng một lớp.
Lỗi số 3: Lặp lại mã trong các lớp con.
Nếu bạn sao chép cùng một hiện thực vào nhiều lớp con, hãy đưa nó lên lớp cha.
Lỗi số 4: Hệ phân cấp quá phức tạp hoặc quá sâu.
Hệ phân cấp nhiều tầng rất khó bảo trì và kiểm thử. Càng đơn giản — càng tốt!
Lỗi số 5: Ghi đè phương thức mà không có chú thích @Override.
Không có chú thích này, rất dễ sai chữ ký phương thức, và khi đó phương thức sẽ không được ghi đè, mà sẽ bị coi là mới. Luôn sử dụng @Override!
GO TO FULL VERSION