1. 多層抽象
當你剛開始寫程式,一切看起來很簡單:寫好類別、呼叫方法、得到結果。但在真實專案中事情會變得複雜:數十個類別、上百個方法、上千行程式碼……而且如果是團隊合作——難度只會更高!如何不在這個混亂中迷失?
答案——把複雜拆成簡單,更好的是——拆成抽象層級。
什麼是抽象層級?
把它想成一棟大樓的樓層:每一層都有自己的活動,但彼此相互關聯。在程式設計中,慣例上會劃分出這些層(層級):
- 使用者介面(UI)——使用者所看到的一切。
- 商業邏輯——實作應用程式本質的規則與流程。
- 資料存取(DAO、Repository)——與資料庫或檔案打交道。
每一層都只面向抽象,不了解其他層的細節。例如,對商業邏輯而言,UI 如何實作或資料如何儲存並不重要——重要的是存在像 saveOrder() 或 findUserById() 這樣的方法。
生活中的類比
想像一家餐廳。客人(UI)透過服務生(介面抽象)下單,廚師(商業邏輯)負責烹飪,庫管(資料存取)負責倉庫的食材存量。客人不知道廚師如何烹飪,廚師也不關心馬鈴薯放在哪——重要的是需要時能拿得到。
2. 範例:多層架構實作
讓我們擴充教學專案——例如一個任務管理(task manager)應用。我們已會為任務建立類別,現在把應用拆成多層。
確立抽象
- Task——任務的抽象描述:任務有名稱、狀態,以及用來執行的相關方法。
- TaskRepository——用於儲存任務的抽象(不關心位置——記憶體、檔案或資料庫)。
- TaskService——商業邏輯:新增任務、查找、執行。
抽象類別與介面
// 商業邏輯層
public abstract class Task {
private String title;
private boolean completed;
public Task(String title) {
this.title = title;
this.completed = false;
}
public abstract void complete();
public String getTitle() { return title; }
public boolean isCompleted() { return completed; }
protected void setCompleted(boolean completed) { this.completed = completed; }
}
// 資料儲存層(抽象)
public interface TaskRepository {
void save(Task task);
Task findByTitle(String title);
List<Task> findAll();
}
各層的實作
實作 Task
public class WorkTask extends Task {
private String deadline;
public WorkTask(String title, String deadline) {
super(title);
this.deadline = deadline;
}
@Override
public void complete() {
setCompleted(true);
System.out.println("工作任務 '" + getTitle() + "' 已於截止日期 " + deadline + " 完成");
}
}
實作 TaskRepository
public class InMemoryTaskRepository implements TaskRepository {
private List<Task> tasks = new ArrayList<>();
@Override
public void save(Task task) {
tasks.add(task);
}
@Override
public Task findByTitle(String title) {
for (Task task : tasks) {
if (task.getTitle().equals(title)) {
return task;
}
}
return null;
}
@Override
public List<Task> findAll() {
return new ArrayList<>(tasks);
}
}
實作 TaskService
public class TaskService {
private TaskRepository repository;
public TaskService(TaskRepository repository) {
this.repository = repository;
}
public void addTask(Task task) {
repository.save(task);
}
public void completeTask(String title) {
Task task = repository.findByTitle(title);
if (task != null) {
task.complete();
} else {
System.out.println("找不到任務:" + title);
}
}
public void showAllTasks() {
for (Task task : repository.findAll()) {
System.out.println(task.getTitle() + " — " + (task.isCompleted() ? "已完成" : "未完成"));
}
}
}
在主類別中的使用
public class Main {
public static void main(String[] args) {
TaskRepository repo = new InMemoryTaskRepository();
TaskService service = new TaskService(repo);
service.addTask(new WorkTask("撰寫報告", "2025-07-15"));
service.addTask(new WorkTask("準備簡報", "2025-07-16"));
service.showAllTasks();
service.completeTask("撰寫報告");
service.showAllTasks();
}
}
我們得到了什麼?
- 主程式類別(Main)不知道任務儲存如何實作——它只與抽象的 TaskRepository 互動。
- TaskService 不知道有哪種任務型別——它只與抽象類別 Task 互動。
- 如果明天想把任務存到資料庫而不是記憶體——只要實作新的 DatabaseTaskRepository 類別,無需重寫商業邏輯與 UI。
- 如果新增一種任務型別,例如 HomeTask,——只需新增一個子類別。
3. 對團隊協作的優勢
在大型專案中,很少有人能包辦所有事情。團隊通常會分成「前端」、「後端」、「儲存庫開發」等等。抽象如何幫助他們彼此不踩線?
責任分工
每個人都在自己對應的抽象層上工作。
- 一位開發者編寫與資料庫對接的 TaskRepository 實作。
- 另一位專注於商業邏輯(TaskService)。
- 第三位負責使用者介面。
層與層之間的契約由抽象來確立。
只要大家同意 TaskRepository 具有 save、findByTitle、findAll 這些方法——實作細節就不重要。
易於測試與替換元件
- 可以輕鬆替換一個實作為另一個(例如測試時用 InMemoryTaskRepository,而在正式環境用資料庫)。
- 測試人員可以以「假物件」(mock)或 stub 取代資料層,從而隔離測試商業邏輯。
獨立演進
- 若有人想新增新的任務型別,不會破壞現有程式碼——只要實作 Task 的新子類即可。
- 若出現新的資料儲存方式,只需更換介面的實作,其餘程式碼不必變動。
4. 最佳實務:如何不濫用抽象
抽象就像菜裡的鹽:沒有就清淡,放多了就毀了。以下是幾個建議:
只在能真正簡化系統的地方使用抽象。
不要為了抽象而抽象。如果你只有一種任務型別,也許根本不需要抽象。
為抽象類別與方法撰寫文件。
好的文件能幫助理解繼承者應該實作什麼,以及為什麼需要這樣做。
讓抽象具有明確意義。
抽象類別應該表達真正共通的行為與/或狀態。
不要混淆責任。
不要把只屬於某個子類別才需要的方法硬塞進抽象類別。
5. 大型系統中的抽象:實際案例
看看抽象在大型專案中如何運作——以電商網站為例。
系統分層
- 控制器(UI):接收使用者的請求(例如「提交訂單」)。
- 服務(商業邏輯):檢查存貨、計算折扣、建立訂單。
- 儲存庫(資料存取):把訂單、商品、使用者保存到資料庫。
抽象範例
// 訂單處理服務的抽象
public interface OrderService {
void createOrder(Order order);
Order findOrderById(String id);
}
// 訂單儲存的抽象
public interface OrderRepository {
void save(Order order);
Order findById(String id);
}
每一層只了解自己的抽象。如果明天決定把訂單存到雲端——只需要更換 OrderRepository 的實作。
各層交互——示意
[UI/Controller] <--> [OrderService (抽象)] <--> [OrderRepository (抽象)] <--> [資料庫]
- 每一層都只面向抽象,不了解下層的細節。
- 這讓各層得以獨立開發、測試與演進。
抽象與程式碼維護
- 容易新增功能(新的任務類型、支付方式、運輸方式)。
- 容易修正錯誤(在一處修掉 bug——所有子類都能得到更新)。
- 容易測試(可將各層替換成「stub」或 mock 來做單元測試)。
6. 設計抽象時的常見錯誤
錯誤 №1:抽象過多。 有時會想逢物就做抽象類別。但如果只有一種實體型別,別為了流行而抽象——只會讓程式碼更複雜。
錯誤 №2:抽象過於模糊。 如果你的抽象類別涵蓋過多且沒有清楚的職責範圍,子類就會被迫實作不需要的方法,或背負「死」欄位。
錯誤 №3:違反單一職責原則。 抽象類別應只對一個行為領域負責。不要在同一個抽象類別中同時放入資料儲存與商業邏輯的方法。
錯誤 №4:層與層之間耦合過緊。 如果商業邏輯層直接依賴某個具體的儲存實作(例如在內部使用 new InMemoryTaskRepository()),那麼一旦要更換儲存,就得重寫整段程式碼。使用抽象(介面、抽象類別)來降低耦合。
錯誤 №5:文件不足。 抽象是一種契約,必須清楚描述。如果不寫清楚繼承者應該做什麼,很容易出現意料之外的錯誤,或是同事過度「發揮創意」。
GO TO FULL VERSION