1. 真實應用中的抽象:為什麼需要它
在前面的課程中,我們已經認識了抽象的概念並看過簡單範例。現在來看看這個方法在更真實的任務中是如何運作的。 在實際專案中,幾乎總會面對不同但相似的物件。例如,不同的支付方式、不同的交通工具、圖形編輯器中的不同圖形。若不運用抽象,程式碼很快就會變成一堆「if-else」和重複貼上的片段。有了抽象——一切更嚴謹、漂亮,而且最重要的是,便於擴展與維護。
抽象可以:
- 隱藏實作細節:透過共同介面與物件互動,不必關心其內部實作。
- 避免程式碼重複:共同行為與欄位抽取到基底類別。
- 容易擴充系統:新增新的物件類型不需要重寫舊程式碼。
- 讓程式碼更靈活:可在不改動其餘系統的情況下替換實作。
讓我們從不同領域挑幾個例子來看看。
2. 範例 1:支付系統
任務說明
假設你在為一個網路商店撰寫模組。你的任務是實作不同類型的支付處理:信用卡、PayPal、加密貨幣。它們都需要「處理付款」,但每種方式的細節各不相同。
抽象:Payment 類別
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// 抽象方法:如何具體處理付款 — 由子類決定
public abstract void process();
// 所有支付方式的通用方法
public void printAmount() {
System.out.println("付款金額: " + amount + " 盧布");
}
}
具體實作
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("處理信用卡付款: " + cardNumber);
// 這裡本可以與銀行整合 :)
}
}
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("處理 PayPal 付款,帳號: " + email);
// 而這裡 — 呼叫 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("處理加密貨幣付款,錢包位址: " + walletAddress);
// 這裡本可以有區塊鏈魔法
}
}
抽象的使用
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("---");
}
}
}
執行結果:
付款金額: 1500.0 盧布
處理信用卡付款: 1234 5678 9012 3456
---
付款金額: 500.0 盧布
處理 PayPal 付款,帳號: user@example.com
---
付款金額: 0.05 盧布
處理加密貨幣付款,錢包位址: 0xABCD...
---
優點:
- 可以新增新的支付方式而不需修改舊程式碼(例如 Apple Pay)。
- 處理支付的程式碼不依賴其具體型別。
- 共用邏輯(例如透過 printAmount() 列印金額)只需在一處實作。
3. 範例 2:交通工具
任務說明
在遊戲或模擬中,你有不同種類的交通工具:汽車、自行車、火車。它們都會「移動」,但方式各不相同。有些需要加油,有些則不需要。
抽象:Transport 類別
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// 並非所有交通工具都需要加油,預設為不需要
public void fuelUp() {
System.out.println(name + ": 不需要加油。");
}
}
具體實作
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " 在道路上行駛。");
}
@Override
public void fuelUp() {
System.out.println(name + ": 加汽油。");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " 踩踏板前進。");
}
// 不覆寫 fuelUp — 自行車不需要加油
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " 在鐵軌上飛馳。");
}
@Override
public void fuelUp() {
System.out.println(name + ": 補給柴油或電力。");
}
}
抽象的使用
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("---");
}
}
}
執行結果:
Toyota 在道路上行駛。
Toyota: 加汽油。
---
Stels 踩踏板前進。
Stels: 不需要加油。
---
Sapsan 在鐵軌上飛馳。
Sapsan: 補給柴油或電力。
---
優點:
- 可以用同一種方式處理任何交通工具,而不必檢查其型別。
- 容易新增新的交通工具(例如電動滑板車)。
4. 範例 3:圖形編輯器
任務說明
你要撰寫一個迷你圖形編輯器。當中有直線、橢圓形、多邊形——它們都是「圖形」,可以被繪製並調整尺寸,而且每種圖形會以自己的方式實作這些操作。
抽象: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;
}
}
具體實作
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("繪製長度為 " + length + " 、顏色為 " + color + " 的線條");
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("線條的新長度: " + 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("繪製軸長 " + a + " 與 " + b + " 、顏色為 " + color + " 的橢圓");
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("橢圓的新尺寸: " + 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("繪製具有 " + sides + " 條邊、顏色為 " + color + " 的多邊形");
}
@Override
public void resize(double factor) {
System.out.println("將具有 " + sides + " 條邊的多邊形縮放為 " + factor);
}
}
抽象的使用
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("---");
}
}
}
執行結果:
繪製長度為 10.0 、顏色為 green 的線條
線條的新長度: 20.0
---
繪製軸長 5.0 與 3.0 、顏色為 green 的橢圓
橢圓的新尺寸: 10.0 x 6.0
---
繪製具有 6 條邊、顏色為 green 的多邊形
將具有 6 條邊的多邊形縮放為 2.0
---
優點:
- 可以將所有圖形放在同一個清單中並以相同方式處理。
- 容易新增新的圖形(例如星形或愛心)。
- 共用方法(例如透過 setColor() 設定顏色)只需實作一次。
5. 抽象如何幫助簡化程式碼
以上每個範例都有共同的模式:
- 基底抽象類別 定義契約(物件能做什麼)。
- 具體子類 負責實作細節。
- 使用抽象的程式碼 不依賴物件型別,使系統更靈活且可擴充。
方法比較表
| 沒有抽象(if-else) | 具有抽象(OOP) |
|---|---|
| 大量依賴型別的條件 | 新型別 — 需要改動程式碼 |
| 邏輯重複 | 邏輯集中在一處 |
| 難以擴充 | 新增 — 容易 |
| 難以測試 | 容易替換實作 |
6. 設計抽象時的常見錯誤
錯誤 1:為了抽象而抽象。
如果你只有一種物件類型,且不打算擴充——就不需要抽象類別。不要無故複雜化程式碼。
錯誤 2:抽象過於籠統。
如果基底類別過於「模糊」,子類可能除了名字外毫無共通點。例如,拿「Thing」當作萬用抽象。這會讓維護與理解變得困難。
錯誤 3:在子類中重複程式碼。
如果所有子類對某個方法的實作都相同,就應該把它上移到基底類別(改為非抽象)。
錯誤 4:違反「由一般到特殊」的原則。
如果抽象類別出現只對某個子類才需要的細節——代表抽象選錯了。
錯誤 5:忘了實作抽象方法。
如果沒有在子類中實作所有抽象方法,編譯器會強制將該類別也標記為抽象。有時這會讓人意外 :)
GO TO FULL VERSION