🚀 Day 5:打造「MVC 模組化重構與 CLI 版發布 (Django Ready MVT 架構)」
5 天全日營隊 (Day 5 / 總時數 6 Hours) | 登頂軟體工程:模組解耦、網頁體驗與 Demo Day 發表
📖 為什麼學 these?——「從雜亂腳本到 MVT 頂級架構」關卡冒險故事線
前四天我們像是在廚房邊做菜邊把食材跟炒鍋亂塞。今天我們要化身為 **軟體架構師**,用 def 把程式拆解為掌管資料的 Model、負責畫面的 View 與指揮大腦 Controller,並在發表會上展示你的專案:
| 單元名稱 | 記帳程式關卡角色 | 為什麼這個單元不可或缺?(技術關聯) |
|---|---|---|
| 單元 1:函式 (def) 與模組化 | 🍹 自動販賣機與模組封裝 | 將重複寫兩次以上的邏輯用 def 封裝(DRY 原則),投入參數傳回 return 結果,程式不再雜亂。 |
| 單元 2:MVT/MVC 職責分離重構 | 🏗️ 建築師重構藍圖 (MVT) | 實作「職責分離」:Model 專管 JSON 讀寫、View 專管介面印出、Controller 負責指揮,無縫銜接 Django 核心! |
| 單元 3:CLI UI 視覺美化排版 | 🎨 極客介面設計師 (TUI UI/UX) | 運用 f-string 欄位對齊({:<10})與 Emoji / ASCII 藝術標題,打造工程師級的質感終端機介面。 |
| 單元 4:未來銜接:Django 網頁預告 | 🌐 跨界門戶前哨站 (Django Web) | 把相同的 expenses.json 直接匯入 Django 網頁中,發現 CLI 列表與 HTML {% for %} 語法完全同構! |
| 單元 5:專案打包與簡報準備 | 📦 產品出版說明書 (README.md) | 撰寫 Markdown 格式的 README.md,向世界展示你的功能亮點、安裝步驟與 GitHub 專案規範。 |
| 單元 6:Demo Day 成果發表 | 🏆 理財咖啡館發布會 (Demo Day) | 每人 2 分鐘上台展示專案亮點與 MVT 重構結構,接受導師反饋,體驗敏捷開發與社群分享文化! |
📌 課程前導與簡介說明
- 營隊定位:最後一天從「初學者腳本」邁向「專業軟體工程」。重構為 **MVT 職責分離架構**。
- 高中生心理特性:體驗程式碼變乾淨的清爽感,透過 Demo Day 發表建立自信。
- Django 銜接點:明確標示
model_(對應 models.py)、view_(對應 HTML) 與handle_(對應 views.py/urls.py)。
歡迎來到 Day 5 登頂時刻!今天你要把前四天的成果重構為「工程師級模組架構」!請閱讀每個單元的 **故事單元說明**,點擊 **延伸閱讀** 展開深層觀念與 Google 搜尋,並善用每區塊的 **AI 協作除錯提示詞**!
💻 Day 5 專案總成果:Django-Ready MVT 模組化記帳本
【教師參考解答】學生於 Day 5 結束時將能獨立撰寫的完整模組化程式:
"""
====================================================================
高中生極簡終端機記帳本 (CLI 版 - Django 銜接預備架構)
系統架構:採 MVT (Model - View - Template/Controller) 模組化設計
====================================================================
"""
import json
import os
from datetime import datetime
DATA_FILE = "expenses.json"
# ==========================================
# 📦 [MODEL 區塊] 資料庫與持久化操作 (對應 Django models.py / Database)
# ==========================================
def model_load_data():
""" 讀取 JSON 資料檔 (模擬 Django ORM 物件查詢) """
if not os.path.exists(DATA_FILE):
return []
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return []
def model_save_data(records):
""" 儲存資料至 JSON 檔案 (模擬 Django .save() 寫入資料庫) """
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=4)
def model_add_record(records, item, price, category):
""" 新增一筆紀錄 (模擬 Model 創立紀錄) """
record = {
"id": len(records) + 1,
"item": item,
"price": price,
"category": category,
"date": datetime.now().strftime("%Y-%m-%d %H:%M")
}
records.append(record)
model_save_data(records)
return record
def model_delete_record(records, index):
""" 刪除指定索引的記錄 """
if 0 <= index < len(records):
deleted = records.pop(index)
for i, r in enumerate(records):
r["id"] = i + 1
model_save_data(records)
return deleted
return None
# ==========================================
# 🎨 [VIEW 區塊] 介面渲染與印出 (對應 Django Templates / HTML)
# ==========================================
def view_header():
print("\n" + "=" * 45)
print(" 🚀 酷炫 Python 終端機記帳本 🚀")
print("=" * 45)
def view_menu():
view_header()
print(" [1] ➕ 新增消費紀錄")
print(" [2] 📊 查看所有明細與統計")
print(" [3] 🔍 依分類篩選紀錄")
print(" [4] ❌ 刪除特定消費紀錄")
print(" [5] 💾 儲存並退出系統")
print("=" * 45)
def view_render_list(records):
""" 渲染帳務明細 (模擬 Django HTML {% for %} 走訪) """
if not records:
print("\n📭 目前尚無任何記帳資料!")
return
print("\n=== 💰 您的記帳明細列表 ===")
print(f"{'編號':<5}{'品項':<12}{'分類':<10}{'金額':<8}{'時間'}")
print("-" * 50)
total = 0
for r in records:
print(f"{r['id']:<5}{r['item']:<12}{r['category']:<10}${r['price']:<7}{r['date']}")
total += r["price"]
print("-" * 50)
print(f"💵 總計累計消費:${total} 元\n")
# ==========================================
# ⚙️ [CONTROLLER / ROUTER 區塊] 控制邏輯 (對應 Django views.py / urls.py)
# ==========================================
def handle_add(records):
""" 處理新增消費邏輯 """
item = input("👉 請輸入消費名稱 (例如: 珍珠奶茶): ").strip()
if not item:
print("⚠️ 名稱不能為空!")
return
price_input = input("👉 請輸入金額: ").strip()
if not price_input.isdigit():
print("❌ 錯誤:金額必須是純數字!")
return
category = input("👉 請輸入分類 (例如: 餐飲/娛樂/學習): ").strip() or "一般"
record = model_add_record(records, item, int(price_input), category)
print(f"✅ 成功寫入:{record['item']} ${record['price']} 元 [{record['category']}]")
def handle_filter(records):
""" 處理按分類搜尋邏輯 """
cat = input("🔍 請輸入要搜尋的分類名稱: ").strip()
filtered = [r for r in records if r["category"] == cat]
print(f"\n🔎 搜尋分類結果: '{cat}'")
view_render_list(filtered)
def handle_delete(records):
""" 處理刪除邏輯 """
view_render_list(records)
if not records:
return
idx_input = input("❌ 請輸入要刪除的編號 (ID): ").strip()
if idx_input.isdigit():
idx = int(idx_input) - 1
deleted = model_delete_record(records, idx)
if deleted:
print(f"🗑️ 已成功刪除:{deleted['item']} ${deleted['price']} 元")
else:
print("⚠️ 找不到該編號!")
else:
print("❌ 輸入無效,請輸入數字 ID。")
# ==========================================
# 🏁 [MAIN 區塊] 程式入口 (主生命週期)
# ==========================================
def main():
records = model_load_data()
while True:
view_menu()
choice = input("👉 請選擇功能選項 (1-5): ").strip()
if choice == "1":
handle_add(records)
elif choice == "2":
view_render_list(records)
elif choice == "3":
handle_filter(records)
elif choice == "4":
handle_delete(records)
elif choice == "5":
print("\n💾 系統資料已保存,期待下次運算!再見!👋")
break
else:
print("⚠️ 無效的選項,請輸入數字 1 至 5!")
if __name__ == "__main__":
main()
【學生自主實作框架】請根據今日各單元所學,將空缺的 `____` 填入正確程式碼:
"""
🎯 Day 5 自主挑戰:請將下方的 ____ 填入正確的 def、return 與 MVT 模組語法!
"""
import json
# 1. [MODEL 區塊]: 專管資料讀寫 (絕對不寫 print/input)
____ model_save_data(records): # 提示:宣告函式的關鍵字
with open("expenses.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=4)
def model_add_record(records, item, price):
record = {"item": item, "price": price}
records.append(record)
model_save_data(records)
____ record # 提示:將新建物件傳回給 Controller 的關鍵字
# 2. [VIEW 區塊]: 專管介面渲染
def view_render_item(item, price):
print(f"✨ [UI 繪製] 商品: {item:<10} | 金額: ${price}")
# 3. [CONTROLLER 區塊]: 邏輯溝通橋樑
def handle_add(records):
item = input("品項: ").strip()
price = int(input("金額: "))
# 呼叫 Model 建立資料,再呼叫 View 渲染畫面
new_rec = model_add_record(records, item, price)
view_render_item(new_rec["item"], new_rec["price"])
⏰ 單元 1:函式 (def) 與模組化(09:00 - 10:00)
函式(def)就像是「自動販賣機」!你把投幣與按下按鈕的動作定義好(傳入參數),投入硬幣後,自動販賣機處理內部邏輯,最後吐出你點的飲料(return 回傳結果)。遵守工程師的 **DRY 原則 (Don't Repeat Yourself)**:只要一段程式碼重複寫了兩次以上,就應該把它打包成獨立的販賣機(函式)!
函式是現代模組化程式設計(Modular Programming)的基礎:
return與print()的根本差異:print()只是把字串印在螢幕上(無法被後續程式利用);return則是把計算結果傳回給呼叫者。- 純函式 (Pure Functions):相同的輸入永遠傳回相同的輸出,且無副作用 (Side Effect),極易維護與測試。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請向初學者詳細說明 Python def 函式中 return 的作用,並用簡單的例子解釋為什麼 print() 不能替代 return,以及什麼是 DRY (Don't Repeat Yourself) 原則。
- 宣告
def、傳遞參數與return值。 - 強調 DRY 原則 (Don't Repeat Yourself)。
- Django 銜接點:Django 中所有的 View 視圖邏輯本質上都是一個獨立的 Python 函式
def index(request):。
# 1-1: 宣告簡單函式與 return 值
def calculate_tax(price, rate=0.05):
""" 計算含稅價格 """
tax = price * rate
return price + tax
# 呼叫函式
total_price = calculate_tax(100)
print(f"含稅總價:${total_price:.0f} 元")
- 漏寫 return 導致回傳值為 None:函式內部有計算但漏寫
return,變數接到NoneType拋出TypeError。
我寫了一個 def 函式計算金額,但在外部呼叫並列印變數時卻印出了 None。請幫我檢查函式末端是否漏寫了 return 關鍵字。
⏰ 單元 2:MVT/MVC 職責分離重構(10:00 - 11:00)
樂高積木為什麼能拼出城堡?因為每一個積木都有明確、獨立的功能!今天我們要採用軟體工程最頂級的 **職責分離 (Separation of Concerns)** 進行重構:
• **Model**:專門掌管 JSON 檔案讀寫(絕對不寫 print 或 input)
• **View**:專門負責介面排版與渲染
• **Controller/Handler**:負責接收使用者輸入,並指揮 Model 與 View!
將資料邏輯、顯示介面與控制流程解耦(Decoupling)是現代大專案維護的核心:
- Model (models.py):對應資料庫結構與 ORM 查詢。
- Template / View (templates/*.html):對應使用者看到的 HTML 網頁介面。
- View / Controller (views.py / urls.py):接收 HTTP Request 並調用 Model 與 Template。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請向初學者解釋什麼是『職責分離 (Separation of Concerns)』,並對比 Django 的 MVT (Model, View, Template) 與傳統 MVC 架構的元件對映關係。
- 重構為 Model、View、Controller 三分法。
- 規範:Model 裡面絕對不寫
print()或input(),確保未來能直接移籍至 Web。 - Django 銜接點:Django MVT 核心架構全貌(models.py, templates, views.py)。
# [Model 區塊]: 純資料操作
def model_get_total(records):
return sum(r["price"] for r in records)
# [View 區塊]: 純 UI 排版
def view_show_total(total_amount):
print(f"✨ [畫面渲染] 總金額為:${total_amount} 元 ✨")
# [Controller 區塊]: 邏輯溝通橋樑
def handle_show_total_flow(records):
total = model_get_total(records) # 找 Model 要資料
view_show_total(total) # 交給 View 畫出畫面
- 職責模糊 (Inappropriate Intimacy):在 Model 函式寫了 `input()`,導致資料層黏死在 CLI 上無法被網頁重複利用。
請幫我檢查這段 MVT 重構程式碼,確保所有 input() 與 print() 都被抽離出 Model 函式,並只放在 View 與 Controller 中。
⏰ 單元 3:CLI UI 視覺美化排版(11:00 - 12:00)
工程師的軟體也需要高質感介面!透過 Emoji(`🚀`, `📊`, `❌`)、ASCII Title Art 與 f-string 欄位對齊 formatting 語法(如 {item:<12} 靠左對齊佔 12 格、{price:>8} 靠右對齊佔 8 格),印出精美如 Word 表格一般的 CLI 終端機介面!
Terminal 排版依賴精確的控制字元縮排:
- 欄位對齊語法
{:<10}/{:>8}:靠左/靠右對齊並指定固定字元寬度。 - 全半形寬度偏移:中文字元(全形佔 2 個字寬)與英數(半形佔 1 個字寬)混用時,可能造成 Console 欄位邊框稍微偏移。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請幫我設計一個漂亮的 Python CLI 終端機選單與明細表格,使用 {:<10} 欄位對齊語法與 Emoji 圖示,讓表格在終端機中看起來整齊劃一。
- 運用 Emoji、ASCII 標題與 f-string 縮排對齊。
- 進階語法:
{name:<12}(靠左佔 12 格)、{price:>8}(靠右佔 8 格)。 - Django 銜接點:前端 HTML/CSS 樣式設計與使用者介面體驗 (UI/UX)。
# 表格化格式化輸出對齊
print(f"{'編號':<5}{'品項':<10}{'金額':<8}")
print("-" * 25)
print(f"{'1':<5}{'珍珠奶茶':<10}${60:<8}")
print(f"{'2':<5}{'雞排':<10}${85:<8}")
- 全半形混用邊框偏移:中文全形佔 2 字寬,導致 ASCII 框線
|線對不齊。
我在 CLI 印出中文字與數字時,表格右側邊框 | 稍微對不齊。請幫我調整 f-string 的欄位寬度讓介面看起來整齊。
⏰ 單元 4:未來銜接:Django 網頁預告(13:00 - 14:00)
神奇的時刻到了!導師將展示把剛才 CLI 新增的 expenses.json 資料,直接無縫載入至 Django Web 網頁中展示!你會發現:CLI 版 View 的 for 迴圈走訪,與 Django HTML 中的 {% for r in records %} 語法完全一模一樣!
Web 開發與 CLI 的核心差異在於資料渲染的載體:
- CLI View:使用
print()將字串格式化發送到 Terminal 視窗。 - Django Web View:將 Context Data 裝填進 HTML Template,經由 HTTP Response 傳送給瀏覽器繪製
<table>。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請對比 Python CLI 的 view_render_list() 列表印出與 Django HTML 模板中的 {% for r in records %} 表格語法,說明它們在資料渲染上的異同。
- Live Demo:將
expenses.json直接連載至極簡 Django 網頁中展示。 - 語法對照:CLI 版
view_render_list()的for迴圈,與 Django HTML 中的{% for r in records %}完全同構。 - Django 銜接點:引爆全棧 Web 開發的強烈後續學習動機。
<!-- 未來 Django HTML 模板中的對應渲染語法 (templates/expense_list.html) -->
<table border="1">
<tr><th>品項</th><th>金額</th><th>分類</th></tr>
{% for r in records %}
<tr>
<td>{{ r.item }}</td>
<td>${{ r.price }}</td>
<td>{{ r.category }}</td>
</tr>
{% endfor %}
</table>
- 觀念混淆:以為網頁會自動執行 `print()` $\rightarrow$ 釐清網頁是 View 將 Context 塞入 HTML 後傳給瀏覽器。
我想知道如何把剛才寫好的 Python CLI 記帳專案轉成 Django 網頁,請給我一張簡單的圖解說明 CLI 與 Django MVT 元件對應關係。
⏰ 單元 5:專案打包與簡報準備(14:00 - 15:00)
優秀的軟體產品不能沒有「說明書」!我們將學習 Markdown 語法撰寫 README.md 文件,寫出你的專案簡介、功能亮點、安裝執行指令與 MVT 模組架構說明,呈現專業工程師打包專案的態度!
良好的 Documentation 是開源社群(Open Source)的核心精神:
- Markdown 語法:輕量級標記語言,使用 `#` 標題、```` 程式碼區塊、`-` 項目符號。
- 專案 README 必備要素:專案簡介、功能亮點 (Features)、環境需求 (Prerequisites) 與執行步驟 (Usage)。
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請幫我這份 Python CLI 記帳程式專案寫一份吸引人的 GitHub README.md,包含專案簡介、功能亮點、安裝執行步驟與採用的 MVT 模組化架構說明。
- 編寫 Markdown 格式
README.md說明文件。 - 輔導準備 2 分鐘 Demo Day 發表演練。
- Django 銜接點:GitHub 專案管理與開源分享文化。
# 🚀 Python 終端機個人財務記帳本 (MVT Architecture)
一個基於 Python 撰寫的極簡 CLI 記帳工具,具備 JSON 資料持久化與完整 CRUD 功能。
## 🌟 功能特色
- 📦 **MVT 架構解耦**:資料層與 UI 介面高度模組化
- 💾 **自動持久化**:開機自動讀取 `expenses.json`
- 🛡️ **安全防呆**:內建 `try-except` 防止輸入崩潰
## 🔧 執行方式
```bash
python main.py
```
- 根目錄路徑不一致:Terminal 執行
python main.py跳出No such file or directory$\rightarrow$ 檢查 VS Code 開啟的目錄。
我在 VS Code 終端機執行 python main.py 時跳出 No such file or directory。請教我如何用 cd 指令切換至正確的檔案目錄。
⏰ 單元 6:Demo Day 專案成果發表(15:00 - 16:00)
恭喜大家!在短短 30 個小時內,你們從連 print() 都不懂,一路解鎖了變數、資料結構、JSON 持久化,甚至獨立寫出了 MVT 模組化記帳軟體!在最後的 Demo Day 發表會上,請用 2 分鐘自信展示你的產品亮點與彩蛋功能吧!
將技術成果說給別人聽是極客工程師重要的軟實力 (Soft Skills):
- 敏捷 Sprint Demo:展示真正的運作軟體 (Working Software) 接受 User 反饋。
- 優雅面對 Live Bug:即使現場展示遇到 Bug,冷靜閱讀 Terminal Traceback,正是展現除錯實力的最佳時刻!
🔗 Google 搜尋關鍵字 (依相關度排序,紫羅蘭標記為進階內容,點擊開啟新分頁):
🤖 AI 自主學習探索提示詞 (Prompt):
請扮演一位熱情的資深軟體架構師,針對一位完成 Python CLI MVT 模組化記帳程式的高中生寫一段 100 字的正向結業評語,肯定他的模組重構與除錯努力,並鼓勵他未來繼續學習 Django。
- 每人 2 分鐘展示專案亮點與 MVT 分離結構。
- 營造正向、溫馨且具備 Geek 發布會氛圍的結業儀式。
- Django 銜接點:敏捷開發 (Agile) 中的 Sprint Demo 與工程團隊分享文化。
- Live Demo 遇到突發 Bug:告訴學生「連蘋果發布會都會遇到 Bug!冷靜看 Traceback,展現工程師除錯實力!」
我在 Demo 簡報時現場執行程式跳出了意外的 Bug。請告訴我如何冷靜閱讀 Terminal 訊息,並用專業的姿態向大家說明除錯步驟。