iT邦幫忙

2026 iThome 鐵人賽

DAY 15
0

目的:讀者會應用模組化程式設計與關注點分離原則,在專案中實踐 MVC 架構、編寫不同功能的模組,藉此達成易修改、可重用的設計

前言

我們在 day 1 提到項我們這樣的自學者,很容易寫出義大利麵程式碼 (Spaghetti code)。例如UI 建構、數據讀取、良率計算、Matplotlib 繪圖全部都在一個長長的 py 檔內。這種程式碼就如同所有配料混雜在一鍋的義大利麵(或是颱風麵)一樣,會有以下問題

  • 因為所有功能都混雜在同一檔案中,該檔案會很長

  • 需要修改特定功能時,要在很長的 py 檔中搜尋目標

  • 同功能程式碼難以重複利用

我們在 day 5 時提到了 function 設定、day8 提到了 ETL 架構,而 day 14 提到了 class 的編寫方式,寫成 class 便是是一種模組化概念。而今天 day 15 會整合上面 day 5 ~ day 14 的內容、介紹關注點分離與 MVC 架構的概念。我們會將不同目的的程式碼模組化並分門別列,為日後的大型專案做準備。

什麼是關注點分離 (Separation of Concerns, SoC)

關注點分離的核心如下

  • 一個模組只處理單一、明確的目的,例如管理讀檔路徑的模組、資料清洗、計算的模組等

  • 不同物件(對應不同模組)之間的資訊傳遞與修改會透過明確的變數 - 引數傳遞,例如不該在物件 b 中直接修改物件 a 的 property

  • 模組之間的關聯性小,也稱為低耦合性 (low coupling)

若以我們熟知的 fab 站點舉例,蝕刻站只會注意蝕刻線寬的穩定性、溶液的濃度,而不是因應黃光的偏差去修改蝕刻時間 (只處理單一目的)。黃光站也不能因為近期線寬變動,私下要求蝕刻站修改作業時間 (明確的變數 - 引數傳遞)。另外黃光站與蝕刻站都是獨立站點,黃光站後續不一定是蝕刻站 (低耦合性)。

軟體設計的概念也相同,UI 顯示歸 UI 顯示、計算歸計算、流程調度歸流程調度。計算完成應透過調度層通知 UI 模組做修改,而不是計算完成後直接在計算模組內修改 UI 的顯示內容。

關注點分離可帶來以下好處

  • 容易新增與修改模組之間的關聯,例如我若需要在清洗資料模組與計算模組之間、再加入一個轉換模組,我只需要確保轉換模組可以讀取清洗模組的輸出、以及輸出計算模組可接受的格式

  • 功能獨立,容易測試,例如我不需要透過 UI ,即可直接測試計算模組

什麼是 MVC (UI-Main-Cal) 架構

當我們在專案中遵守關注點分離的原則時,會自然將不同目地的程式碼封裝成不同 class、模組,例如 UI 模組、計算模組等。而 MVC 架構便是強調 UI 模組 - 調度模組(controller) - 計算模組(cal)相互分離,UI 與計算之間的交互行為由調度層處理。MVC 架構的示意圖如下。

https://ithelp.ithome.com.tw/upload/images/20260928/201823194xIPv8mqsQ.png

三個層的內容差異如下

  1. View (UI 層)— 介面與顯示:

    • 由 Qt Designer 產生的 py 檔案

    • 職責:提供按鈕、表格、輸入框等 UI 元件

    • 守則:只處理外觀佈局,不會涉及連接方式、計算邏輯

  2. Cal (計算層) — 核心商業邏輯:

    • 包含數據清洗、Binning 計算、良率統計等演算法(如 Day 14 的資料清洗模組 RawDataHolder)。

    • 職責:專注於數據處理與產出結果。

    • 守則:純資料處理邏輯,若需要 UI 介面上之值會由 controller 連接與傳入,不會直接讀取 UI 上之值

  3. Controller (調度層) — 溝通樞紐與狀態管理:

    • 連接 View 與 Cal 的指揮官。

    • 職責:從 View 取得使用者輸入、傳給 Cal 計算,並將計算結果傳入給 View 顯示。

應用例: 一個資料分析專案的模組拆分方式

以下是我實際執行的某專案架構,可以在以下的樹狀圖中看到不少資料夾與 py 檔。這些 py 檔的實際用途都可以歸類到 MVC 架構的某層中,例如為了避免 controller.py 過長,我多設置了 ui_state_manager.py 來專門控制 UI 元件行為, 是 view 層的輔助類別,但 ui_state_manager.py 的狀態還是由 controller.py 管理。

