CodeGym /課程 /JAVA 25 SELF /使用抽象簡化複雜系統

使用抽象簡化複雜系統

JAVA 25 SELF
等級 19 , 課堂 4
開放

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 具有 savefindByTitlefindAll 這些方法——實作細節就不重要。

易於測試與替換元件

  • 可以輕鬆替換一個實作為另一個(例如測試時用 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:文件不足。 抽象是一種契約,必須清楚描述。如果不寫清楚繼承者應該做什麼,很容易出現意料之外的錯誤,或是同事過度「發揮創意」。

1
問卷/小測驗
抽象類別,等級 19,課堂 4
未開放
抽象類別
抽象化與抽象類別
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION