1. Trừu tượng hóa trong ứng dụng thực tế: vì sao cần
Ở các bài trước, chúng ta đã làm quen với khái niệm trừu tượng hóa và xem các ví dụ đơn giản. Giờ hãy xem cách tiếp cận này hoạt động trong các bài toán thực tế hơn. Trong các dự án thực, hầu như luôn phải làm việc với các đối tượng khác nhau nhưng tương tự nhau. Ví dụ, các phương thức thanh toán khác nhau, các loại phương tiện khác nhau, các hình khác nhau trong trình biên tập đồ họa. Nếu không áp dụng trừu tượng hóa, mã nhanh chóng biến thành tập hợp “if-else” và copy-paste. Còn với trừu tượng hóa — mọi thứ chặt chẽ, đẹp và quan trọng nhất là thuận tiện cho phát triển và bảo trì.
Trừu tượng hóa cho phép:
- Ẩn chi tiết hiện thực: làm việc với đối tượng qua một giao diện chung, không cần quan tâm bên trong nó được cài đặt ra sao.
- Tránh trùng lặp mã: hành vi và trường chung được đưa lên lớp cơ sở.
- Dễ dàng mở rộng hệ thống: thêm loại đối tượng mới không đòi hỏi viết lại mã cũ.
- Làm cho mã linh hoạt: có thể thay thế một hiện thực bằng hiện thực khác mà không đổi phần còn lại của hệ thống.
Hãy cùng xem một vài ví dụ ở các lĩnh vực khác nhau.
2. Ví dụ 1: Hệ thống thanh toán
Đề bài
Giả sử bạn viết mô-đun cho cửa hàng trực tuyến. Nhiệm vụ của bạn là hiện thực xử lý các loại thanh toán khác nhau: thẻ ngân hàng, PayPal, tiền mã hóa. Tất cả đều phải “xử lý thanh toán”, nhưng mỗi loại có chi tiết riêng.
Trừu tượng hóa: lớp Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Phương thức trừu tượng: cách xử lý thanh toán cụ thể — do các lớp con quyết định
public abstract void process();
// Phương thức chung cho mọi khoản thanh toán
public void printAmount() {
System.out.println("Số tiền thanh toán: " + amount + " RUB");
}
}
Hiện thực cụ thể
public class CreditCardPayment extends Payment {
private String cardNumber;
public CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
public void process() {
System.out.println("Xử lý thanh toán bằng thẻ: " + cardNumber);
// Ở đây có thể là tích hợp với ngân hàng :)
}
}
public class PaypalPayment extends Payment {
private String email;
public PaypalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public void process() {
System.out.println("Xử lý thanh toán PayPal cho tài khoản: " + email);
// Và ở đây — gọi PayPal API
}
}
public class CryptoPayment extends Payment {
private String walletAddress;
public CryptoPayment(double amount, String walletAddress) {
super(amount);
this.walletAddress = walletAddress;
}
@Override
public void process() {
System.out.println("Xử lý thanh toán tiền mã hóa tới ví: " + walletAddress);
// Ở đây có thể có phép màu blockchain
}
}
Sử dụng trừu tượng hóa
import java.util.*;
public class PaymentDemo {
public static void main(String[] args) {
List<Payment> payments = new ArrayList<>();
payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
payments.add(new PaypalPayment(500.0, "user@example.com"));
payments.add(new CryptoPayment(0.05, "0xABCD..."));
for (Payment payment : payments) {
payment.printAmount();
payment.process();
System.out.println("---");
}
}
}
Kết quả chạy:
Số tiền thanh toán: 1500.0 RUB
Xử lý thanh toán bằng thẻ: 1234 5678 9012 3456
---
Số tiền thanh toán: 500.0 RUB
Xử lý thanh toán PayPal cho tài khoản: user@example.com
---
Số tiền thanh toán: 0.05 RUB
Xử lý thanh toán tiền mã hóa tới ví: 0xABCD...
---
Ưu điểm:
- Có thể thêm phương thức thanh toán mới mà không đổi mã cũ (ví dụ, Apple Pay).
- Mã làm việc với thanh toán không phụ thuộc vào kiểu cụ thể của chúng.
- Logic chung (ví dụ, in số tiền qua printAmount()) được hiện thực một nơi.
3. Ví dụ 2: Phương tiện vận tải
Đề bài
Trong trò chơi hay mô phỏng, bạn có các loại phương tiện khác nhau: ô tô, xe đạp, tàu hỏa. Tất cả đều có thể “di chuyển”, nhưng cách thực hiện khác nhau. Một số cần tiếp nhiên liệu, số khác thì không.
Trừu tượng hóa: lớp Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// Không phải mọi loại phương tiện đều cần tiếp nhiên liệu; mặc định là không
public void fuelUp() {
System.out.println(name + ": không cần tiếp nhiên liệu.");
}
}
Hiện thực cụ thể
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " chạy trên đường.");
}
@Override
public void fuelUp() {
System.out.println(name + ": tiếp nhiên liệu bằng xăng.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " đạp bàn đạp.");
}
// không ghi đè fuelUp — xe đạp không cần tiếp nhiên liệu
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " lao vùn vụt trên đường ray.");
}
@Override
public void fuelUp() {
System.out.println(name + ": tiếp nhiên liệu bằng dầu diesel hoặc điện.");
}
}
Sử dụng trừu tượng hóa
import java.util.*;
public class TransportDemo {
public static void main(String[] args) {
List<Transport> vehicles = Arrays.asList(
new Car("Toyota"),
new Bicycle("Stels"),
new Train("Sapsan")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
Kết quả chạy:
Toyota chạy trên đường.
Toyota: tiếp nhiên liệu bằng xăng.
---
Stels đạp bàn đạp.
Stels: không cần tiếp nhiên liệu.
---
Sapsan lao vùn vụt trên đường ray.
Sapsan: tiếp nhiên liệu bằng dầu diesel hoặc điện.
---
Ưu điểm:
- Có thể xử lý mọi phương tiện giống nhau mà không cần kiểm tra kiểu của chúng.
- Dễ thêm loại phương tiện mới (ví dụ, xe scooter điện).
4. Ví dụ 3: Trình biên tập đồ họa
Đề bài
Bạn viết một trình biên tập đồ họa mini. Trong đó có đường thẳng, elip, đa giác — tất cả đều là “hình” có thể vẽ và thay đổi kích thước. Mỗi hình tự hiện thực các hành động này theo cách riêng.
Trừu tượng hóa: lớp Figure
public abstract class Figure {
protected String color = "black";
public abstract void draw();
public abstract void resize(double factor);
public void setColor(String color) {
this.color = color;
}
}
Hiện thực cụ thể
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Vẽ một đường thẳng dài " + length + " với màu " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Độ dài mới của đường thẳng: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("Vẽ một hình elip với các trục " + a + " và " + b + " với màu " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Kích thước mới của elip: " + a + " x " + b);
}
}
public class Polygon extends Figure {
private int sides;
public Polygon(int sides) {
this.sides = sides;
}
@Override
public void draw() {
System.out.println("Vẽ một đa giác với " + sides + " cạnh, màu " + color);
}
@Override
public void resize(double factor) {
System.out.println("Thay đổi kích thước đa giác có " + sides + " cạnh theo " + factor);
}
}
Sử dụng trừu tượng hóa
import java.util.*;
public class EditorDemo {
public static void main(String[] args) {
List<Figure> figures = new ArrayList<>();
figures.add(new Line(10));
figures.add(new Ellipse(5, 3));
figures.add(new Polygon(6));
for (Figure f : figures) {
f.setColor("green");
f.draw();
f.resize(2);
System.out.println("---");
}
}
}
Kết quả chạy:
Vẽ một đường thẳng dài 10.0 với màu green
Độ dài mới của đường thẳng: 20.0
---
Vẽ một hình elip với các trục 5.0 và 3.0 với màu green
Kích thước mới của elip: 10.0 x 6.0
---
Vẽ một đa giác với 6 cạnh, màu green
Thay đổi kích thước đa giác có 6 cạnh theo 2.0
---
Ưu điểm:
- Tất cả các hình có thể lưu trong một danh sách và xử lý giống nhau.
- Dễ thêm hình mới (ví dụ, ngôi sao hoặc trái tim).
- Các phương thức chung (ví dụ, đặt màu qua setColor()) được hiện thực một lần.
5. Trừu tượng hóa giúp đơn giản hóa mã như thế nào
Trong mỗi ví dụ trên có một sơ đồ chung:
- Lớp trừu tượng cơ sở đặt ra hợp đồng (đối tượng làm được gì).
- Các lớp con cụ thể hiện thực chi tiết.
- Mã làm việc với trừu tượng không phụ thuộc vào kiểu đối tượng, giúp hệ thống linh hoạt và dễ mở rộng.
Bảng so sánh cách tiếp cận
| Không có trừu tượng hóa (if-else) | Có trừu tượng hóa (OOP) |
|---|---|
| Nhiều điều kiện theo kiểu | Kiểu mới — phải sửa mã |
| Trùng lặp logic | Logic — tập trung một chỗ |
| Khó mở rộng | Bổ sung — dễ dàng |
| Khó kiểm thử | Dễ thay thế các triển khai |
6. Các lỗi thường gặp khi thiết kế trừu tượng hóa
Lỗi số 1: Trừu tượng hóa vì trừu tượng hóa.
Nếu bạn chỉ có một kiểu đối tượng và không dự định mở rộng — không cần lớp trừu tượng. Đừng làm phức tạp mã mà không có lý do.
Lỗi số 2: Trừu tượng hóa quá chung chung.
Nếu lớp cơ sở quá “mơ hồ”, các lớp con có thể chẳng có điểm chung nào ngoài cái tên. Ví dụ, trừu tượng “Thing” cho mọi thứ tùy tiện. Điều này gây khó khăn cho việc bảo trì và hiểu mã.
Lỗi số 3: Trùng lặp mã trong các lớp con.
Nếu tất cả các lớp con đều có cùng một hiện thực của một phương thức, hãy đưa nó lên lớp cơ sở (không để trừu tượng).
Lỗi số 4: Vi phạm nguyên tắc “từ tổng quát đến cụ thể”.
Nếu trong lớp trừu tượng xuất hiện các chi tiết chỉ cần cho một lớp con — tức là bạn đã chọn trừu tượng hóa sai.
Lỗi số 5: Quên hiện thực các phương thức trừu tượng.
Nếu không hiện thực đầy đủ các phương thức trừu tượng trong lớp con, trình biên dịch sẽ buộc lớp đó cũng phải là trừu tượng. Đôi khi điều này khiến bạn bất ngờ :)
GO TO FULL VERSION