project/
│
├── main.py                      # 程式進入點:啟動 QApplication 與主視窗
│
├── ui/
│   ├── ui.py                    # View 層:Qt Designer 產出的介面代碼
│   └── ui_state_manager.py      # UI 狀態管理:控制元件的啟用/停用、模式切換
│
├── controller/
│   └── controller.py            # Controller 層:事件綁定、調度 Cal 與 UI 更新
│
├── config/
│   └── JsonManager.py        # 負責使用者設定的讀取與寫入   
│
├── cal/
│   ├── data_holder.py           # 資料讀取與清洗
│   └── report_calculator.py     # Cal 層:良率與報表計算等(不可直接抓取 UI 值)
│
└── utils/
    └── file_dialog.py           # 管理讀檔路徑

示範例:簡單 MVC 架構的演示

上面的實際應用例對於 MVC 初學者有些複雜,因此以下提供一個使用 tkinter (原生 UI 模組)撰寫的計數器範例,大家可以直接複製到電腦中執行並體驗 MVC 架構的精妙之處。因為這個範例很簡單,因此只要拆分成 cal、view、controller 與 main 的4個獨立檔案。

project/
│
├── main.py                 
│
├── ui/
│   └── view.py
│
├── controller/
│   └── controller.py     
│
└── cal/
   └── cal.py

以下說明這些模組的職責切分:

  1. cal 層 (cal.py)

只處理加加減減的計算部分,可以看到此模組中不會有任何的連接、或是讀取 UI 元件的行為。

class ClickCounter:
    """Cal 層:純粹的資料與計算邏輯,完全不知道介面的存在"""

    def __init__(self):
        self.count = 0

    def increment(self) -> int:
        """計數加 1 並回傳最新數值"""
        self.count += 1
        return self.count
  1. view 層 (view.py)

只處理 UI 元件與排版,一樣不涉及功能連接與計算。

import tkinter as tk


class CounterView:
    """View 層:只負責建立與排版畫面元件,不寫任何計算邏輯"""

    def __init__(self, root: tk.Tk):
        self.root = root
        self.root.title("MVC 計數器最小範例")
        self.root.geometry("300x160")

        # 建立畫面元件
        self.label_count = tk.Label(
            root, text="按鈕尚未被點擊", font=("Microsoft JhengHei", 14)
        )
        self.label_count.pack(pady=20)

        self.btn_add = tk.Button(
            root, text="點我計數", font=("Microsoft JhengHei", 12), width=15
        )
        self.btn_add.pack(pady=10)
  1. controller 層 (controller.py)

我們會在 controller 中將 view 與 cal 串接,實作方式為在 controller 中實例化 view 與 cal,這樣才能對他們各自的屬性做設定。在本案例中的串接方式是將 view 的按鈕點擊事件,連接到 controller 的 handle_click,並在 handle_click 呼叫 cal 的計數方法 (increment)。

import tkinter as tk
from cal import ClickCounter
from view import CounterView


class CounterController:
    """Controller 層:指揮官,負責把 View 的操作交給 Cal 運算,再更新回 View"""

    def __init__(self, root: tk.Tk):
        # 1. 同時持有 View 與 Cal 的實例
        self.view = CounterView(root)
        self.cal = ClickCounter()

        # 2. 進行事件綁定 (使用者點擊按鈕時,呼叫 Controller 的方法)
        self.view.btn_add.config(command=self.handle_click)

    def handle_click(self):
        """調度流程:呼叫 Cal 計算 -> 拿到結果 -> 更新 View"""
        # 1. 呼叫 Cal 計算最新次數
        current_count = self.cal.increment()

        # 2. 指示 View 更新顯示文字
        self.view.label_count.config(text=f"按鈕已被點擊了 {current_count} 次!")

handle_click 因為將多個需要按鍵驅動的功能結合在一 function 中,被稱為 wrapper

大家可以想想看如果按鈕想要連接多個 function,又或是連接多個 function 後想斷開連結該怎麼處理

  1. 程式進入點 (main.py)

只負責啟動應用程式視窗,程式怎麼運作是交給 controller 管理。

import tkinter as tk
from controller import CounterController

if __name__ == "__main__":
    root = tk.Tk()
    app = CounterController(root)
    root.mainloop()

To be continued →

今天我們透過 tkinter 的 command 串接了 View 與 Cal,但 Qt 其實有更加靈活的串接方式。

下一篇(Day 16),我們將深入探討 Qt 最強大的通訊機制:Signal & Slot,讓我們可以做出連接多 function、跨執行緒連接等功能


上一篇
Day 14:一為全,全為一 - 使用 Python class 整合function、引數與結果
下一篇
Day 16: 千里姻緣一線牽- 用 signal 與 slot 為 UI 接上功能
系列文
用 Python 打造最順手的良率統計工具:半導體工程師的模組化開發之道 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言