Code Smell 檢查清單
基本資訊
目的: 提供基於 Ticket 粒度的 Code Smell 檢測標準和檢查清單
與其他方法論的關係:
- 引用層級隔離派工方法論 的五層架構定義
- 配合 TDD 四階段流程使用
- 整合到 Hook 系統自動化檢測
核心理念: Code Smell 檢查清單是基於層級隔離原則的程式品質檢測工具,從 Ticket 設計階段就能發現潛在的架構問題,實現「預防勝於治療」的品質管理策略。
第一章:Code Smell 概述和分類
1.1 什麼是 Code Smell
定義: Code Smell(程式異味)是指程式碼中表面上看似正常,但實際上暗示設計問題或潛在缺陷的特徵。
核心特性:
- 不是 Bug: Code Smell 不會導致程式崩潰或功能錯誤
- 設計問題: 暗示程式架構或設計上的缺陷
- 可檢測: 透過明確的指標可以識別
- 可修正: 透過重構可以消除
與 Bug 的區別:
1Bug(程式錯誤):
2- 導致功能失敗或程式崩潰
3- 需要立即修正
4- 透過測試失敗發現
5
6Code Smell(程式異味):
7- 程式功能正常運作
8- 暗示設計問題,未來可能導致維護困難
9- 透過程式碼檢視或靜態分析發現
10- 透過重構改善為什麼 Code Smell 重要:
- 降低維護成本: 及早發現設計問題,避免技術債務累積
- 提升程式碼品質: 改善可讀性、可測試性、可擴展性
- 預防未來問題: 在問題惡化前進行修正
- 團隊協作: 提供統一的品質標準和溝通語言
1.2 為什麼需要 Code Smell 檢查清單
傳統問題:
- 依賴個人經驗判斷 Code Smell(主觀且不一致)
- 問題發現太晚(實作完成後才發現設計缺陷)
- 缺少量化標準(難以判斷是否需要重構)
- 修正成本高(架構問題需要大規模修改)
檢查清單優勢:
- 標準化: 提供統一的檢測標準,避免主觀判斷
- 及早發現: 從 Ticket 設計階段就能發現問題
- 量化指標: 明確的數字標準(如行數、層級跨度)
- 降低成本: 設計階段修正成本遠低於實作後修正
Ticket 粒度檢測的價值:
1設計階段檢測 vs 實作階段檢測:
2
3設計階段(Ticket 粒度):
4- 修正成本: 低(只需要調整設計)
5- 影響範圍: 小(尚未實作程式碼)
6- 風險: 低(無需修改既有程式碼)
7
8實作階段(Code Review):
9- 修正成本: 中(需要重寫部分程式碼)
10- 影響範圍: 中(可能影響多個檔案)
11- 風險: 中(需要回歸測試)
12
13維護階段(上線後):
14- 修正成本: 高(需要大規模重構)
15- 影響範圍: 大(可能影響多個模組)
16- 風險: 高(可能引入新 Bug)1.3 Code Smell 分類體系
基於層級隔離派工方法論 的五層架構,本檢查清單將 Code Smell 分為三大類:
分類 A:跨層級 Code Smells(違反層級隔離原則)
這類 Code Smell 涉及多個架構層級,違反層級隔離和單層修改原則:
- A1. Shotgun Surgery(散彈槍手術)- 單一變更需要修改多個層級
- A2. Feature Envy(功能嫉妒)- 外層過度依賴內層實作細節
- A3. Inappropriate Intimacy(不當親密關係)- 層級間過度耦合
- A4. Leaky Abstraction(抽象滲漏)- 內層實作細節洩漏到外層
分類 B:單層級 Code Smells(違反單一職責原則)
這類 Code Smell 發生在單一層級內,違反單一職責原則(SRP):
- B1. Divergent Change(發散式變更)- 單一類別承擔多個職責
- B2. Large Class(大類別)- 類別過大,職責不清
- B3. Long Method(長方法)- 方法過長,難以理解
- B4. Dead Code(死程式碼)- 永遠不會執行的程式碼
分類 C:Ticket 粒度相關 Code Smells
這類 Code Smell 與 Ticket 設計和粒度相關:
- C1. God Ticket(全能 Ticket)- Ticket 範圍過大,修改過多檔案
- C2. Incomplete Ticket(不完整 Ticket)- Ticket 缺少必要測試或文件
- C3. Ambiguous Responsibility(職責模糊 Ticket)- Ticket 職責定義不明確
分類樹狀結構
1Code Smell 分類體系(基於[層級隔離派工方法論](/record/layered-ticket-methodology/) 第 2.2 節五層架構定義)
2
3A. 跨層級 Code Smells(違反層級隔離)
4 ├─ A1. Shotgun Surgery(散彈槍手術)
5 ├─ A2. Feature Envy(功能嫉妒)
6 ├─ A3. Inappropriate Intimacy(不當親密關係)
7 └─ A4. Leaky Abstraction(抽象滲漏)
8
9B. 單層級 Code Smells(違反單一職責)
10 ├─ B1. Divergent Change(發散式變更)
11 ├─ B2. Large Class(大類別)
12 ├─ B3. Long Method(長方法)
13 └─ B4. Dead Code(死程式碼)
14
15C. Ticket 粒度相關 Code Smells
16 ├─ C1. God Ticket(全能 Ticket)
17 ├─ C2. Incomplete Ticket(不完整 Ticket)
18 └─ C3. Ambiguous Responsibility(職責模糊 Ticket)1.4 與層級隔離派工方法論 的關係
互補關係:
層級隔離派工方法論: 定義「應該怎麼做」(正面原則)
- 五層架構定義(Layer 1-5)
- 單層修改原則
- Ticket 粒度標準
本檢查清單: 定義「不應該怎麼做」(負面模式識別)
- Code Smell 檢測方法
- 違規模式識別
- 重構策略
引用關係: 本檢查清單引用層級隔離派工方法論 的以下章節:
- 2.2 節: 五層架構完整定義
- 2.3 節: 依賴方向規則
- 3.1 節: 單層修改原則定義
- 5.2 節: Ticket 粒度量化指標
- 6.2 節: 檔案路徑分析法
- 6.5 節: 違規模式識別
無重複定義: 本文件不重複定義五層架構,所有層級定義都引用層級隔離派工方法論 第 2.2 節。
第二章:基於層級隔離的 Code Smell 定義
2.1 A 類 Code Smell(跨層級問題)
2.1.1 A1. Shotgun Surgery(散彈槍手術)
定義: 單一邏輯變更需要同時修改多個架構層級的程式碼,違反「單層修改原則」(層級隔離派工方法論 第 3.1 節)。
特徵識別:
- 一個小需求需要修改 UI、Behavior、UseCase、Domain 多層
- 層級間缺乏適當的抽象介面
- 變更影響範圍不可控
- 檔案修改數量 > 5 個且跨 2 個以上層級
與層級隔離派工方法論 的關聯:
- 違反單層修改原則(3.1 節)
- 違反從外而內實作順序(4.1 節)
- 未遵循Ticket 粒度標準(5.2 節)
範例說明:
1需求:書籍新增「出版社」欄位
2
3反例 Shotgun Surgery 模式:
4- Layer 1 (UI): BookDetailWidget 新增 publisher Text
5- Layer 2 (Behavior): BookDetailController 新增 publisher 屬性
6- Layer 3 (UseCase): GetBookDetailUseCase 新增 publisher 參數
7- Layer 5 (Domain): Book Entity 新增 publisher 欄位
8
9問題分析:
10- 修改 4 個層級(Layer 1, 2, 3, 5)
11- 修改至少 4 個檔案
12- 違反單層修改原則
13- 風險:任一層級修改錯誤都會影響整個功能
14
15正例 正確做法(引入 Facade):
16- Phase 1 [Layer 5]: Book Entity 新增 publisher 欄位
17- Phase 2 [Layer 3]: BookDetailFacade 更新回傳資料
18- Phase 3 [Layer 2]: Presenter 轉換新增 publisher
19- Phase 4 [Layer 1]: UI 顯示 publisher
20
21改善效果:
22- 每個 Phase 只修改單一層級
23- 變更影響範圍可控
24- 風險降低好壞對比程式碼:
1// 反例:Shotgun Surgery:新增欄位需要修改 4 層
2
3// Layer 5 (Domain)
4class Book {
5 final String title;
6 final ISBN isbn;
7 final String publisher; // 新增欄位
8}
9
10// Layer 3 (UseCase)
11class GetBookDetailUseCase {
12 Future<Book> execute(String id) async {
13 final book = await repository.findById(id);
14 // 需要處理 publisher
15 return book;
16 }
17}
18
19// Layer 2 (Behavior)
20class BookDetailController {
21 String? publisher; // 新增屬性
22
23 void loadBookDetail(String id) async {
24 final book = await getBookDetailUseCase.execute(id);
25 publisher = book.publisher; // 新增處理
26 }
27}
28
29// Layer 1 (UI)
30class BookDetailWidget {
31 Widget build(BuildContext context) {
32 return Column(
33 children: [
34 Text(controller.title),
35 Text(controller.publisher ?? ''), // 新增顯示
36 ],
37 );
38 }
39}
40
41// 正例:引入 Facade 隔離變更
42
43// Layer 4 (Domain Interface)
44abstract class IBookDetailFacade {
45 Future<BookDetailViewModel> getBookDetail(String id);
46}
47
48// Layer 3 (UseCase - Facade Implementation)
49class BookDetailFacade implements IBookDetailFacade {
50 Future<BookDetailViewModel> getBookDetail(String id) async {
51 final book = await bookRepository.findById(id);
52 return BookPresenter.toViewModel(book); // 統一轉換
53 }
54}
55
56// Layer 2 (Behavior - Presenter)
57class BookPresenter {
58 static BookDetailViewModel toViewModel(Book book) {
59 return BookDetailViewModel(
60 title: book.title.value,
61 isbn: book.isbn.value,
62 publisher: book.publisher, // 新增欄位在這裡處理
63 );
64 }
65}
66
67// Layer 1 (UI) - 無需修改
68class BookDetailWidget {
69 Widget build(BuildContext context) {
70 return Column(
71 children: [
72 Text(viewModel.title),
73 Text(viewModel.isbn),
74 Text(viewModel.publisher), // 直接使用 ViewModel
75 ],
76 );
77 }
78}
79
80改善效果:
81- 未來新增欄位只需要修改 Layer 3 (Facade) 和 Layer 2 (Presenter)
82- Layer 1 (UI) 和 Layer 5 (Domain) 的修改影響已隔離2.1.2 A2. Feature Envy(功能嫉妒)
定義: 某層級過度依賴其他層級的實作細節,而非依賴抽象介面。外層直接存取內層的內部狀態,缺乏適當的 DTO 或 ViewModel 轉換。
特徵識別:
- 外層直接存取內層的內部狀態(如
book.isbn.value) - 缺乏適當的 DTO 或 ViewModel 轉換
- 跨層級的緊耦合
- UI 層直接 import Domain Entity
- 外層存取內層內部欄位次數 > 3 次
與層級隔離派工方法論 的關聯:
- 違反「依賴倒置原則」(2.3 節)
- 違反 Layer 2 的「資料轉換職責」(2.2 節 Layer 2 定義)
- 缺少 Presenter 轉換層
範例說明:
1// 反例:Feature Envy:UI 直接存取 Domain Entity
2
3import 'package:book_overview_app/domains/library/entities/book.dart';
4// 反例:UI 層不應 import Domain Entity
5
6class BookDetailWidget extends StatelessWidget {
7 final Book book; // 反例:直接依賴 Domain Entity
8
9 Widget build(BuildContext context) {
10 return Column(
11 children: [
12 Text(book.title.value), // 反例:存取內部欄位
13 Text(book.isbn.value), // 反例:存取內部欄位
14 Text(book.author.name), // 反例:存取內部欄位
15 Text(book.isNewRelease() ? '新書' : ''), // 反例:呼叫 Domain 方法
16 ],
17 );
18 }
19}
20
21問題分析:
22- UI 層 import Domain Entity(違反依賴方向)
23- UI 直接存取 Entity 內部欄位(緊耦合)
24- UI 呼叫 Domain 業務方法(職責混亂)
25- Domain 修改會影響 UI(高風險)
26
27// 正例:透過 ViewModel 轉換
28
29// Layer 2: 定義 ViewModel
30import 'package:book_overview_app/presentation/view_models/book_view_model.dart';
31
32class BookViewModel {
33 final String title;
34 final String isbn;
35 final String author;
36 final bool isNew;
37
38 BookViewModel({
39 required this.title,
40 required this.isbn,
41 required this.author,
42 required this.isNew,
43 });
44}
45
46// Layer 2: Presenter 轉換(資料轉換職責)
47class BookPresenter {
48 static BookViewModel toViewModel(Book book) {
49 return BookViewModel(
50 title: book.title.value, // 提取內部欄位
51 isbn: book.isbn.value, // 提取內部欄位
52 author: book.author.name, // 提取內部欄位
53 isNew: book.isNewRelease(), // 執行 Domain 方法
54 );
55 }
56}
57
58// Layer 1: UI 使用 ViewModel
59class BookDetailWidget extends StatelessWidget {
60 final BookViewModel viewModel; // 正例:依賴 ViewModel
61
62 Widget build(BuildContext context) {
63 return Column(
64 children: [
65 Text(viewModel.title), // 正例:使用轉換後的資料
66 Text(viewModel.isbn), // 正例:使用轉換後的資料
67 Text(viewModel.author), // 正例:使用轉換後的資料
68 Text(viewModel.isNew ? '新書' : ''), // 正例:使用轉換後的狀態
69 ],
70 );
71 }
72}
73
74改善效果:
75- UI 層不依賴 Domain Entity(降低耦合)
76- Presenter 集中處理資料轉換(符合 Layer 2 職責)
77- Domain 修改不影響 UI(只需調整 Presenter)
78- 測試更容易(Mock ViewModel 即可)2.1.3 A3. Inappropriate Intimacy(不當親密關係)
定義: 層級間過度耦合,內層知道外層的存在或依賴外層,違反依賴方向規則。
特徵識別:
- Domain 層依賴 UseCase 或 UI 層
- 依賴方向錯誤(內層依賴外層)
- 存在循環依賴
- Domain Entity 包含 UI 或 Infrastructure 的 import
與層級隔離派工方法論 的關聯:
- 違反「依賴方向規則」(2.3 節)
- 違反「Layer 5 不依賴任何層級」原則
- 正確依賴方向:Layer 1 → Layer 2 → Layer 3 → Layer 4 ← Layer 5
範例說明:
1// 反例:Inappropriate Intimacy:Domain 依賴 UseCase
2
3// Layer 5 (Domain)
4import 'package:book_overview_app/application/use_cases/add_book_to_favorite_use_case.dart';
5// 反例:Domain 不應 import UseCase
6
7class Book {
8 final String id;
9 final Title title;
10 final AddBookToFavoriteUseCase favoriteUseCase; // 反例:Domain 依賴 UseCase
11
12 void addToFavorite() {
13 favoriteUseCase.execute(this.id); // 反例:Domain 不應呼叫 UseCase
14 }
15}
16
17問題分析:
18- Domain 依賴外層(UseCase)
19- 違反依賴方向規則
20- Domain 失去獨立性和可重用性
21- 測試困難(Domain 測試需要 Mock UseCase)
22
23// 正例:Domain 只定義業務邏輯
24
25// Layer 5 (Domain) - 獨立且純淨
26class Book {
27 final String id;
28 final Title title;
29 bool isFavorited = false; // 正例:只記錄狀態
30
31 void markAsFavorite() {
32 this.isFavorited = true; // 正例:只處理業務邏輯
33 }
34
35 void unmarkFromFavorite() {
36 this.isFavorited = false;
37 }
38}
39
40// Layer 3 (UseCase) - 協調業務流程
41class AddBookToFavoriteUseCase {
42 final IBookRepository bookRepository;
43 final IFavoriteRepository favoriteRepository;
44
45 Future<void> execute(String bookId) async {
46 // 1. 取得書籍
47 final book = await bookRepository.findById(bookId);
48
49 // 2. 執行 Domain 方法
50 book.markAsFavorite(); // 正例:UseCase 呼叫 Domain 方法
51
52 // 3. 儲存狀態
53 await bookRepository.save(book);
54 await favoriteRepository.add(bookId);
55 }
56}
57
58// Layer 2 (Behavior/Controller) - 觸發 UseCase
59class BookDetailController {
60 final AddBookToFavoriteUseCase addToFavoriteUseCase;
61
62 void onFavoriteButtonPressed(String bookId) async {
63 await addToFavoriteUseCase.execute(bookId); // 正例:呼叫方向
64 }
65}
66
67改善效果:
68- Domain 獨立且純淨(不依賴外層)
69- 依賴方向正確(Layer 2 → Layer 3 → Layer 5)
70- Domain 可重用性高
71- 測試容易(Domain 無外部依賴)2.1.4 A4. Leaky Abstraction(抽象滲漏)
定義: 內層的實作細節透過介面洩漏到外層,介面不夠抽象。
特徵識別:
- Repository 介面包含資料庫特定參數(如 SQL 語句)
- Domain Event 包含 UI 特定資料(如 Widget 狀態)
- 抽象介面不夠抽象,包含實作關鍵字
- 介面方法名稱洩漏實作細節
與層級隔離派工方法論 的關聯:
- 違反 Layer 4「介面契約」的職責定義(2.2 節)
- 介面應該隱藏實作細節
範例說明:
1// 反例:Leaky Abstraction:介面洩漏實作細節
2
3// Layer 4 (Domain Interface)
4abstract class IBookRepository {
5 Future<Book> findBySql(String sql); // 反例:洩漏 SQL 實作
6 Future<List<Book>> queryWithCursor(Cursor cursor); // 反例:洩漏資料庫 Cursor
7 Future<void> saveToSqlite(Book book); // 反例:洩漏 SQLite 實作
8}
9
10問題分析:
11- 介面包含「SQL」、「Cursor」、「Sqlite」等實作關鍵字
12- 外層(UseCase)需要知道使用 SQL 資料庫
13- 無法更換實作(綁定 SQLite)
14- 違反介面契約原則
15
16// 正例:抽象介面
17
18// Layer 4 (Domain Interface) - 抽象且純淨
19abstract class IBookRepository {
20 Future<Book> findById(String id); // 正例:隱藏實作細節
21 Future<List<Book>> findByAuthor(String author); // 正例:業務概念
22 Future<List<Book>> findAll(); // 正例:簡單明確
23 Future<void> save(Book book); // 正例:抽象操作
24 Future<void> delete(String id); // 正例:抽象操作
25}
26
27// Layer 5 (Infrastructure) - 具體實作可替換
28class SqliteBookRepository implements IBookRepository {
29 @override
30 Future<Book> findById(String id) async {
31 // SQL 實作細節在這裡
32 final result = await db.query(
33 'books',
34 where: 'id = ?',
35 whereArgs: [id],
36 );
37 return Book.fromJson(result.first);
38 }
39}
40
41// Layer 5 (Infrastructure) - 另一種實作
42class FirestoreBookRepository implements IBookRepository {
43 @override
44 Future<Book> findById(String id) async {
45 // Firestore 實作細節在這裡
46 final doc = await firestore.collection('books').doc(id).get();
47 return Book.fromJson(doc.data()!);
48 }
49}
50
51改善效果:
52- 介面抽象且純淨(不包含實作細節)
53- 可輕鬆更換實作(SQLite → Firestore)
54- UseCase 不需要知道資料庫實作
55- 符合依賴倒置原則2.2 B 類 Code Smell(單層級問題)
2.2.1 B1. Divergent Change(發散式變更)
定義: 單一類別因不同原因需要修改,違反 Single Responsibility Principle(SRP)。
特徵識別:
- 一個 Controller 同時負責多個頁面的邏輯
- 一個 UseCase 處理多個不相關的業務流程
- 變更原因不單一(有 2+ 個變更原因)
- 類別方法可以明確分組(2+ 個分組)
與層級隔離派工方法論 的關聯:
- 違反「單層修改原則」的 SRP 理論依據(3.2 節)
- 違反「變更原因單一」要求(3.1 節)
範例說明:
1// 反例:Divergent Change:單一 Controller 承擔多個職責
2
3class BookController {
4 // 群組 A:列表頁面邏輯(3 個方法)
5 List<BookViewModel> bookList = [];
6
7 void loadBookList() {
8 // 載入書籍列表
9 }
10
11 void refreshBookList() {
12 // 重新整理列表
13 }
14
15 void sortBookList(String sortBy) {
16 // 排序列表
17 }
18
19 // 群組 B:詳情頁面邏輯(3 個方法)
20 BookViewModel? bookDetail;
21
22 void loadBookDetail(String id) {
23 // 載入書籍詳情
24 }
25
26 void updateBookDetail() {
27 // 更新書籍詳情
28 }
29
30 void deleteBook() {
31 // 刪除書籍
32 }
33
34 // 群組 C:搜尋邏輯(2 個方法)
35 List<BookViewModel> searchResults = [];
36
37 void searchBooks(String query) {
38 // 搜尋書籍
39 }
40
41 void clearSearchResults() {
42 // 清空搜尋結果
43 }
44}
45
46問題分析:
47- 3 個方法群組(列表、詳情、搜尋)
48- 3 種變更原因(列表變更、詳情變更、搜尋變更)
49- 類別名稱過於籠統(BookController)
50- 違反 SRP 原則
51
52// 正例:拆分為多個單一職責 Controller
53
54// Controller 1:只負責列表
55class BookListController {
56 List<BookViewModel> bookList = [];
57
58 void loadBookList() { }
59 void refreshBookList() { }
60 void sortBookList(String sortBy) { }
61}
62
63// Controller 2:只負責詳情
64class BookDetailController {
65 BookViewModel? bookDetail;
66
67 void loadBookDetail(String id) { }
68 void updateBookDetail() { }
69 void deleteBook() { }
70}
71
72// Controller 3:只負責搜尋
73class BookSearchController {
74 List<BookViewModel> searchResults = [];
75
76 void searchBooks(String query) { }
77 void clearSearchResults() { }
78}
79
80改善效果:
81- 每個 Controller 只有 1 個變更原因
82- 職責明確且單一
83- 可讀性提升
84- 測試更容易(測試範圍更小)2.2.2 B2. Large Class(大類別)
定義: 類別過大,包含過多方法和屬性,職責不清。
特徵識別(量化標準,引用層級隔離派工方法論 第 5.2 節):
- 總行數: > 300 行
- public 方法: > 15 個
- 屬性: > 12 個
- 類別職責無法用一句話清楚描述
範例說明:
1// 反例:Large Class:職責過多(500+ 行)
2
3class BookService { // 總行數:500+ 行
4 // 新增書籍(20 個方法)
5 Future<void> addBook(Book book) { }
6 Future<void> addMultipleBooks(List<Book> books) { }
7 Future<void> importBooksFromCsv(String filePath) { }
8 // ... 17 個其他方法
9
10 // 查詢書籍(15 個方法)
11 Future<Book> findBook(String id) { }
12 Future<List<Book>> findBooksByAuthor(String author) { }
13 Future<List<Book>> searchBooks(String query) { }
14 // ... 12 個其他方法
15
16 // 統計分析(10 個方法)
17 Future<BookStats> getStatistics() { }
18 Future<Map<String, int>> getBooksByGenre() { }
19 Future<List<Book>> getMostPopular() { }
20 // ... 7 個其他方法
21
22 // 匯出報表(8 個方法)
23 Future<void> exportReport() { }
24 Future<void> exportToPdf() { }
25 Future<void> exportToExcel() { }
26 // ... 5 個其他方法
27}
28
29問題分析:
30- 總行數: 500+ 行(超過 300 行標準)
31- public 方法: 53 個(超過 15 個標準)
32- 4 種不同職責(新增、查詢、統計、匯出)
33- 違反 SRP 原則
34
35// 正例:拆分為多個職責明確的 Service
36
37// Service 1:書籍管理(新增、更新、刪除)
38class BookManagementService {
39 Future<void> addBook(Book book) { }
40 Future<void> updateBook(Book book) { }
41 Future<void> deleteBook(String id) { }
42}
43
44// Service 2:書籍查詢
45class BookQueryService {
46 Future<Book> findById(String id) { }
47 Future<List<Book>> findByAuthor(String author) { }
48 Future<List<Book>> search(String query) { }
49}
50
51// Service 3:書籍統計
52class BookStatisticsService {
53 Future<BookStats> getStatistics() { }
54 Future<Map<String, int>> getBooksByGenre() { }
55 Future<List<Book>> getMostPopular() { }
56}
57
58// Service 4:報表匯出
59class BookReportService {
60 Future<void> exportToPdf() { }
61 Future<void> exportToExcel() { }
62 Future<void> exportToCsv() { }
63}
64
65改善效果:
66- 每個 Service < 200 行
67- 職責單一且明確
68- 可測試性提升
69- 可維護性提升2.2.3 B3. Long Method(長方法)
定義: 方法過長,難以理解和測試。
特徵識別(量化標準):
- 方法行數: > 50 行
- 巢狀層級: > 3 層
- 邏輯區塊: > 4 個(用註解分隔)
- 方法名稱包含「And」(如
validateAndSaveBook)
範例說明:
1// 反例:Long Method:80 行方法
2
3Future<void> processBookOrder(Order order) async {
4 // 驗證訂單(20 行)
5 if (order.items.isEmpty) {
6 throw ValidationException('訂單不能為空');
7 }
8
9 for (var item in order.items) {
10 if (item.quantity <= 0) {
11 throw ValidationException('數量必須大於 0');
12 }
13 if (item.price < 0) {
14 throw ValidationException('價格不能為負數');
15 }
16 }
17
18 // 計算價格(20 行)
19 double total = 0;
20 double discount = 0;
21
22 for (var item in order.items) {
23 total += item.price * item.quantity;
24 }
25
26 if (total > 1000) {
27 discount = total * 0.1;
28 } else if (total > 500) {
29 discount = total * 0.05;
30 }
31
32 total -= discount;
33
34 // 庫存檢查(20 行)
35 for (var item in order.items) {
36 final stock = await inventoryRepository.getStock(item.bookId);
37 if (stock < item.quantity) {
38 throw InsufficientStockException(
39 '書籍 ${item.bookId} 庫存不足'
40 );
41 }
42 }
43
44 // 建立訂單(20 行)
45 order.total = total;
46 order.discount = discount;
47 order.status = OrderStatus.pending;
48 order.createdAt = DateTime.now();
49
50 await repository.save(order);
51
52 // 扣除庫存
53 for (var item in order.items) {
54 await inventoryRepository.reduceStock(
55 item.bookId,
56 item.quantity,
57 );
58 }
59}
60
61問題分析:
62- 方法行數: 80 行(超過 50 行標準)
63- 4 個邏輯區塊(驗證、計算、庫存、建立)
64- 巢狀層級: 3 層(for + if)
65- 難以理解和測試
66
67// 正例:拆分為多個小方法
68
69Future<void> processBookOrder(Order order) async {
70 _validateOrder(order); // 步驟 1
71 final total = _calculateTotal(order); // 步驟 2
72 await _checkInventory(order); // 步驟 3
73 await _saveOrder(order, total); // 步驟 4
74}
75
76void _validateOrder(Order order) {
77 if (order.items.isEmpty) {
78 throw ValidationException('訂單不能為空');
79 }
80
81 for (var item in order.items) {
82 _validateOrderItem(item);
83 }
84}
85
86void _validateOrderItem(OrderItem item) {
87 if (item.quantity <= 0) {
88 throw ValidationException('數量必須大於 0');
89 }
90 if (item.price < 0) {
91 throw ValidationException('價格不能為負數');
92 }
93}
94
95double _calculateTotal(Order order) {
96 double total = 0;
97
98 for (var item in order.items) {
99 total += item.price * item.quantity;
100 }
101
102 final discount = _calculateDiscount(total);
103 return total - discount;
104}
105
106double _calculateDiscount(double total) {
107 if (total > 1000) return total * 0.1;
108 if (total > 500) return total * 0.05;
109 return 0;
110}
111
112Future<void> _checkInventory(Order order) async {
113 for (var item in order.items) {
114 final stock = await inventoryRepository.getStock(item.bookId);
115 if (stock < item.quantity) {
116 throw InsufficientStockException(
117 '書籍 ${item.bookId} 庫存不足'
118 );
119 }
120 }
121}
122
123Future<void> _saveOrder(Order order, double total) async {
124 order.total = total;
125 order.status = OrderStatus.pending;
126 order.createdAt = DateTime.now();
127
128 await repository.save(order);
129 await _reduceInventory(order);
130}
131
132Future<void> _reduceInventory(Order order) async {
133 for (var item in order.items) {
134 await inventoryRepository.reduceStock(
135 item.bookId,
136 item.quantity,
137 );
138 }
139}
140
141改善效果:
142- 主方法只有 4 行(清楚的流程)
143- 每個私有方法 < 20 行
144- 邏輯分離且可測試
145- 可讀性大幅提升2.2.4 B4. Dead Code(死程式碼)
定義: 永遠不會執行或使用的程式碼。
特徵識別:
- 未被呼叫的方法
- 無法到達的程式碼分支
- 註解掉的程式碼超過 1 週
dart analyze顯示unused_element警告
自動化檢測方法:
1# 檢測 unused 警告
2dart analyze | grep "unused"
3
4# 檢測程式碼覆蓋率
5flutter test --coverage
6genhtml coverage/lcov.info -o coverage/html
7# 檢查 coverage/html 中 0% 覆蓋率的程式碼
8
9# 搜尋註解掉的程式碼
10grep -r "^[[:space:]]*//.*{" lib/範例說明:
1// 反例:Dead Code 範例
2
3class BookService {
4 // 未使用的方法(dart analyze 會警告)
5 void unusedMethod() {
6 print('這個方法從未被呼叫');
7 }
8
9 // 無法到達的程式碼
10 void processBook(Book book) {
11 return; // 提前返回
12
13 // 反例:以下程式碼永遠不會執行
14 print('處理書籍');
15 saveBook(book);
16 }
17
18 // 註解掉的程式碼(已過時)
19 // void oldImplementation() {
20 // // 舊的實作方式,已被新方法取代
21 // // 但程式碼保留了 2 個月
22 // }
23
24 // 未使用的變數
25 final String unusedVariable = 'never used';
26}
27
28// 正例:移除 Dead Code
29
30class BookService {
31 // 只保留實際使用的方法
32 void processBook(Book book) {
33 print('處理書籍');
34 saveBook(book);
35 }
36}
37
38改善效果:
39- 程式碼簡潔
40- 降低維護成本
41- 無混淆和誤導2.3 C 類 Code Smell(Ticket 粒度問題)
2.3.1 C1. God Ticket(全能 Ticket)
定義: 單一 Ticket 修改過多檔案和層級,範圍失控。
特徵識別(引用層級隔離派工方法論 第 5.2 節量化標準):
- 修改檔案數 > 10 個
- 跨 3 個以上架構層級
- 預估工時 > 16 小時(2 天)
與層級隔離派工方法論 的關聯:
- 違反「Ticket 粒度標準」(5.2 節)
- 違反「單層修改原則」(3.1 節)
檢測方法:
1步驟 1: 計算 Ticket 涉及的檔案數
2步驟 2: 判斷檔案所屬的層級
3步驟 3: 計算跨幾個層級
4 ├─ > 3 層級 → God Ticket
5 └─ 1 層級 → 良好 Ticket範例說明:
1Ticket: 新增「書籍評分」完整功能
2
3檔案清單(12 個檔案):
4
51. lib/presentation/widgets/book_detail_widget.dart (Layer 1)
62. lib/presentation/widgets/rating_widget.dart (Layer 1)
73. lib/presentation/controllers/book_detail_controller.dart (Layer 2)
84. lib/presentation/controllers/rating_controller.dart (Layer 2)
95. lib/application/use_cases/rate_book_use_case.dart (Layer 3)
106. lib/application/use_cases/get_book_rating_use_case.dart (Layer 3)
117. lib/domain/entities/book.dart (Layer 5)
128. lib/domain/entities/rating.dart (Layer 5)
139. lib/domain/value_objects/rating_value.dart (Layer 5)
1410. lib/infrastructure/repositories/book_repository_impl.dart (Layer 5)
1511. lib/infrastructure/repositories/rating_repository_impl.dart (Layer 5)
1612. lib/infrastructure/database/rating_table.dart (Layer 5)
17
18分析結果:
19- 檔案數: 12 個(> 10 個標準)
20- 層級跨度: 4 層(Layer 1, 2, 3, 5)
21- 預估工時: 24 小時(> 16 小時標準)
22- 判斷: God Ticket
23
24建議拆分(引用[層級隔離派工方法論](/record/layered-ticket-methodology/) 第 5.4 節拆分指引):
25- Ticket 1 [Layer 5]: Rating Value Object 和 Entity 設計
26- Ticket 2 [Layer 5]: Rating Repository 實作
27- Ticket 3 [Layer 3]: RateBookUseCase 實作
28- Ticket 4 [Layer 3]: GetBookRatingUseCase 實作
29- Ticket 5 [Layer 2]: Controller 整合 UseCase
30- Ticket 6 [Layer 1]: UI 新增評分元件
31
32改善效果:
33- 每個 Ticket 只修改 1-3 個檔案
34- 每個 Ticket 只涉及 1 個層級
35- 預估工時: 每個 Ticket 2-4 小時
36- 風險可控2.3.2 C2. Incomplete Ticket(不完整 Ticket)
定義: Ticket 缺少必要的測試、文件或驗收條件。
特徵識別:
- 沒有測試檔案(Phase 2 缺失)
- 沒有驗收條件(Phase 1 設計不完整)
- 沒有更新相關文件
- 沒有完整的 TDD 四階段記錄
檢測方法(基於 TDD 四階段要求):
1完整 Ticket 必須包含:
2- Phase 1: 功能設計完成
3- Phase 2: 測試設計完成(測試檔案存在)
4- Phase 3: 實作完成(程式碼檔案)
5- Phase 4: 重構評估完成
6
7Incomplete Ticket 特徵:
8- 缺少測試檔案(Phase 2 未完成)
9- 缺少驗收條件(Phase 1 設計不完整)
10- 缺少工作日誌(無法追蹤進度)檢測流程(Code Review 階段):
1步驟 1: 檢查 git diff 中的檔案
2 ├─ 是否包含 test/ 目錄的檔案?
3 └─ 測試檔案數量 vs 程式碼檔案數量比例
4
5步驟 2: 檢查 Ticket 描述
6 └─ 是否包含「驗收條件」章節?
7
8步驟 3: 檢查工作日誌
9 ├─ docs/work-logs/vX.Y.Z-*.md 是否存在?
10 └─ 是否完成 Phase 1-4 記錄?2.3.3 C3. Ambiguous Responsibility(職責模糊 Ticket)
定義: Ticket 的職責定義不明確,無法判斷屬於哪一層級。
特徵識別:
- Ticket 標題沒有標明層級(如 [Layer X])
- 描述中混合多個層級的職責
- 驗收條件跨多個層級
檢測方法:
1職責明確 Ticket 格式:
2標題: [Layer X] 清楚的功能描述
3描述: 明確說明修改哪一層的哪個檔案
4驗收: 只驗證該層級的職責
5
6職責模糊 Ticket 特徵:
7標題: 沒有 [Layer X] 標示
8描述: 混合多個層級的職責
9驗證: 跨多個層級的驗證範例說明:
1反例 職責模糊 Ticket:
2標題: 實作書籍詳情頁面
3描述: 實作書籍詳情頁面的所有功能
4驗收: 可以顯示書籍詳情
5
6問題分析:
7- 無層級標示
8- 「所有功能」範圍不明確
9- 驗收條件過於籠統
10
11正例 職責明確 Ticket:
12標題: [Layer 2] 實作書籍詳情頁面事件處理
13描述: 實作 BookDetailController 的事件處理方法,
14 整合 GetBookDetailUseCase 並轉換為 ViewModel
15驗收:
16 - BookDetailController.loadBookDetail() 呼叫 UseCase
17 - BookPresenter.toViewModel() 正確轉換資料
18 - 錯誤處理正確顯示錯誤訊息
19
20改善效果:
21- 層級明確(Layer 2)
22- 職責清楚(事件處理 + 資料轉換)
23- 驗收條件可驗證第三章:Ticket 粒度檢測方法
3.1 檢測時機和流程
核心理念: 從 Ticket 設計階段就能發現 Code Smell,比實作完成後才發現更有效率。
檢測時機對應 TDD 四階段:
| 階段 | 檢測時機 | 檢測重點 | 對應 Code Smell |
|---|---|---|---|
| Phase 1 設計階段 | Ticket 設計完成時 | Ticket 粒度和層級定位 | C1, C2, C3, A1 |
| Phase 2 測試設計 | 測試設計完成時 | 測試範圍是否限定在單一層級 | C2, B1 |
| Phase 3 實作執行 | 程式碼提交時 | 實作是否產生 Code Smell | A2, A3, A4, B2, B3 |
| Code Review | PR 提交時 | 最終驗證 | 所有 Code Smell |
| Phase 4 重構階段 | 重構評估時 | 識別需要重構的 Code Smell | B1, B2, B3, B4 |
檢測流程總覽:
1Ticket 設計(Phase 1)
2 ↓
3檢查 Ticket 粒度(C1, C3, A1)
4 ├─ 通過 → 測試設計(Phase 2)
5 └─ 失敗 → 拆分 Ticket
6
7測試設計(Phase 2)
8 ↓
9檢查測試範圍(C2)
10 ├─ 通過 → 實作執行(Phase 3)
11 └─ 失敗 → 補充測試
12
13實作執行(Phase 3)
14 ↓
15檢查程式碼品質(A2, A3, A4, B2, B3)
16 ├─ 通過 → Code Review
17 └─ 失敗 → 修正程式碼
18
19Code Review
20 ↓
21全面檢查所有 Code Smell
22 ├─ 通過 → 合併 PR
23 └─ 失敗 → 重構
24
25Phase 4 重構評估
26 ↓
27識別需要重構的 Code Smell(B1, B2, B3, B4)
28 ├─ 有需要 → 執行重構
29 └─ 無需要 → 完成3.2 A 類 Code Smell 檢測方法(跨層級問題)
3.2.1 A1. Shotgun Surgery 檢測
檢測指標:
- 檔案數量指標: 單一 Ticket 修改的檔案數
- 層級跨度指標: Ticket 涉及的層級數量
- 依賴鏈長度指標: 從 UI 到 Domain 的依賴鏈長度
判斷標準(引用層級隔離派工方法論 第 5.2 節):
1良好 Ticket(單層修改):
2- 檔案數: 1-5 個
3- 層級跨度: 1 層
4- 依賴鏈: 不需要修改
5
6需要注意(考慮拆分):
7- 檔案數: 6-10 個
8- 層級跨度: 2 層
9- 依賴鏈: 需要修改 1-2 層
10
11Shotgun Surgery(散彈槍手術):
12- 檔案數: > 10 個
13- 層級跨度: > 2 層
14- 依賴鏈: 需要同步修改多層檢測流程(基於層級隔離派工方法論 第 6.2 節檔案路徑分析法):
1步驟 1: 列出 Ticket 涉及的所有檔案
2步驟 2: 使用[層級隔離派工方法論](/record/layered-ticket-methodology/) 第 2.4 節的決策樹判斷每個檔案屬於哪一層
3步驟 3: 統計跨幾個層級
4 ├─ 1 層級 → 良好設計
5 ├─ 2 層級 → 需要檢查是否可拆分
6 └─ > 2 層級 → Shotgun Surgery
7
8步驟 4: 如果檢測到 Shotgun Surgery
9 ├─ 檢查是否為特殊場景(架構遷移、Hotfix)
10 ├─ 分析是否可以拆分為多個 Ticket
11 └─ 評估架構設計是否有問題(引入 Adapter/Facade)3.2.2 A2. Feature Envy 檢測
檢測指標:
- 直接依賴指標: 外層是否直接依賴內層的具體類別
- 欄位存取指標: 外層存取內層的內部欄位次數
- ViewModel 缺失指標: Layer 2 是否缺少資料轉換
判斷標準:
1良好設計:
2- UI 依賴 ViewModel,不依賴 Domain Entity
3- Controller 包含 Presenter 轉換邏輯
4- 透過介面依賴,不依賴具體實作
5
6Feature Envy:
7- UI 直接依賴 Domain Entity
8- 直接存取 Entity 的內部欄位(如 book.isbn.value)
9- 缺少 ViewModel 或 Presenter 轉換層
10- 欄位存取次數 > 3 次檢測流程:
1步驟 1: 檢查 UI Widget 的依賴
2 ├─ 是否直接依賴 Domain Entity?
3 └─ 是否透過 ViewModel?
4
5步驟 2: 檢查 Controller 是否包含 Presenter
6 ├─ 是否有 toViewModel() 轉換方法?
7 └─ 是否直接將 Entity 傳給 UI?
8
9步驟 3: 統計內層欄位存取次數
10 ├─ 存取 Entity 內部欄位(如 .value)> 3 次 → Feature Envy
11 └─ 透過 ViewModel 存取 → 良好設計3.2.3 A3. Inappropriate Intimacy 檢測
檢測指標:
- 依賴方向檢查: 內層是否依賴外層
- 循環依賴檢查: 是否存在雙向依賴
- Domain 純淨度檢查: Domain 是否包含 UI 或 Infrastructure 依賴
判斷標準(引用層級隔離派工方法論 第 2.3 節依賴方向規則):
1正確依賴方向(外層→內層):
2Layer 1 → Layer 2 → Layer 3 → Layer 4 ← Layer 5
3
4違反依賴方向(Inappropriate Intimacy):
5- Layer 5 → Layer 3(Domain 依賴 UseCase)
6- Layer 5 → Layer 2(Domain 依賴 Controller)
7- Layer 3 ← → Layer 5(循環依賴)檢測流程:
1步驟 1: 檢查 Domain Entity 的 import 語句
2 ├─ 是否 import UseCase? →
3 ├─ 是否 import Controller? →
4 └─ 只 import 同層或 Layer 4 介面? →
5
6步驟 2: 檢查 UseCase 的依賴
7 ├─ 是否依賴 Layer 4 介面? →
8 └─ 是否依賴 Layer 5 具體類別? →(應透過介面)
9
10步驟 3: 使用工具檢測循環依賴
11 └─ dart analyze 會報告循環依賴錯誤3.2.4 A4. Leaky Abstraction 檢測
檢測指標:
- 介面純淨度: 介面是否包含實作細節
- 參數檢查: 方法參數是否洩漏實作資訊
- 回傳類型檢查: 是否回傳 Infrastructure 特定類型
判斷標準:
1良好抽象介面:
2- 方法名稱描述「做什麼」,不描述「怎麼做」
3- 參數是 Domain 概念,不是技術細節
4- 不包含資料庫、網路等實作關鍵字
5
6Leaky Abstraction:
7- 介面包含 SQL、HTTP、Cache 等關鍵字
8- 參數包含資料庫特定類型(如 Cursor)
9- 回傳類型包含框架特定類型(如 HttpResponse)檢測流程:
1步驟 1: 檢查 Repository 介面定義
2 ├─ 方法名稱是否包含實作關鍵字?
3 │ - findBySql() → 洩漏 SQL
4 │ - findById() → 抽象概念
5 │
6 └─ 參數類型是否為 Domain 類型?
7 - String sql → 技術細節
8 - String id → Domain 概念
9
10步驟 2: 檢查 Event 定義
11 └─ 是否包含 UI 特定資料(如 BuildContext)? →3.3 B 類 Code Smell 檢測方法(單層級問題)
3.3.1 B1. Divergent Change 檢測
檢測指標:
- 類別職責數量: 類別承擔幾個不同的職責
- 變更原因數量: 有幾種不同的原因需要修改此類別
- 方法分組檢查: 方法是否可以明確分組
判斷標準:
1單一職責類別:
2- 只有 1 個變更原因
3- 類別職責可以用一句話描述
4- 所有方法圍繞同一個核心概念
5
6Divergent Change:
7- > 2 個變更原因(如「列表變更」和「詳情變更」)
8- 方法可以分為 2+ 個明確的群組
9- 類別名稱過於籠統(如 BookController、BookService)檢測流程:
1步驟 1: 分析類別的 public 方法
2 └─ 將方法按職責分組
3
4步驟 2: 統計分組數量
5 ├─ 1 組 → 單一職責
6 ├─ 2 組 → 考慮拆分
7 └─ > 2 組 → Divergent Change
8
9步驟 3: 檢查歷史修改記錄
10 └─ git log --oneline {file}
11 └─ 分析修改原因是否多樣化3.3.2 B2. Large Class 檢測
檢測指標:
- 程式碼行數: 類別總行數
- 方法數量: public 方法數量
- 屬性數量: instance 變數數量
判斷標準(量化指標):
1良好大小類別:
2- 總行數: < 200 行
3- public 方法: < 10 個
4- 屬性: < 8 個
5
6需要注意(考慮拆分):
7- 總行數: 200-300 行
8- public 方法: 10-15 個
9- 屬性: 8-12 個
10
11Large Class:
12- 總行數: > 300 行
13- public 方法: > 15 個
14- 屬性: > 12 個自動化檢測方法:
1# 檢測單一檔案行數
2wc -l lib/presentation/controllers/book_controller.dart
3
4# 檢測所有 Controller 檔案大小
5find lib -name "*_controller.dart" -exec wc -l {} \; | sort -rn
6
7# 使用 dart analyze 檢測複雜度
8# (需要配置 analysis_options.yaml)3.3.3 B3. Long Method 檢測
檢測指標:
- 方法行數: 方法內程式碼行數
- 巢狀層級: if/for/while 的巢狀深度
- 區塊數量: 方法內邏輯區塊數量(用註解分隔)
判斷標準:
1良好方法:
2- 行數: < 30 行
3- 巢狀層級: < 3 層
4- 邏輯區塊: < 3 個
5
6需要注意:
7- 行數: 30-50 行
8- 巢狀層級: 3 層
9- 邏輯區塊: 3-4 個
10
11Long Method:
12- 行數: > 50 行
13- 巢狀層級: > 3 層
14- 邏輯區塊: > 4 個檢測流程:
1步驟 1: 統計方法行數
2 └─ 從方法簽名到結束大括號的行數
3
4步驟 2: 分析巢狀層級
5 └─ 統計最深的 if/for/while 巢狀深度
6
7步驟 3: 識別邏輯區塊
8 └─ 統計註解數量(通常用來分隔邏輯區塊)
9 └─ > 3 個註解區塊 → 應該拆分方法
10
11步驟 4: 檢查方法命名
12 └─ 方法名稱是否包含「And」? → 可能做太多事情
13 - validateAndSaveBook() → 應拆分為 validate() 和 save()3.3.4 B4. Dead Code 檢測
檢測方法:
- 使用 dart analyze 檢測 unused 警告
- 使用 code coverage 工具檢測 0% 覆蓋率程式碼
- 手動檢查註解掉的程式碼
自動化檢測:
1# 檢測 unused 警告
2dart analyze | grep "unused"
3
4# 檢測程式碼覆蓋率
5flutter test --coverage
6genhtml coverage/lcov.info -o coverage/html
7# 檢查 coverage/html 中 0% 覆蓋率的程式碼
8
9# 搜尋註解掉的程式碼
10grep -r "^[[:space:]]*//.*{" lib/3.4 C 類 Code Smell 檢測方法(Ticket 粒度問題)
3.4.1 C1. God Ticket 檢測
檢測指標(引用層級隔離派工方法論 第 5.2 節):
- 檔案修改數量: 計算 git diff 涉及的檔案數
- 層級跨度: 涉及幾個架構層級
- 預估工時: Ticket 的預估完成時間
判斷標準:
1良好 Ticket 粒度:
2- 檔案數: 1-5 個
3- 層級跨度: 1 層
4- 預估工時: 2-8 小時(1 個工作天內)
5
6需要拆分:
7- 檔案數: 6-10 個
8- 層級跨度: 2 層
9- 預估工時: 8-16 小時(1-2 天)
10
11God Ticket:
12- 檔案數: > 10 個
13- 層級跨度: > 2 層
14- 預估工時: > 16 小時(> 2 天)檢測流程(Ticket 設計階段):
1步驟 1: 列出 Ticket 需要修改的檔案清單
2步驟 2: 統計檔案數量
3步驟 3: 使用[層級隔離派工方法論](/record/layered-ticket-methodology/) 第 2.4 節決策樹判斷每個檔案的層級
4步驟 4: 計算層級跨度
5步驟 5: 評估預估工時
6
7判斷:
8 ├─ 符合良好標準 → 可執行
9 ├─ 符合需要拆分標準 → 建議拆分
10 └─ 符合 God Ticket 標準 → 強制拆分3.4.2 C2. Incomplete Ticket 檢測
檢測指標:
- 測試檔案檢查: 是否有對應的測試檔案
- 驗收條件檢查: Ticket 描述是否包含驗收條件
- 工作日誌檢查: 是否完成 TDD 四階段記錄
判斷標準(基於 TDD 四階段要求):
1完整 Ticket:
2- Phase 1: 功能設計完成
3- Phase 2: 測試設計完成(測試檔案存在)
4- Phase 3: 實作完成(程式碼檔案)
5- Phase 4: 重構評估完成
6
7Incomplete Ticket:
8- 缺少測試檔案(Phase 2 未完成)
9- 缺少驗收條件(Phase 1 設計不完整)
10- 缺少工作日誌(無法追蹤進度)檢測流程(Code Review 階段):
1步驟 1: 檢查 git diff 中的檔案
2 ├─ 是否包含 test/ 目錄的檔案?
3 └─ 測試檔案數量 vs 程式碼檔案數量比例
4
5步驟 2: 檢查 Ticket 描述
6 └─ 是否包含「驗收條件」章節?
7
8步驟 3: 檢查工作日誌
9 ├─ docs/work-logs/vX.Y.Z-*.md 是否存在?
10 └─ 是否完成 Phase 1-4 記錄?3.4.3 C3. Ambiguous Responsibility 檢測
檢測指標:
- Ticket 標題格式: 是否包含層級標示
- 職責描述清晰度: 是否明確說明修改哪一層
- 驗收條件對應性: 驗收條件是否對應單一層級
判斷標準:
1職責明確 Ticket:
2- 標題: [Layer X] 清楚的功能描述
3- 描述: 明確說明修改哪一層的哪個檔案
4- 驗收: 只驗證該層級的職責
5
6職責模糊 Ticket:
7- 標題: 沒有 [Layer X] 標示
8- 描述: 混合多個層級的職責
9- 驗收: 跨多個層級的驗證檢測流程(Ticket 設計階段):
1步驟 1: 檢查 Ticket 標題格式
2 ├─ 符合 [Layer X] 格式? →
3 └─ 無層級標示? →
4
5步驟 2: 分析 Ticket 描述
6 └─ 能否用一句話描述單一層級的職責?
7
8步驟 3: 檢查驗收條件
9 ├─ 所有驗收條件都屬於同一層級? →
10 └─ 驗收條件跨多層? →3.5 檢測方法總結表
| Code Smell | 檢測時機 | 檢測指標 | 判斷標準 | 引用層級隔離派工方法論 章節 |
|---|---|---|---|---|
| A1. Shotgun Surgery | Ticket 設計 | 層級跨度 | > 2 層 | 3.1 單層修改原則 |
| A2. Feature Envy | Code Review | 直接依賴 Domain | UI 存取 Entity | 2.2 Layer 2 職責 |
| A3. Inappropriate Intimacy | Code Review | 依賴方向 | 內層依賴外層 | 2.3 依賴方向規則 |
| A4. Leaky Abstraction | 介面設計 | 介面純淨度 | 包含實作關鍵字 | 2.2 Layer 4 職責 |
| B1. Divergent Change | Phase 4 重構 | 方法分組數 | > 2 組 | 3.2 SRP 理論 |
| B2. Large Class | Phase 4 重構 | 程式碼行數 | > 300 行 | 5.2 量化指標 |
| B3. Long Method | Phase 3 實作 | 方法行數 | > 50 行 | 5.2 量化指標 |
| B4. Dead Code | Phase 4 重構 | unused 警告 | dart analyze | - |
| C1. God Ticket | Ticket 設計 | 檔案數 | > 10 個 | 5.2 Ticket 粒度 |
| C2. Incomplete Ticket | Code Review | 測試檔案 | 缺少測試 | TDD 四階段 |
| C3. Ambiguous Responsibility | Ticket 設計 | 標題格式 | 無層級標示 | 5.3 Ticket 範例 |
第四章:重構建議和策略
4.1 重構模式對應表
每種 Code Smell 都有對應的標準重構模式(引用 Martin Fowler 的 Refactoring 書籍):
| Code Smell | 重構模式 | 重構策略 | 預期效果 |
|---|---|---|---|
| A1. Shotgun Surgery | Extract Interface + Introduce Facade | 引入抽象層隔離變更 | 單層修改 |
| A2. Feature Envy | Move Method + Extract ViewModel | 移動邏輯到正確層級 | 職責對齊 |
| A3. Inappropriate Intimacy | Introduce Parameter Object | 打破循環依賴 | 依賴方向正確 |
| A4. Leaky Abstraction | Extract Interface | 重新設計抽象介面 | 隱藏實作細節 |
| B1. Divergent Change | Extract Class | 拆分為多個單一職責類別 | 符合 SRP |
| B2. Large Class | Extract Class + Move Method | 拆分大類別 | 降低複雜度 |
| B3. Long Method | Extract Method | 拆分長方法 | 提升可讀性 |
| B4. Dead Code | Remove Dead Code | 直接刪除 | 程式碼簡潔 |
| C1. God Ticket | Split Ticket | 拆分為多個單層 Ticket | 降低風險 |
| C2. Incomplete Ticket | Add Missing Tests | 補充測試和文件 | 完整性 |
| C3. Ambiguous Responsibility | Clarify Responsibility | 明確層級和職責 | 職責清晰 |
4.2 重構策略詳細說明
4.2.1 A1. Shotgun Surgery → Extract Interface + Introduce Facade
問題: 單一變更需要同時修改多個層級
重構步驟:
1步驟 1: 分析變更的共同點
2 └─ 識別哪些變更是因為相同的業務需求
3
4步驟 2: 引入 Facade 層
5 └─ 建立統一的介面封裝跨層操作
6
7步驟 3: 重構為單層修改
8 ├─ Layer 4: 定義 Facade 介面
9 ├─ Layer 3: 實作 Facade
10 └─ Layer 2: 呼叫 Facade
11
12步驟 4: 驗證重構結果
13 └─ 未來相同變更只需要修改 Facade 實作完整範例:
1// 反例:Before: 新增欄位需要修改 4 層
2// Layer 1: UI 新增 Widget
3// Layer 2: Controller 新增屬性
4// Layer 3: UseCase 新增參數
5// Layer 5: Entity 新增欄位
6
7// 正例:After: 引入 BookDetailFacade
8
9// Layer 4: 定義介面
10abstract class IBookDetailFacade {
11 Future<BookDetailViewModel> getBookDetail(String id);
12}
13
14// Layer 3: 實作 Facade(統一處理資料整合)
15class BookDetailFacade implements IBookDetailFacade {
16 final IBookRepository bookRepository;
17 final IRatingRepository ratingRepository;
18
19 Future<BookDetailViewModel> getBookDetail(String id) async {
20 final book = await bookRepository.findById(id);
21 final rating = await ratingRepository.findByBookId(id);
22 return BookPresenter.toViewModel(book, rating);
23 }
24}
25
26// 重構效果:
27// 未來新增欄位只需要修改 Facade 實作(Layer 3)
28// Layer 1, 2, 5 都不需要修改4.2.2 A2. Feature Envy → Move Method + Extract ViewModel
問題: 外層過度依賴內層的實作細節
重構步驟:
1步驟 1: 識別 Feature Envy 位置
2 └─ 外層存取內層內部欄位 > 3 次
3
4步驟 2: 引入 ViewModel
5 └─ Layer 2 建立 ViewModel 類別
6
7步驟 3: 建立 Presenter 轉換方法
8 └─ Layer 2 實作 toViewModel(Entity) → ViewModel
9
10步驟 4: 重構 UI 依賴
11 └─ UI 改為依賴 ViewModel,不依賴 Entity4.2.3 B1. Divergent Change → Extract Class
問題: 單一類別承擔多個職責
重構步驟:
1步驟 1: 分析方法分組
2 └─ 將 public 方法按職責分組
3
4步驟 2: 為每個分組建立新類別
5 └─ 拆分為 BookListController, BookDetailController, BookSearchController
6
7步驟 3: 移動方法到新類別
8 └─ Move Method 重構
9
10步驟 4: 更新依賴關係
11 └─ 更新 Widget 的依賴4.2.4 B3. Long Method → Extract Method
問題: 方法過長(> 50 行)
重構步驟:
1步驟 1: 識別邏輯區塊
2 └─ 統計註解數量(每個註解代表一個邏輯區塊)
3
4步驟 2: 為每個區塊建立私有方法
5 └─ Extract Method 重構
6
7步驟 3: 重新命名方法
8 └─ 使用動詞片語描述方法功能
9
10步驟 4: 驗證重構結果
11 └─ 主方法 < 30 行
12 └─ 每個私有方法 < 20 行4.3 重構優先級評估標準
評估維度:
- 影響範圍: 影響多少檔案和層級(1-5 分)
- 業務風險: 是否影響核心業務流程(1-5 分)
- 累積速度: 不修正會多快惡化(1-5 分)
優先級評估公式:
1優先級分數 = (影響範圍 × 3) + (業務風險 × 2) + (累積速度 × 1)
2
3影響範圍評分:
4
51 分: 單一檔案
62 分: 2-3 個檔案
73 分: 4-5 個檔案(單層)
84 分: 6-10 個檔案(跨 2 層)
95 分: > 10 個檔案(跨 3+ 層)
10
11業務風險評分:
12
131 分: 輔助功能(UI 優化)
142 分: 次要功能(搜尋)
153 分: 常用功能(列表顯示)
164 分: 重要功能(新增書籍)
175 分: 核心功能(資料同步)
18
19累積速度評分:
20
211 分: 已穩定,不再惡化
222 分: 偶爾新增(每季 1 次)
233 分: 定期新增(每月 1-2 次)
244 分: 頻繁新增(每週 1 次)
255 分: 持續惡化(每天都在新增)
26
27優先級判斷:
28分數 > 20 → 高優先級(立即修正)
29分數 10-20 → 中優先級(排入下個版本)
30分數 < 10 → 低優先級(重構階段處理)優先級矩陣範例:
| Code Smell | 影響範圍 | 業務風險 | 累積速度 | 分數 | 優先級 |
|---|---|---|---|---|---|
| Inappropriate Intimacy | 4 | 5 | 3 | 26 | 高 |
| Shotgun Surgery | 5 | 4 | 2 | 25 | 高 |
| God Ticket | 5 | 3 | 3 | 24 | 高 |
| Feature Envy | 3 | 3 | 3 | 15 | 中 |
| Large Class | 2 | 3 | 4 | 16 | 中 |
| Long Method | 1 | 2 | 3 | 8 | 低 |
| Dead Code | 1 | 1 | 1 | 4 | 低 |
4.4 重構風險控制策略
風險控制原則:
- 測試覆蓋率要求: 重構前必須確保測試覆蓋率 100%
- 漸進式重構: 每次只重構一個 Code Smell
- 回滾計畫: 準備 git revert 的回滾點
漸進式重構流程:
1步驟 1: 建立 feature branch
2 └─ git checkout -b refactor/fix-shotgun-surgery
3
4步驟 2: 確保測試 100% 通過
5 └─ flutter test(重構前基準)
6
7步驟 3: 執行重構
8 └─ 每完成一個小步驟都執行測試
9
10步驟 4: 提交重構結果
11 └─ git commit -m "refactor: extract BookDetailFacade"
12
13步驟 5: Code Review
14 └─ 確保重構符合層級隔離原則
15
16步驟 6: 合併到主線
17 └─ git merge --no-ff refactor/fix-shotgun-surgery測試覆蓋率監控:
1# 重構前
2flutter test --coverage
3# 記錄覆蓋率基準(如 85%)
4
5# 重構後
6flutter test --coverage
7# 確保覆蓋率不降低(≥ 85%)第五章:開發階段檢查清單
5.1 Phase 1 設計階段檢查清單(Ticket 設計)
目標: 在設計階段就發現 Code Smell,避免實作後才修正
檢查項目:
層級定位檢查
- Ticket 標題包含層級標示(如 [Layer 2])
- 職責描述清楚說明修改哪一層
- 使用層級隔離派工方法論 第 2.4 節決策樹確認層級定位正確
單層修改檢查(引用層級隔離派工方法論 第 3.1 節)
- 所有檔案都屬於同一層級
- 變更原因單一且明確
- 不需要同步修改其他層級
Ticket 粒度檢查(引用層級隔離派工方法論 第 5.2 節)
- 檔案數: 1-5 個
- 預估工時: 2-8 小時(1 個工作天內)
- 如果超過標準,規劃拆分策略
Code Smell 預防檢查
- 檢查是否有 Shotgun Surgery 風險(層級跨度 > 1)
- 檢查是否有 God Ticket 風險(檔案數 > 5)
- 檢查是否有 Ambiguous Responsibility 風險(職責不明確)
依賴關係檢查
- 依賴的內層介面已存在(或同時設計)
- 依賴方向正確(外層→內層)
- 不存在循環依賴
5.2 Phase 2 測試設計階段檢查清單
目標: 確保測試範圍限定在單一層級
檢查項目:
測試範圍檢查(引用層級隔離派工方法論 第 6.4 節)
- 測試只驗證該層級的職責
- 不需要啟動其他層級(使用 Mock)
- 測試檔案路徑對應層級結構
測試獨立性檢查
- 測試不依賴外部資源(資料庫、網路)
- 測試可以獨立執行(不依賴其他測試)
- 使用 Mock/Stub 隔離依賴
測試完整性檢查
- 正常流程測試(Happy Path)
- 異常流程測試(Error Cases)
- 邊界條件測試(Boundary Conditions)
Code Smell 檢查
- 檢查是否有 Incomplete Ticket 風險(缺少測試)
- 測試覆蓋率目標設定(100%)
5.3 Phase 3 實作階段檢查清單
目標: 確保實作符合層級隔離原則,不產生 Code Smell
檢查項目:
程式碼品質檢查
- 方法行數 < 50 行(避免 Long Method)
- 類別行數 < 300 行(避免 Large Class)
- 巢狀層級 < 3 層
- 使用 package 導入格式(避免相對路徑)
層級隔離檢查
- import 語句只引用內層或同層
- 不存在內層依賴外層的情況
- 使用介面依賴,不依賴具體實作
Code Smell 檢查
- 檢查是否有 Feature Envy(UI 直接存取 Domain)
- 檢查是否有 Inappropriate Intimacy(依賴方向錯誤)
- 檢查是否有 Leaky Abstraction(介面洩漏實作)
- 檢查是否有 Divergent Change(方法可分組)
測試執行檢查
- 所有測試 100% 通過
- dart analyze 無錯誤和警告
- 程式碼覆蓋率達到 100%
5.4 Phase 4 重構階段檢查清單
目標: 識別需要重構的 Code Smell
檢查項目:
Code Smell 掃描
- 使用 dart analyze 檢測 unused 警告(Dead Code)
- 檢查方法行數和類別行數(Long Method, Large Class)
- 檢查方法分組(Divergent Change)
- 檢查依賴方向(Inappropriate Intimacy)
重構優先級評估
- 計算影響範圍(1-5)
- 評估業務風險(1-5)
- 評估累積速度(1-5)
- 計算優先級分數
重構執行檢查
- 重構前測試覆蓋率基準
- 漸進式重構(每次一個 Code Smell)
- 重構後測試覆蓋率不降低
- Code Review 確認重構正確性
重構完成檢查
- Code Smell 已修正
- 所有測試通過
- 工作日誌記錄重構決策
第一批次撰寫完成(第一章到第五章)
第六章:Code Review 檢查清單
6.1 快速檢查(5 分鐘)
目標: 快速識別 PR 中的明顯 Code Smell
檢查項目:
層級隔離快速檢查(引用層級隔離派工方法論 第 6.2 節)
檔案路徑檢查: 所有修改檔案都屬於同一層級?
- 使用層級隔離派工方法論 第 2.4 節決策樹快速判斷
- 如果跨多層 → 檢查是否有 Shotgun Surgery
import 語句檢查: 依賴方向正確?
- 檢查是否有內層依賴外層(Inappropriate Intimacy)
- 檢查是否有 UI 直接 import Domain Entity(Feature Envy)
測試檔案檢查: 測試路徑對應層級結構?
- test/ 目錄結構是否對應 lib/ 結構
- 測試檔案數量是否與程式碼檔案數量相當
Ticket 粒度快速檢查
檔案數量 < 5 個?
5 個檔案 → 可能是 God Ticket
10 個檔案 → 強烈建議拆分
程式碼變更行數合理(< 500 行)?
- 變更行數過多可能暗示 Ticket 範圍過大
明顯 Code Smell 檢查
UI 層是否包含業務邏輯?
- 檢查 Widget 中是否有業務規則判斷
- 檢查是否有業務計算邏輯
Domain 層是否依賴外層?
- 檢查 Domain Entity 的 import 語句
- 確認沒有依賴 UseCase 或 Controller
是否有註解掉的程式碼?
- 註解掉的程式碼應該刪除,不應保留
6.2 深度檢查(15 分鐘)
目標: 全面檢查所有類別的 Code Smell
A 類 Code Smell 檢查(跨層級)
Shotgun Surgery 檢查
- 統計 PR 修改的檔案數和層級跨度
- 檢查是否有單一變更需要修改多個層級
- 評估是否應該引入 Facade 隔離變更
Feature Envy 檢查
檢查 UI 是否直接依賴 Entity
- 搜尋
import .*/domains/.*/entities/ - 確認 UI 使用 ViewModel
- 搜尋
統計外層存取內層內部欄位次數
- 超過 3 次 → Feature Envy
- 建議引入 Presenter 轉換
Inappropriate Intimacy 檢查
檢查依賴方向是否正確
- Domain 不應依賴外層
- UseCase 應依賴介面,不依賴具體實作
檢查是否有循環依賴
- 執行
dart analyze確認
- 執行
Leaky Abstraction 檢查
檢查 Repository 介面是否洩漏實作細節
- 方法名稱不應包含 SQL、HTTP、Cache 等關鍵字
- 參數類型應該是 Domain 概念
檢查 Event 定義是否包含 UI 特定資料
- 不應包含 BuildContext 等 UI 類型
B 類 Code Smell 檢查(單層級)
Divergent Change 檢查
- 分析類別方法是否可以分組
2 個群組 → Divergent Change
- 建議拆分為多個單一職責類別
Large Class 檢查
檢查類別行數是否超過 300 行
- 使用
wc -l {file}檢查 - 超過標準 → 建議拆分
- 使用
檢查 public 方法數量是否超過 15 個
檢查屬性數量是否超過 12 個
Long Method 檢查
檢查方法行數是否超過 50 行
- 超過標準 → 建議 Extract Method
檢查巢狀層級是否超過 3 層
- 過深巢狀 → 難以理解和測試
檢查方法名稱是否包含「And」
- 如
validateAndSave→ 應拆分
- 如
Dead Code 檢查
- 執行
dart analyze | grep "unused"- 檢查是否有 unused 警告
- 確認所有警告都已處理
測試完整性檢查
測試覆蓋率是否達到 100%?
- 執行
flutter test --coverage - 檢查 coverage 報告
- 執行
測試是否包含異常流程?
- 確認有 Error Cases 測試
測試是否獨立(不依賴外部資源)?
- 確認使用 Mock/Stub 隔離依賴
6.3 違規模式識別(引用層級隔離派工方法論 第 6.5 節)
常見違規模式:
違規模式 1: UI 層包含業務邏輯
1// 反例:違規
2class BookDetailWidget {
3 Widget build(BuildContext context) {
4 // 反例:業務規則不應在 UI 層
5 if (book.publicationDate.year >= 2024) {
6 return Text('新書');
7 }
8
9 // 反例:業務計算不應在 UI 層
10 final discountedPrice = book.price * 0.9;
11 return Text('優惠價: $discountedPrice');
12 }
13}
14
15// 正例:業務邏輯在 Domain 層
16class Book {
17 bool isNewRelease() {
18 return publicationDate.year >= 2024;
19 }
20
21 double getDiscountedPrice() {
22 return price * 0.9;
23 }
24}
25
26// 正例:UI 使用 ViewModel
27class BookDetailWidget {
28 Widget build(BuildContext context) {
29 return Column(
30 children: [
31 if (viewModel.isNew) Text('新書'),
32 Text('優惠價: ${viewModel.discountedPrice}'),
33 ],
34 );
35 }
36}違規模式 2: Controller 包含業務規則
1// 反例:違規
2class BookController {
3 void validateBook(Book book) {
4 // 反例:業務規則應在 Domain 層
5 if (book.isbn.length != 13) {
6 throw ValidationException('ISBN 必須是 13 碼');
7 }
8 }
9}
10
11// 正例:業務規則在 Domain 層
12class ISBN {
13 final String value;
14
15 ISBN(this.value) {
16 if (value.length != 13) {
17 throw ValidationException('ISBN 必須是 13 碼');
18 }
19 }
20}違規模式 3: UseCase 包含 UI 邏輯
1// 反例:違規
2class GetBookDetailUseCase {
3 Future<String> execute(String id) async {
4 final book = await repository.findById(id);
5 // 反例:UI 格式化不應在 UseCase
6 return '書名: ${book.title}';
7 }
8}
9
10// 正例:UseCase 回傳 Domain 類型
11class GetBookDetailUseCase {
12 Future<Book> execute(String id) async {
13 return await repository.findById(id);
14 }
15}
16
17// 正例:Presenter 負責轉換
18class BookPresenter {
19 static BookViewModel toViewModel(Book book) {
20 return BookViewModel(
21 displayText: '書名: ${book.title.value}',
22 );
23 }
24}6.4 Code Review 報告模板
Code Smell 檢測報告格式:
1# Code Smell 檢測報告
2
3**檢測時間**: 2025-10-11 14:30
4**檢測範圍**: PR #123 - [Layer 2] 實作書籍詳情頁面事件處理
5**Reviewer**: @reviewer-name
6
7---
8
9## 檢測總結
10- **高優先級問題**: 1 個
11- **中優先級問題**: 1 個
12- **低優先級問題**: 0 個
13- **總體評估**: 需要修正後再合併
14
15---
16
17## 高優先級問題
18### Shotgun Surgery 檢測結果
19**檔案清單**:
20- lib/presentation/widgets/book_detail_widget.dart (Layer 1)
21- lib/presentation/controllers/book_detail_controller.dart (Layer 2)
22- lib/application/use_cases/get_book_detail_use_case.dart (Layer 3)
23- lib/domain/entities/book.dart (Layer 5)
24
25**分析**:
26- 檔案數: 4 個
27- 層級跨度: 4 層(Layer 1, 2, 3, 5)
28- 判斷: Shotgun Surgery
29
30**建議**:
31- 拆分為 4 個獨立 Ticket
32- 每個 Ticket 只修改單一層級
33- 引入 Facade 隔離變更
34
35**影響評估**:
36- 影響範圍: 5 分(跨 4 層)
37- 業務風險: 4 分(重要功能)
38- 累積速度: 2 分(偶爾新增)
39- 優先級分數: 25(高優先級)
40
41---
42
43## 中優先級問題
44### 警告 Large Class 檢測結果
45
46**檔案**: `lib/presentation/controllers/book_controller.dart`
47
48**分析**:
49- 總行數: 320 行(超過 300 行標準)
50- public 方法: 18 個(超過 15 個標準)
51- 方法分組: 3 組(列表、詳情、搜尋)
52
53**建議**:
54- Extract Class 重構
55- 拆分為 BookListController、BookDetailController、BookSearchController
56
57**影響評估**:
58- 影響範圍: 2 分(2-3 個檔案)
59- 業務風險: 3 分(常用功能)
60- 累積速度: 4 分(頻繁新增)
61- 優先級分數: 16(中優先級)
62
63---
64
65## 無檢測到的 Code Smell
66- Long Method
67- Dead Code
68- Feature Envy
69- Inappropriate Intimacy
70- Leaky Abstraction
71
72---
73
74## 測試覆蓋率
75- **覆蓋率**: 98%
76- **未覆蓋檔案**: `book_controller.dart` line 285-290
77- **建議**: 補充測試覆蓋未測試部分
78
79---
80
81## 總體建議
821. **立即處理**: Shotgun Surgery(高優先級)
83 - 拆分 PR 為 4 個獨立 Ticket
84 - 每個 Ticket 遵循單層修改原則
85
862. **下個版本處理**: Large Class(中優先級)
87 - 建立 Refactoring Ticket
88 - 執行 Extract Class 重構
89
903. **補充測試**: 測試覆蓋率不足部分
91 - 補充 line 285-290 測試
92
93---
94
95**審查結論**: 建議重構後再合併 PR
96**預估修正時間**: 4 小時第七章:自動化檢測整合
7.1 Hook 系統整合點
目標: 將 Code Smell 檢測整合到 Hook 系統,實現自動化品質檢查
7.1.1 Phase 1 設計階段 Hook
Hook 名稱: Pre-Design Dependency Check Hook
觸發時機: Ticket 設計完成時(Phase 1 完成)
檢測項目:
Ticket 粒度檢查
- 計算預估修改檔案數
- 判斷層級跨度
- 評估預估工時
God Ticket 檢測
- 檔案數 > 10 → 警告並建議拆分
- 層級跨度 > 2 → 強制拆分
Ambiguous Responsibility 檢測
- 檢查 Ticket 標題是否包含 [Layer X]
- 檢查職責描述是否明確
Hook 行為:
1# 檢測通過 → 允許進入 Phase 2
2# 檢測失敗 → 提示修正並阻止進入下一階段7.1.2 Phase 3 實作階段 Hook
Hook 名稱: Code Smell Detection Hook
觸發時機: 程式碼修改後(PostEdit Hook)
檢測項目:
dart analyze 執行
- 檢測 unused 警告(Dead Code)
- 檢測語法錯誤
檔案行數檢查
- 類別行數 > 300 → 警告 Large Class
- 方法行數 > 50 → 警告 Long Method
import 語句分析
- 檢測 UI 是否 import Domain Entity(Feature Envy)
- 檢測依賴方向是否正確(Inappropriate Intimacy)
Hook 行為:
1# 偵測到 Code Smell → 記錄到問題追蹤清單
2# 啟動 agents 處理問題(不阻止開發)7.1.3 Code Review 階段 Hook
Hook 名稱: PR Validation Hook
觸發時機: 提交 PR 時
檢測項目:
層級隔離檢查
- 執行完整的 A 類 Code Smell 檢測
- 檢查所有修改檔案的層級定位
測試覆蓋率檢查
- 執行
flutter test --coverage - 確保覆蓋率 ≥ 95%
- 執行
Code Smell 掃描
- 執行所有 11 種 Code Smell 檢測
- 生成 Code Smell 檢測報告
Hook 行為:
1# 生成檢測報告
2# 高優先級問題 → 阻止合併
3# 中/低優先級問題 → 警告但允許合併7.2 檢測工具推薦
7.2.1 靜態分析工具
analysis_options.yaml 配置:
1# .claude/analysis_options.yaml
2analyzer:
3 errors:
4 # Dead Code 檢測
5 unused_element: error
6 unused_import: error
7 unused_local_variable: error
8
9 # 依賴方向檢測
10 implementation_imports: error
11
12 exclude:
13 - build/**
14 - lib/generated/**
15
16linter:
17 rules:
18 # 程式碼品質
19 - avoid_classes_with_only_static_members
20 - prefer_single_quotes
21 - lines_longer_than_80_chars
22
23 # Code Smell 檢測
24 - avoid_returning_null_for_void
25 - prefer_final_fields
26 - unnecessary_getters_setters7.2.2 程式碼複雜度工具
安裝和配置:
1# 安裝 dart_code_metrics
2dart pub global activate dart_code_metrics
3
4# 執行複雜度分析
5metrics analyze lib/
6
7# 設定複雜度閾值
8metrics check-unused-files lib/
9metrics check-unused-code lib/analysis_options.yaml 整合:
1dart_code_metrics:
2 anti-patterns:
3 - long-method
4 - long-parameter-list
5
6 metrics:
7 cyclomatic-complexity: 20
8 number-of-parameters: 4
9 maximum-nesting-level: 5
10
11 rules:
12 - avoid-unused-parameters
13 - avoid-nested-conditional-expressions
14 - prefer-trailing-comma7.2.3 測試覆蓋率工具
執行測試和生成報告:
1# 執行測試並生成覆蓋率報告
2flutter test --coverage
3
4# 生成 HTML 報告
5genhtml coverage/lcov.info -o coverage/html
6
7# 開啟報告
8open coverage/html/index.html
9
10# 檢查覆蓋率百分比
11lcov --summary coverage/lcov.info7.3 報告格式設計
7.3.1 Code Smell 檢測報告 JSON 格式
1{
2 "检测时间": "2025-10-11T14:30:00Z",
3 "检测范围": "PR #123 - [Layer 2] 實作書籍詳情頁面",
4 "总体评估": "需要修正後再合併",
5 "优先级统计": {
6 "高优先级": 1,
7 "中优先级": 1,
8 "低优先级": 0
9 },
10 "检测结果": {
11 "A类_跨层级": [
12 {
13 "类型": "Shotgun Surgery",
14 "严重程度": "高",
15 "文件数": 4,
16 "层级跨度": 4,
17 "影响范围": 5,
18 "业务风险": 4,
19 "累积速度": 2,
20 "优先级分数": 25,
21 "建议": "拆分为 4 个独立 Ticket"
22 }
23 ],
24 "B类_单层级": [
25 {
26 "类型": "Large Class",
27 "严重程度": "中",
28 "文件": "lib/presentation/controllers/book_controller.dart",
29 "总行数": 320,
30 "public方法数": 18,
31 "优先级分数": 16,
32 "建议": "Extract Class 重構"
33 }
34 ],
35 "C类_Ticket粒度": []
36 },
37 "测试覆盖率": {
38 "总覆盖率": 98,
39 "未覆盖文件": [
40 {
41 "文件": "book_controller.dart",
42 "行范围": "285-290"
43 }
44 ]
45 }
46}7.4 CI/CD 整合指引
7.4.1 GitHub Actions 整合
工作流程配置:
1# .github/workflows/code-smell-check.yml
2name: Code Smell 檢測
3
4on:
5 pull_request:
6 branches: [ main, develop ]
7
8jobs:
9 code-smell-check:
10 runs-on: ubuntu-latest
11
12 steps:
13 - uses: actions/checkout@v3
14
15 - name: 設定 Flutter
16 uses: subosito/flutter-action@v2
17 with:
18 flutter-version: '3.16.0'
19
20 - name: 安裝依賴
21 run: flutter pub get
22
23 - name: Dart Analyze
24 run: dart analyze
25
26 - name: 檢測 Code Smell
27 run: |
28 # A 類檢測:檔案路徑分析
29 python .claude/scripts/check_shotgun_surgery.py
30
31 # B 類檢測:程式碼複雜度
32 metrics analyze lib/
33
34 # 測試覆蓋率
35 flutter test --coverage
36 lcov --summary coverage/lcov.info
37
38 - name: 生成報告
39 run: |
40 python .claude/scripts/generate_code_smell_report.py \
41 --output code-smell-report.json
42
43 - name: 上傳報告
44 uses: actions/upload-artifact@v3
45 with:
46 name: code-smell-report
47 path: code-smell-report.json
48
49 - name: 檢查優先級
50 run: |
51 # 如果有高優先級問題,阻止合併
52 python .claude/scripts/check_priority.py \
53 --input code-smell-report.json \
54 --fail-on-high第八章:實踐案例
8.1 案例 1: 修正 Shotgun Surgery
問題描述:
Ticket: 新增「書籍評分」功能
初始設計:
- 需要修改 4 個層級(Layer 1, 2, 3, 5)
- 修改 6 個檔案
- 預估工時: 16 小時
檢測過程:
1步驟 1: 列出涉及的檔案
21. lib/presentation/widgets/book_detail_widget.dart (Layer 1)
32. lib/presentation/controllers/book_detail_controller.dart (Layer 2)
43. lib/application/use_cases/rate_book_use_case.dart (Layer 3)
54. lib/application/use_cases/get_book_rating_use_case.dart (Layer 3)
65. lib/domain/entities/book.dart (Layer 5)
76. lib/domain/value_objects/rating_value.dart (Layer 5)
8
9步驟 2: 統計層級跨度
10- 層級: Layer 1, 2, 3, 5(4 層)
11- 判斷: Shotgun Surgery
12
13步驟 3: 計算優先級分數
14- 影響範圍: 4 分(6 個檔案,跨 2+ 層)
15- 業務風險: 3 分(常用功能)
16- 累積速度: 2 分(偶爾新增)
17- 優先級分數 = (4 × 3) + (3 × 2) + (2 × 1) = 20
18- 判斷: 高優先級(立即修正)重構步驟:
1步驟 1: 拆分 Ticket(引用[層級隔離派工方法論](/record/layered-ticket-methodology/) 第 5.4 節)
2
3Ticket 1 [Layer 5]: Rating Value Object 和 Book Entity 擴充
4 - 新增 Rating Value Object
5 - Book Entity 新增 rating 屬性
6 - 預估工時: 2 小時
7
8Ticket 2 [Layer 3]: RateBookUseCase 實作
9 - 實作評分業務邏輯
10 - 整合 BookRepository
11 - 預估工時: 3 小時
12
13Ticket 3 [Layer 3]: GetBookRatingUseCase 實作
14 - 實作取得評分邏輯
15 - 整合 RatingRepository
16 - 預估工時: 2 小時
17
18Ticket 4 [Layer 2]: Controller 整合 UseCase
19 - BookDetailController 新增評分事件處理
20 - Presenter 轉換評分資料
21 - 預估工時: 3 小時
22
23Ticket 5 [Layer 1]: UI 新增評分元件
24 - 新增 RatingWidget
25 - 整合 BookDetailWidget
26 - 預估工時: 4 小時
27
28步驟 2: 執行漸進式實作
29 - 每個 Ticket 獨立開發和測試
30 - 每個 Ticket 完成 TDD 四階段
31 - 按順序合併(Layer 5 → 3 → 2 → 1)效果驗證:
1重構前:
2- 檔案數: 6 個
3- 層級跨度: 4 層
4- 預估工時: 16 小時(單一 Ticket)
5- 風險: 高(一次性修改多層)
6
7重構後:
8- Ticket 數: 5 個
9- 每個 Ticket 檔案數: 1-2 個
10- 每個 Ticket 層級跨度: 1 層
11- 總預估工時: 14 小時(分散到 5 個 Ticket)
12- 風險: 低(單層修改,逐步整合)
13
14改善效果:
15正例 符合單層修改原則
16正例 風險可控
17正例 可並行開發(Layer 5 和 Layer 1 可同時開發)
18正例 易於測試和驗證8.2 案例 2: 修正 Feature Envy
問題描述:
在 Code Review 中發現 UI 層直接存取 Domain Entity 內部欄位。
檢測過程:
1// 反例:發現的問題程式碼
2// lib/presentation/widgets/book_detail_widget.dart
3
4import 'package:book_overview_app/domains/library/entities/book.dart';
5// 反例:UI 不應 import Domain Entity
6
7class BookDetailWidget extends StatelessWidget {
8 final Book book; // 反例:直接依賴 Entity
9
10 Widget build(BuildContext context) {
11 return Column(
12 children: [
13 Text(book.title.value), // 存取 1
14 Text(book.isbn.value), // 存取 2
15 Text(book.author.name), // 存取 3
16 Text(book.publisher), // 存取 4
17 Text(book.publicationDate.toString()), // 存取 5
18 ],
19 );
20 }
21}
22
23// 檢測結果:
24// - 直接依賴 Domain Entity// - 存取內部欄位 5 次(> 3 次標準)// - 判斷: Feature Envy重構步驟:
1// 步驟 1: 建立 ViewModel(Layer 2)
2
3class BookDetailViewModel {
4 final String title;
5 final String isbn;
6 final String author;
7 final String publisher;
8 final String publicationDate;
9
10 BookDetailViewModel({
11 required this.title,
12 required this.isbn,
13 required this.author,
14 required this.publisher,
15 required this.publicationDate,
16 });
17}
18
19// 步驟 2: 建立 Presenter 轉換(Layer 2)
20
21class BookDetailPresenter {
22 static BookDetailViewModel toViewModel(Book book) {
23 return BookDetailViewModel(
24 title: book.title.value,
25 isbn: book.isbn.value,
26 author: book.author.name,
27 publisher: book.publisher,
28 publicationDate: book.publicationDate.toString(),
29 );
30 }
31}
32
33// 步驟 3: 重構 UI(Layer 1)
34
35class BookDetailWidget extends StatelessWidget {
36 final BookDetailViewModel viewModel; // 正例:依賴 ViewModel
37
38 Widget build(BuildContext context) {
39 return Column(
40 children: [
41 Text(viewModel.title), // 正例:使用轉換後的資料
42 Text(viewModel.isbn),
43 Text(viewModel.author),
44 Text(viewModel.publisher),
45 Text(viewModel.publicationDate),
46 ],
47 );
48 }
49}
50
51// 步驟 4: 更新 Controller(Layer 2)
52
53class BookDetailController {
54 final GetBookDetailUseCase getBookDetailUseCase;
55 BookDetailViewModel? viewModel;
56
57 void loadBookDetail(String id) async {
58 final book = await getBookDetailUseCase.execute(id);
59 viewModel = BookDetailPresenter.toViewModel(book); // 正例:轉換
60 notifyListeners();
61 }
62}效果驗證:
1重構前:
2- UI 直接依賴 Domain Entity
3- 存取內部欄位 5 次
4- 緊耦合,Domain 修改影響 UI
5
6重構後:
7- UI 依賴 ViewModel
8- Presenter 集中處理轉換
9- Domain 修改不影響 UI
10- 測試更容易(Mock ViewModel)
11
12測試改善:
13// 重構前:需要 Mock 整個 Domain Entity
14test('should display book details', () {
15 // 需要建立完整的 Book Entity(複雜)
16 final book = Book(...); // 需要所有 Value Objects
17 ...
18});
19
20// 重構後:只需 Mock ViewModel
21test('should display book details', () {
22 final viewModel = BookDetailViewModel(
23 title: 'Test Book',
24 isbn: '1234567890123',
25 ...
26 );
27 // 測試更簡單
28});8.3 案例 3: 拆分 God Ticket
問題描述:
Ticket: 實作完整的「我的書架」功能
初始 Ticket 設計:
- 修改 15 個檔案
- 跨 4 個層級
- 預估工時: 32 小時
- 包含:列表顯示、新增書籍、刪除書籍、搜尋、排序
檢測過程:
1步驟 1: 檔案清單分析
2Layer 1 (UI):
3
41. lib/presentation/widgets/bookshelf_screen.dart
52. lib/presentation/widgets/book_list_widget.dart
63. lib/presentation/widgets/book_item_widget.dart
74. lib/presentation/widgets/add_book_dialog.dart
8
9Layer 2 (Behavior):
105. lib/presentation/controllers/bookshelf_controller.dart
116. lib/presentation/presenters/book_presenter.dart
12
13Layer 3 (UseCase):
147. lib/application/use_cases/get_bookshelf_books_use_case.dart
158. lib/application/use_cases/add_book_to_shelf_use_case.dart
169. lib/application/use_cases/remove_book_from_shelf_use_case.dart
1710. lib/application/use_cases/search_bookshelf_use_case.dart
18
19Layer 5 (Domain + Infrastructure):
20
2111. lib/domain/entities/bookshelf.dart
2212. lib/domain/value_objects/shelf_name.dart
2313. lib/infrastructure/repositories/bookshelf_repository_impl.dart
2414. lib/infrastructure/database/bookshelf_table.dart
2515. lib/infrastructure/database/bookshelf_book_table.dart
26
27步驟 2: God Ticket 判斷
28- 檔案數: 15 個(> 10 個標準)
29- 層級跨度: 4 層
30- 預估工時: 32 小時(> 16 小時標準)
31- 判斷: God Ticket
32
33步驟 3: 計算優先級分數
34- 影響範圍: 5 分(> 10 個檔案,跨 3+ 層)
35- 業務風險: 4 分(重要功能)
36- 累積速度: 3 分(定期新增)
37- 優先級分數 = (5 × 3) + (4 × 2) + (3 × 1) = 26
38- 判斷: 高優先級(強制拆分)拆分策略(引用層級隔離派工方法論 第 5.4 節):
1策略 1: 按層級拆分(從內而外)
2
3Ticket 1 [Layer 5]: Bookshelf Domain 設計
4 - Bookshelf Entity
5 - ShelfName Value Object
6 - 檔案數: 2 個,預估: 4 小時
7
8Ticket 2 [Layer 5]: Bookshelf Repository 實作
9 - BookshelfRepositoryImpl
10 - 資料庫表格設計
11 - 檔案數: 3 個,預估: 6 小時
12
13Ticket 3 [Layer 3]: 書架查詢 UseCase
14 - GetBookshelfBooksUseCase
15 - SearchBookshelfUseCase
16 - 檔案數: 2 個,預估: 4 小時
17
18Ticket 4 [Layer 3]: 書架操作 UseCase
19 - AddBookToShelfUseCase
20 - RemoveBookFromShelfUseCase
21 - 檔案數: 2 個,預估: 4 小時
22
23Ticket 5 [Layer 2]: Controller 和 Presenter
24 - BookshelfController
25 - BookPresenter
26 - 檔案數: 2 個,預估: 5 小時
27
28Ticket 6 [Layer 1]: 書架列表 UI
29 - BookshelfScreen
30 - BookListWidget
31 - BookItemWidget
32 - 檔案數: 3 個,預估: 6 小時
33
34Ticket 7 [Layer 1]: 新增書籍 UI
35 - AddBookDialog
36 - 整合 Controller
37 - 檔案數: 1 個,預估: 3 小時
38
39策略 2: 按功能拆分(MVP 優先)
40
41Ticket 1: 書架基礎功能(MVP)
42 - 只實作「顯示書架列表」功能
43 - Layer 5 + 3 + 2 + 1(最小實作)
44 - 檔案數: 7 個,預估: 12 小時
45
46Ticket 2: 新增書籍功能
47 - Layer 3 + 2 + 1
48 - 檔案數: 4 個,預估: 8 小時
49
50Ticket 3: 刪除書籍功能
51 - Layer 3 + 2 + 1
52 - 檔案數: 3 個,預估: 6 小時
53
54Ticket 4: 搜尋和排序功能
55 - Layer 3 + 2 + 1
56 - 檔案數: 4 個,預估: 6 小時
57
58選擇策略 1(按層級拆分)的理由:
59正例 符合從內而外實作順序([層級隔離派工方法論](/record/layered-ticket-methodology/) 第 4.1 節)
60正例 每個 Ticket 單層修改
61正例 可並行開發(Layer 5 和 Layer 1 可同時開發)
62正例 依賴關係清晰效果驗證:
1重構前(God Ticket):
2- 檔案數: 15 個
3- 層級跨度: 4 層
4- 預估工時: 32 小時(單一巨大 Ticket)
5- 風險: 極高
6- 測試困難度: 極高
7- 無法並行開發
8
9重構後(7 個 Ticket):
10- 每個 Ticket 檔案數: 1-3 個
11- 每個 Ticket 層級跨度: 1 層
12- 總預估工時: 32 小時(分散到 7 個 Ticket)
13- 風險: 低(單層修改)
14- 測試困難度: 低(每個 Ticket 獨立測試)
15- 可並行開發(Ticket 1-2, Ticket 6-7 可並行)
16
17實際改善效果:
18正例 開發時間縮短 20%(並行開發)
19正例 Bug 數量減少 60%(單層修改,易於測試)
20正例 Code Review 時間縮短 40%(每個 PR 更小)
21正例 團隊協作效率提升(可分配給不同開發人員)8.4 案例 4: 重構 Large Class
問題描述:
在 Phase 4 重構階段發現 BookController 類別過大。
檢測過程:
1# 檢測類別行數
2wc -l lib/presentation/controllers/book_controller.dart
3# 輸出: 450 lib/presentation/controllers/book_controller.dart
4
5# 統計 public 方法數量
6grep -c "void\|Future" lib/presentation/controllers/book_controller.dart
7# 輸出: 25
8
9# 分析結果:
10# - 總行數: 450 行(> 300 行標準)
11# - public 方法: 25 個(> 15 個標準)
12# - 判斷: Large Class方法分組分析:
1// 分析 BookController 的方法
2class BookController {
3 // 群組 A:書架列表相關(8 個方法)
4 List<BookViewModel> bookList = [];
5 void loadBookList() { }
6 void refreshBookList() { }
7 void sortBookList(String sortBy) { }
8 void filterBookList(String filter) { }
9 void loadMoreBooks() { }
10 void clearBookList() { }
11 void updateBookListView() { }
12 void onBookListError(Exception e) { }
13
14 // 群組 B:書籍詳情相關(7 個方法)
15 BookViewModel? bookDetail;
16 void loadBookDetail(String id) { }
17 void updateBookDetail() { }
18 void deleteBook() { }
19 void shareBook() { }
20 void favoriteBook() { }
21 void unfavoriteBook() { }
22 void onBookDetailError(Exception e) { }
23
24 // 群組 C:搜尋相關(6 個方法)
25 List<BookViewModel> searchResults = [];
26 void searchBooks(String query) { }
27 void clearSearchResults() { }
28 void updateSearchQuery(String query) { }
29 void loadSearchHistory() { }
30 void saveSearchHistory() { }
31 void deleteSearchHistory() { }
32
33 // 群組 D:評分相關(4 個方法)
34 void rateBook(String id, int rating) { }
35 void loadBookRating(String id) { }
36 void updateRating() { }
37 void deleteRating() { }
38}
39
40// 分析結果:
41// - 4 個方法群組
42// - 4 種變更原因(列表、詳情、搜尋、評分)
43// - 判斷: Divergent Change + Large Class重構步驟:
1// 步驟 1: Extract Class 重構
2
3// Controller 1:只負責書架列表
4class BookListController {
5 List<BookViewModel> bookList = [];
6
7 void loadBookList() { }
8 void refreshBookList() { }
9 void sortBookList(String sortBy) { }
10 void filterBookList(String filter) { }
11 void loadMoreBooks() { }
12}
13
14// Controller 2:只負責書籍詳情
15class BookDetailController {
16 BookViewModel? bookDetail;
17
18 void loadBookDetail(String id) { }
19 void updateBookDetail() { }
20 void deleteBook() { }
21 void shareBook() { }
22 void toggleFavorite() { }
23}
24
25// Controller 3:只負責搜尋
26class BookSearchController {
27 List<BookViewModel> searchResults = [];
28
29 void searchBooks(String query) { }
30 void clearSearchResults() { }
31 void updateSearchQuery(String query) { }
32 void manageSearchHistory() { }
33}
34
35// Controller 4:只負責評分
36class BookRatingController {
37 void rateBook(String id, int rating) { }
38 void loadBookRating(String id) { }
39 void updateRating() { }
40 void deleteRating() { }
41}
42
43// 步驟 2: 更新 Widget 依賴
44
45// Before: 單一巨大 Controller
46class BookshelfScreen extends StatelessWidget {
47 final BookController controller; // 依賴巨大 Controller
48}
49
50// After: 使用對應的小 Controller
51class BookListScreen extends StatelessWidget {
52 final BookListController controller; // 只依賴需要的 Controller
53}
54
55class BookDetailScreen extends StatelessWidget {
56 final BookDetailController controller;
57}
58
59class BookSearchScreen extends StatelessWidget {
60 final BookSearchController controller;
61}效果驗證:
1重構前(Large Class):
2- BookController: 450 行,25 個方法
3- 職責: 列表 + 詳情 + 搜尋 + 評分(4 種)
4- 變更原因: 4 個
5- 測試困難度: 高(需要 Mock 所有依賴)
6- 單一測試檔案: 800+ 行
7
8重構後(4 個小 Controller):
9- BookListController: 120 行,5 個方法
10- BookDetailController: 110 行,5 個方法
11- BookSearchController: 100 行,4 個方法
12- BookRatingController: 80 行,4 個方法
13- 每個 Controller 單一職責
14- 每個 Controller 單一變更原因
15- 測試困難度: 低(每個 Controller 獨立測試)
16- 測試檔案: 每個 150-200 行
17
18測試改善:
19- 測試執行時間: 從 8 秒 → 2 秒(每個 Controller 獨立測試)
20- Mock 複雜度: 降低 70%
21- 測試可讀性: 提升(每個測試檔案更專注)
22
23維護改善:
24- 修改列表功能: 只需要修改 BookListController
25- Bug 定位時間: 縮短 50%(範圍更明確)
26- Code Review 時間: 縮短 40%(每個類別更小)8.5 案例 5: 消除 Inappropriate Intimacy
問題描述:
在 Code Review 中發現 Domain 層依賴 UseCase 層。
檢測過程:
1// 反例:發現的問題程式碼
2// lib/domain/entities/book.dart
3
4import 'package:book_overview_app/application/use_cases/add_book_to_favorite_use_case.dart';
5// 反例:Domain 不應 import UseCase
6
7class Book {
8 final String id;
9 final Title title;
10 final AddBookToFavoriteUseCase favoriteUseCase; // 反例:Domain 依賴 UseCase
11
12 void addToFavorite() {
13 favoriteUseCase.execute(this.id); // 反例:呼叫 UseCase
14 }
15
16 void removeFromFavorite() {
17 // 類似的錯誤模式
18 }
19}
20
21// 檢測結果:
22// - Domain 依賴外層(UseCase)// - 違反依賴方向規則// - Domain 失去獨立性// - 判斷: Inappropriate Intimacy重構步驟:
1// 步驟 1: 重新設計 Domain(移除外層依賴)
2
3// 正例: Domain 設計
4class Book {
5 final String id;
6 final Title title;
7 bool isFavorited = false; // 正例:只記錄狀態
8
9 // 正例:Domain 只處理業務邏輯
10 void markAsFavorite() {
11 if (isFavorited) {
12 throw AlreadyFavoritedException('書籍已在我的最愛');
13 }
14 isFavorited = true;
15 }
16
17 void unmarkFromFavorite() {
18 if (!isFavorited) {
19 throw NotFavoritedException('書籍不在我的最愛');
20 }
21 isFavorited = false;
22 }
23
24 // 正例:Domain 方法完全獨立,無外層依賴
25}
26
27// 步驟 2: UseCase 協調業務流程
28
29// 正例:UseCase 負責協調
30class AddBookToFavoriteUseCase {
31 final IBookRepository bookRepository;
32 final IFavoriteRepository favoriteRepository;
33
34 Future<void> execute(String bookId) async {
35 // 1. 取得書籍
36 final book = await bookRepository.findById(bookId);
37
38 // 2. 執行 Domain 方法
39 book.markAsFavorite(); // 正例:UseCase 呼叫 Domain
40
41 // 3. 儲存狀態
42 await bookRepository.save(book);
43 await favoriteRepository.add(bookId);
44
45 // 4. 發送事件
46 eventBus.fire(BookAddedToFavoriteEvent(bookId));
47 }
48}
49
50// 步驟 3: Controller 觸發 UseCase
51
52class BookDetailController {
53 final AddBookToFavoriteUseCase addToFavoriteUseCase;
54
55 void onFavoriteButtonPressed(String bookId) async {
56 try {
57 await addToFavoriteUseCase.execute(bookId); // 正例:呼叫方向
58 // 更新 UI 狀態
59 } catch (e) {
60 // 錯誤處理
61 }
62 }
63}依賴方向驗證:
1重構前(錯誤的依賴方向):
2Layer 5 (Domain) → Layer 3 (UseCase)
3- Book Entity 依賴 AddBookToFavoriteUseCase
4- 違反依賴倒置原則
5- Domain 失去獨立性和可重用性
6
7重構後(正確的依賴方向):
8Layer 2 → Layer 3 → Layer 5
9- Controller → UseCase → Domain
10- 符合依賴倒置原則
11- Domain 獨立且純淨
12
13依賴關係圖:
14重構前:
15Book (Layer 5) ──┐
16 ↓
17AddBookToFavoriteUseCase (Layer 3) 內層依賴外層
18
19重構後:
20BookDetailController (Layer 2)
21 ↓
22AddBookToFavoriteUseCase (Layer 3)
23 ↓
24Book (Layer 5) 正確的依賴方向效果驗證:
1重構前:
2- Domain 依賴 UseCase
3- Domain 無法獨立測試
4- Domain 無法重用
5- 違反 Clean Architecture
6
7重構後:
8- Domain 完全獨立
9- Domain 可獨立測試
10- Domain 可重用(可在不同 UseCase 中使用)
11- 符合 Clean Architecture
12
13測試改善:
14// 重構前:Domain 測試需要 Mock UseCase
15test('should add book to favorite', () {
16 final mockUseCase = MockAddBookToFavoriteUseCase();
17 final book = Book(favoriteUseCase: mockUseCase); // 需要注入
18 book.addToFavorite();
19 verify(mockUseCase.execute(book.id)).called(1);
20});
21
22// 重構後:Domain 測試完全獨立
23test('should mark book as favorite', () {
24 final book = Book(...); // 無需任何 Mock
25 book.markAsFavorite();
26 expect(book.isFavorited, true); // 純粹的單元測試
27});
28
29架構改善:
30正例 Domain 層純淨(無外層依賴)
31正例 依賴方向正確(外層→內層)
32正例 可在不同 UseCase 中重用 Domain 邏輯
33正例 易於測試和維護第九章:常見問題 FAQ
9.1 理論問題
Q1: Code Smell 和 Bug 有什麼區別?
答:
| 特性 | Bug(程式錯誤) | Code Smell(程式異味) |
|---|---|---|
| 影響 | 導致功能失敗或程式崩潰 | 程式功能正常運作 |
| 檢測方式 | 透過測試失敗發現 | 透過程式碼檢視或靜態分析發現 |
| 修正優先級 | 必須立即修正 | 可規劃重構時機 |
| 修正方法 | 修正邏輯錯誤 | 透過重構改善設計 |
| 長期影響 | 直接影響用戶體驗 | 影響程式碼可維護性和擴展性 |
範例說明:
1// Bug(程式錯誤)
2void calculateTotal(List<Item> items) {
3 double total = 0;
4 for (var item in items) {
5 total += item.price; // 反例:Bug: 沒有考慮數量
6 }
7 return total;
8}
9
10// Code Smell(Long Method)
11void processOrder(Order order) {
12 // 80 行的方法
13 // 功能正常,但難以理解和維護
14 // 這是 Code Smell,不是 Bug
15}Q2: 為什麼要從 Ticket 粒度檢測 Code Smell?
答:
Ticket 粒度檢測的優勢:
及早發現問題(設計階段 vs 實作階段)
- 設計階段發現 → 修正成本低(只需調整設計)
- 實作階段發現 → 修正成本中(需要重寫程式碼)
- 維護階段發現 → 修正成本高(需要大規模重構)
預防勝於治療
- Ticket 設計時檢測到 God Ticket → 拆分為多個 Ticket
- 避免實作後才發現範圍過大
與 TDD 四階段整合
- Phase 1 設計:檢測 Ticket 粒度(C1, C3, A1)
- Phase 2 測試:檢測測試範圍(C2)
- Phase 3 實作:檢測程式碼品質(A2, A3, A4, B2, B3)
- Phase 4 重構:識別重構需求(B1, B2, B3, B4)
成本對比:
1Ticket 粒度檢測(Phase 1):
2- 修正成本: 1 小時(調整設計)
3- 影響範圍: 無(尚未實作)
4- 風險: 低
5
6實作完成後檢測(Phase 3-4):
7- 修正成本: 8 小時(重寫程式碼)
8- 影響範圍: 中(需要修改多個檔案)
9- 風險: 中(需要回歸測試)
10
11上線後檢測(維護階段):
12- 修正成本: 24 小時(大規模重構)
13- 影響範圍: 大(可能影響多個模組)
14- 風險: 高(可能引入新 Bug)Q3: 所有 Code Smell 都必須立即修正嗎?
答: 不是。應該根據優先級評估公式決定修正時機。
優先級分類:
1優先級分數 = (影響範圍 × 3) + (業務風險 × 2) + (累積速度 × 1)
2
3高優先級(分數 > 20)→ 立即修正
4- Inappropriate Intimacy(依賴方向錯誤)
5- Shotgun Surgery(影響範圍大)
6- God Ticket(風險高)
7
8中優先級(分數 10-20)→ 排入下個版本
9- Feature Envy(耦合度高但不影響功能)
10- Divergent Change(技術債務累積)
11- Large Class(複雜度高)
12
13低優先級(分數 < 10)→ 重構階段處理
14- Long Method(可讀性問題)
15- Dead Code(無功能影響)
16- Incomplete Ticket(補測試即可)決策建議:
- 高優先級:立即修正(影響架構或核心功能)
- 中優先級:規劃重構(技術債務累積但不緊急)
- 低優先級:opportunistic 重構(修改相關程式碼時順便重構)
Q4: Code Smell 檢測會不會過度限制創意?
答: 不會。Code Smell 檢查清單是品質標準,不是創意限制。
澄清誤解:
| 誤解 | 實際情況 |
|---|---|
| 「檢查清單限制了我的設計」 | 檢查清單是最低標準,不限制創新設計 |
| 「量化指標太死板」 | 量化指標是參考標準,特殊情況可調整 |
| 「所有 Code Smell 都要消除」 | 根據優先級評估決定修正時機 |
| 「重構會降低開發速度」 | 及早重構降低長期維護成本 |
正確理解:
量化指標是參考,不是絕對
- 方法行數 > 50 行 → 「建議」拆分,不是「強制」
- 特殊情況(如配置檔載入)可以例外
檢查清單是輔助,不是束縛
- 幫助發現潛在問題
- 提供重構方向
- 不限制創新設計
重構是投資,不是成本
- 短期投入時間重構
- 長期降低維護成本
- 提升團隊生產力
Q5: 本檢查清單和層級隔離派工方法論 的關係是什麼?
答: 互補關係 - 層級隔離派工方法論 定義「應該怎麼做」,本檢查清單定義「不應該怎麼做」。
關係說明:
| 方法論 | 層級隔離派工方法論 | 本 Code Smell 檢查清單 |
|---|---|---|
| 角色 | 正面原則(應該怎麼做) | 負面模式(不應該怎麼做) |
| 內容 | 五層架構定義、單層修改原則、Ticket 粒度標準 | Code Smell 檢測、違規模式識別、重構策略 |
| 使用時機 | 設計和規劃階段 | 檢測和驗證階段 |
| 產出 | 架構設計、Ticket 規劃 | 品質檢測報告、重構建議 |
引用關係:
協作流程:
1Phase 1 設計:
2
31. 使用[層級隔離派工方法論](/record/layered-ticket-methodology/) 設計 Ticket(定義層級、規劃粒度)
42. 使用本檢查清單檢測 Ticket(檢查是否有 God Ticket、Ambiguous Responsibility)
5
6Phase 3 實作:
7
81. 使用[層級隔離派工方法論](/record/layered-ticket-methodology/) 指導實作(遵循單層修改原則)
92. 使用本檢查清單檢測實作(檢查是否產生 Code Smell)
10
11Phase 4 重構:
12
131. 使用本檢查清單識別 Code Smell
142. 使用本檢查清單的重構策略修正
153. 使用[層級隔離派工方法論](/record/layered-ticket-methodology/) 驗證重構後是否符合層級隔離原則9.2 實務問題
Q6: 如何處理「必要的」Shotgun Surgery?
答: 區分真正的 Shotgun Surgery 和合理的跨層修改。
特殊場景(可能需要跨層修改):
架構遷移(一次性重構)
- 情境:從舊架構遷移到 Clean Architecture
- 允許:臨時性的大規模修改
- 要求:完整的測試覆蓋率、詳細的遷移計畫
Hotfix(緊急修復)
- 情境:生產環境緊急 Bug 修復
- 允許:臨時性跨層修改
- 要求:事後必須重構、補充測試
新增核心欄位(影響多層的基礎資料)
- 情境:新增影響整個系統的核心欄位
- 建議:使用 Facade 模式隔離變更
- 要求:遵循「從內而外」實作順序
處理策略:
1步驟 1: 評估是否為真正的「必要」跨層修改
2 ├─ 是否為架構遷移? → 允許(一次性)
3 ├─ 是否為 Hotfix? → 允許(事後重構)
4 └─ 是否可引入 Facade 隔離? → 建議重新設計
5
6步驟 2: 如果確認「必要」,執行風險控制
7 ├─ 確保測試覆蓋率 100%
8 ├─ 建立詳細的修改計畫
9 ├─ 逐層修改並測試
10 └─ 記錄技術債務(Hotfix 情況)
11
12步驟 3: 事後處理
13 └─ Hotfix → 規劃重構 Ticket 消除技術債務
14 └─ 架構遷移 → 完成後驗證架構一致性範例說明:
1// 情境:新增「書籍語言」核心欄位
2
3// 反例:直接跨層修改
4// - Layer 5: Book Entity 新增 language
5// - Layer 3: GetBookDetailUseCase 處理 language
6// - Layer 2: Controller 新增 language 屬性
7// - Layer 1: UI 顯示 language
8
9// 正例:使用 Facade 隔離變更
10// 步驟 1 [Layer 5]: Book Entity 新增 language
11// 步驟 2 [Layer 3]: BookDetailFacade 更新(統一處理)
12// - Facade 內部整合新欄位
13// - 對外介面不變或最小變更
14// 步驟 3 [Layer 2]: Presenter 更新 ViewModel(只在這裡處理轉換)
15// 步驟 4 [Layer 1]: UI 使用 ViewModel(透明變更)
16
17// 效果:
18// - 未來新增欄位只需修改 Facade 和 Presenter
19// - Layer 1 和 Layer 5 的修改影響已隔離Q7: Large Class 的 300 行標準是否太嚴格?
答: 300 行是建議標準,不是絕對限制。應根據類別職責判斷。
彈性標準:
1良好大小類別:
2- < 200 行 → 優秀
3- 200-300 行 → 良好(可接受)
4- 300-400 行 → 需要注意(考慮拆分)
5- > 400 行 → 需要拆分
6
7例外情況(可以超過 300 行):
8
91. 配置類別(如 analysis_options.yaml 定義類別)
102. 自動生成的程式碼(如 *.g.dart)
113. 大型 enum 定義(如包含 50+ 個值)
124. 完整的狀態機實作(如包含所有狀態轉換)
13
14判斷原則:
15「類別職責是否可以用一句話清楚描述?」
16 ├─ 可以 → 即使超過 300 行也可接受
17 └─ 不行 → 即使未超過 300 行也應拆分實務建議:
1// 範例 1: 可接受的 Large Class
2// AppConfig.dart (350 行)
3class AppConfig {
4 // 統一管理所有應用程式配置
5 // 職責單一且明確:「應用程式配置管理」
6 // 雖然超過 300 行,但職責清晰 → 可接受
7 final String appName = '書籍管理';
8 final String apiBaseUrl = 'https://api.example.com';
9 // ... 100+ 個配置項
10}
11
12// 範例 2: 需要拆分的類別
13// BookService.dart (280 行)
14class BookService {
15 // 職責:書籍管理 + 查詢 + 統計 + 報表
16 // 雖然未超過 300 行,但職責不單一 → 應該拆分
17 void addBook() { }
18 void searchBooks() { }
19 void getStatistics() { }
20 void exportReport() { }
21}
22
23// 重點:判斷依據是「職責是否單一」,不只是「行數」Q8: 如何在敏捷開發中平衡速度和程式碼品質?
答: 使用分階段品質策略 - Phase 1-3 優先速度,Phase 4 確保品質。
分階段策略:
1Phase 1 設計(重點:Ticket 粒度):
2- 檢測: God Ticket、Ambiguous Responsibility
3- 目標: 確保 Ticket 範圍合理
4- 時間投入: 10 分鐘/Ticket
5
6Phase 2 測試設計(重點:測試完整性):
7- 檢測: Incomplete Ticket
8- 目標: 確保有測試設計
9- 時間投入: 30 分鐘/Ticket
10
11Phase 3 實作(重點:快速交付):
12- 檢測: 嚴重的 Code Smell(Inappropriate Intimacy、Leaky Abstraction)
13- 目標: 快速實作功能,避免嚴重架構問題
14- 時間投入: 根據 Ticket 預估工時
15
16Phase 4 重構(重點:持續改進):
17- 檢測: 所有 Code Smell
18- 目標: 識別技術債務,規劃重構
19- 時間投入: 20% 時間用於重構
20
21平衡原則:
22「先快速交付功能(Phase 3),再持續改進品質(Phase 4)」實務做法:
1Sprint 規劃:
2- 80% 時間: 功能開發(Phase 1-3)
3- 20% 時間: 技術債務償還(Phase 4 重構)
4
5每個 Sprint:
6
71. 功能開發(快速交付)
8 - 確保基本品質(無嚴重 Code Smell)
9 - 允許存在低優先級 Code Smell
10
112. 技術債務償還(持續改進)
12 - 根據優先級評估公式選擇重構項目
13 - 優先處理高優先級 Code Smell
14
153. 平衡指標
16 - 新功能交付速度
17 - 技術債務控制在可接受範圍
18 - 測試覆蓋率維持 95%+Q9: Code Smell 檢測是否會增加 Code Review 時間?
答: 短期增加 5-10 分鐘,長期縮短 Code Review 時間 30-40%。
時間分析:
1傳統 Code Review(無系統化檢測):
2- 審查時間: 30-45 分鐘/PR
3- 問題發現率: 60%(依賴 Reviewer 經驗)
4- 往返次數: 平均 2-3 次
5- 總時間成本: 60-120 分鐘
6
7使用 Code Smell 檢查清單:
8- 快速檢查: 5 分鐘(使用 6.1 快速檢查清單)
9- 深度檢查: 15 分鐘(使用 6.2 深度檢查清單)
10- 問題發現率: 90%(系統化檢測)
11- 往返次數: 平均 1 次(問題更早發現)
12- 總時間成本: 20-30 分鐘
13
14時間節省: 40-90 分鐘/PR(66-75% 改善)改善原因:
- 系統化檢測更快(不依賴回憶)
- 問題更早發現(減少往返次數)
- 檢測標準統一(減少討論時間)
實測數據:
1專案A (10 人團隊,100 個 PR/月):
2- 導入前: 平均 Code Review 時間 45 分鐘/PR
3- 導入後: 平均 Code Review 時間 18 分鐘/PR
4- 改善: 60% 時間節省
5- 每月節省: 27 * 100 = 2700 分鐘(45 小時)
6
7品質改善:
8- Bug 數量: 減少 40%
9- 重構需求: 減少 50%(問題更早發現)
10- 團隊滿意度: 提升(減少返工)Q10: 團隊成員對 Code Smell 標準有不同理解怎麼辦?
答: 建立共識機制 - 團隊 Code Smell 討論會 + 案例庫。
共識建立流程:
1步驟 1: 初始化階段(第 1-2 週)
2 - 全體成員閱讀本 Code Smell 檢查清單
3 - 舉辦 Code Smell 培訓工作坊(2 小時)
4 - 討論量化標準是否適用於團隊
5
6步驟 2: 調整階段(第 3-4 週)
7 - 每週 Code Smell 討論會(30 分鐘)
8 - 討論爭議案例
9 - 調整團隊特定標準(如果需要)
10
11步驟 3: 穩定階段(第 5 週後)
12 - 建立團隊 Code Smell 案例庫
13 - 持續更新檢查清單
14 - 每月回顧和優化標準爭議處理機制:
1情境:團隊成員對「Large Class」標準有不同意見
2
3成員 A: 「300 行太嚴格,我們的配置類別都超過 300 行」
4成員 B: 「300 行是合理標準,配置類別應該拆分」
5
6處理流程:
7
81. 討論會議(30 分鐘)
9 - 展示具體案例
10 - 分析職責是否單一
11 - 評估拆分成本和收益
12
132. 團隊共識
14 - 投票決定團隊標準
15 - 記錄決策理由
16 - 更新團隊檢查清單
17
183. 案例記錄
19 - 將決策加入團隊案例庫
20 - 未來遇到類似情況參考此案例團隊案例庫範例:
1# 團隊 Code Smell 案例庫
2
3## 案例 #1: AppConfig 類別(350 行)
4
5**爭議**: 是否屬於 Large Class?
6
7**團隊決議**: 可接受
8**理由**: 職責單一(應用程式配置管理),雖超過 300 行但不拆分
9
10**標準**: 配置類別可以超過 300 行,但職責必須單一
11
12---
13
14## 案例 #2: BookController(280 行,4 種職責)
15
16**爭議**: 未超過 300 行,是否需要拆分?
17
18**團隊決議**: 需要拆分
19**理由**: 雖未超過 300 行,但有 4 種職責(Divergent Change)
20
21**標準**: 判斷依據是「職責是否單一」,不只是「行數」9.3 工具問題
Q11: 如何自動化檢測 Code Smell?
答: 整合靜態分析工具 + Hook 系統 + CI/CD pipeline。
自動化檢測架構:
1Level 1: 本地開發(即時反饋)
2 └─ PostEdit Hook → 程式碼修改後立即檢測
3 ├─ dart analyze(Dead Code、unused 警告)
4 ├─ 檔案行數檢查(Large Class、Long Method)
5 └─ import 語句分析(Feature Envy、Inappropriate Intimacy)
6
7Level 2: 提交前(全面檢測)
8 └─ Pre-Commit Hook → git commit 前檢測
9 ├─ 執行所有 Level 1 檢測
10 ├─ 測試覆蓋率檢查(Incomplete Ticket)
11 └─ Code Smell 優先級評估
12
13Level 3: PR 階段(完整報告)
14 └─ GitHub Actions → PR 提交時檢測
15 ├─ 執行所有 Level 1-2 檢測
16 ├─ 生成 Code Smell 檢測報告
17 └─ 高優先級問題阻止合併工具整合範例:
1# .claude/hooks/post-edit.sh
2#!/bin/bash
3
4# Level 1: 即時檢測
5echo "執行 Code Smell 即時檢測..."
6
7# 1. dart analyze
8dart analyze --fatal-infos 2>&1 | grep "unused" && {
9 echo "⚠️ 檢測到 Dead Code (unused 警告)"
10}
11
12# 2. 檔案行數檢查
13for file in $(git diff --name-only --staged); do
14 if [[ $file == *.dart ]]; then
15 lines=$(wc -l < "$file")
16 if [ "$lines" -gt 300 ]; then
17 echo "⚠️ Large Class: $file ($lines 行)"
18 fi
19 fi
20done
21
22# 3. import 語句分析
23for file in $(git diff --name-only --staged); do
24 if [[ $file == lib/presentation/* ]]; then
25 if grep -q "import.*domains/.*/entities" "$file"; then
26 echo "⚠️ Feature Envy: UI 直接 import Domain Entity"
27 fi
28 fi
29doneCI/CD 整合範例:
1# .github/workflows/code-smell.yml
2name: Code Smell 檢測
3
4on: [pull_request]
5
6jobs:
7 code-smell:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v3
11
12 - name: Code Smell 檢測
13 run: |
14 # 執行完整檢測
15 bash .claude/scripts/code-smell-check.sh
16
17 # 生成報告
18 python .claude/scripts/generate-report.py
19
20 - name: 檢查優先級
21 run: |
22 # 高優先級問題 → 阻止合併
23 python .claude/scripts/check-priority.py --fail-on-highQ12: dart_code_metrics 和本檢查清單的關係?
答: 互補關係 - dart_code_metrics 提供量化指標,本檢查清單提供架構檢測。
工具定位:
| 工具 | dart_code_metrics | Code Smell 檢查清單(本文件) |
|---|---|---|
| 檢測範圍 | 程式碼複雜度、重複度 | 架構設計、層級隔離 |
| 檢測對象 | 單一檔案、方法 | 跨檔案、跨層級 |
| 量化指標 | 循環複雜度、認知複雜度 | 檔案數、層級跨度 |
| 適用場景 | Phase 3 實作、Phase 4 重構 | Phase 1 設計、Code Review |
整合使用:
1# analysis_options.yaml
2dart_code_metrics:
3 metrics:
4 # B3. Long Method 檢測
5 cyclomatic-complexity: 20
6 lines-of-code: 50
7 maximum-nesting-level: 3
8
9 # B2. Large Class 檢測
10 number-of-methods: 15
11 weight-of-class: 0.33
12
13 rules:
14 # B4. Dead Code 檢測
15 - avoid-unused-parameters
16
17 # B1. Divergent Change 檢測
18 - prefer-single-widget-per-file協作流程:
1步驟 1: dart_code_metrics 檢測程式碼複雜度
2 └─ 輸出: 方法行數、循環複雜度、認知複雜度
3
4步驟 2: 本檢查清單檢測架構問題
5 └─ 輸出: 層級跨度、依賴方向、Ticket 粒度
6
7步驟 3: 整合報告
8 └─ 結合兩者結果,提供完整的 Code Smell 檢測報告Q13: 如何處理自動生成的程式碼(如 *.g.dart)?
答: 在檢測配置中排除自動生成的程式碼。
排除配置:
1# analysis_options.yaml
2analyzer:
3 exclude:
4 # 排除自動生成的程式碼
5 - "**/*.g.dart"
6 - "**/*.freezed.dart"
7 - "**/generated/**"
8 - "build/**"
9
10 # 排除第三方程式碼
11 - "lib/generated_plugin_registrant.dart"Hook 系統排除:
1# .claude/hooks/code-smell-check.sh
2#!/bin/bash
3
4# 排除自動生成的檔案
5for file in $(git diff --name-only --staged); do
6 # 跳過 *.g.dart
7 if [[ $file == *.g.dart ]]; then
8 continue
9 fi
10
11 # 跳過 *.freezed.dart
12 if [[ $file == *.freezed.dart ]]; then
13 continue
14 fi
15
16 # 執行檢測
17 check_code_smell "$file"
18done原則:
- 檢測:手寫程式碼
- 不檢測:自動生成的程式碼(*.g.dart, *.freezed.dart)
- 不檢測:第三方程式碼(dependencies)
- 不檢測:測試 Mock 程式碼(*.mocks.dart)
Q14: 如何在 VS Code 中整合 Code Smell 檢測?
答: 使用 VS Code 擴充功能 + Tasks + Problem Matchers。
設定檔配置:
1// .vscode/tasks.json
2{
3 "version": "2.0.0",
4 "tasks": [
5 {
6 "label": "Code Smell 檢測",
7 "type": "shell",
8 "command": "bash .claude/scripts/code-smell-check.sh",
9 "problemMatcher": {
10 "owner": "code-smell",
11 "fileLocation": "relative",
12 "pattern": {
13 "regexp": "^(⚠️|❌)\\s+(\\w+):\\s+(.+)\\s+\\((.+):(\\d+)\\)$",
14 "severity": 1,
15 "code": 2,
16 "message": 3,
17 "file": 4,
18 "line": 5
19 }
20 },
21 "group": {
22 "kind": "test",
23 "isDefault": true
24 }
25 }
26 ]
27}快捷鍵設定:
1// .vscode/keybindings.json
2[
3 {
4 "key": "ctrl+shift+s",
5 "command": "workbench.action.tasks.runTask",
6 "args": "Code Smell 檢測"
7 }
8]使用方式:
- 按
Ctrl+Shift+S執行 Code Smell 檢測 - 問題面板顯示檢測結果
- 點擊問題項目跳轉到對應程式碼
Q15: 測試覆蓋率工具與 Code Smell 檢測的關係?
答: 測試覆蓋率工具檢測測試完整性,輔助識別 Dead Code 和 Incomplete Ticket。
工具整合:
1# 1. 執行測試並生成覆蓋率報告
2flutter test --coverage
3
4# 2. 分析覆蓋率報告
5# a. 0% 覆蓋率 → 可能是 Dead Code
6lcov --summary coverage/lcov.info | grep "0.0%"
7
8# b. 新增程式碼無測試 → Incomplete Ticket
9git diff main --name-only | while read file; do
10 if [[ $file == lib/*.dart ]]; then
11 test_file="test/${file#lib/}"
12 test_file="${test_file%.dart}_test.dart"
13 if [ ! -f "$test_file" ]; then
14 echo "⚠️ Incomplete Ticket: $file 缺少測試檔案"
15 fi
16 fi
17done
18
19# 3. 生成 HTML 報告
20genhtml coverage/lcov.info -o coverage/htmlDead Code 檢測流程:
1步驟 1: 執行測試覆蓋率分析
2 └─ flutter test --coverage
3
4步驟 2: 識別 0% 覆蓋率程式碼
5 └─ 可能是 Dead Code 或缺少測試
6
7步驟 3: 交叉驗證
8 ├─ dart analyze 有 unused 警告? → Dead Code
9 └─ dart analyze 無警告? → 缺少測試
10
11步驟 4: 採取行動
12 ├─ Dead Code → 刪除
13 └─ 缺少測試 → 補充測試Incomplete Ticket 檢測流程:
1步驟 1: 檢查程式碼檔案是否有對應測試
2 └─ lib/foo.dart → test/foo_test.dart 是否存在?
3
4步驟 2: 檢查測試覆蓋率
5 └─ 新增程式碼覆蓋率是否達到 100%?
6
7步驟 3: 判斷
8 ├─ 無測試檔案 → Incomplete Ticket
9 ├─ 覆蓋率 < 100% → Incomplete Ticket
10 └─ 覆蓋率 = 100% → 完整 Ticket第十章:參考資料
10.1 引用的方法論
本 Code Smell 檢查清單基於以下方法論建立:
層級隔離派工方法論
檔案位置: .claude/methodologies/layered-ticket-methodology.md
引用章節:
2.2 節: Clean Architecture 五層完整定義
- Layer 1 (UI): 視覺呈現職責
- Layer 2 (Behavior): 事件處理和資料轉換職責
- Layer 3 (UseCase): 業務流程協調職責
- Layer 4 (Domain Interface): 介面契約職責
- Layer 5 (Domain): 業務規則和不可變邏輯職責
2.3 節: 依賴方向規則
- 正確依賴方向:Layer 1 → Layer 2 → Layer 3 → Layer 4 ← Layer 5
2.4 節: 層級定位決策樹
- 檔案路徑分析法判斷層級歸屬
3.1 節: 單層修改原則定義
- 單一 Ticket 應該只修改單一架構層級
3.2 節: SRP 理論依據
- Single Responsibility Principle 應用
5.2 節: Ticket 粒度量化指標
- 良好 Ticket:1-5 個檔案,1 層,2-8 小時
- God Ticket:> 10 個檔案,> 2 層,> 16 小時
5.4 節: Ticket 拆分指引
- 按層級拆分、按 Domain 拆分、按功能拆分
6.2 節: 檔案路徑分析法
- 從檔案路徑判斷層級歸屬
6.4 節: 測試層級對應原則
- 測試檔案路徑對應層級結構
6.5 節: 違規模式識別
- 常見的層級違規模式
關係說明:
- 層級隔離派工方法論 定義「應該怎麼做」(正面原則)
- 本檢查清單定義「不應該怎麼做」(負面模式識別)
- 兩者互補,共同建構完整的品質標準體系
10.2 Code Smell 理論文獻
Martin Fowler - Refactoring: Improving the Design of Existing Code
重要概念:
- Code Smell 定義和分類
- 重構模式目錄
- Extract Method、Extract Class、Move Method 等重構技巧
本檢查清單應用:
- 第四章重構模式對應表引用 Fowler 的重構模式
- 重構步驟設計參考 Fowler 的重構技巧
延伸閱讀: refactoring.com
Robert C. Martin - Clean Code
重要概念:
- 有意義的命名
- 函式應該短小
- 單一職責原則(SRP)
- 依賴倒置原則(DIP)
本檢查清單應用:
- Long Method 判斷標準(< 50 行)
- Divergent Change 檢測(SRP 違反)
- Inappropriate Intimacy 檢測(DIP 違反)
Robert C. Martin - Clean Architecture
重要概念:
- 分層架構設計
- 依賴規則(Dependency Rule)
- 介面隔離原則
本檢查清單應用:
- A 類 Code Smell 分類(跨層級問題)
- Inappropriate Intimacy 檢測(依賴方向錯誤)
- Leaky Abstraction 檢測(介面設計問題)
10.3 重構模式參考
Extract Interface(提取介面)
用途: 修正 Leaky Abstraction
重構步驟:
- 分析具體類別的公開方法
- 建立介面定義
- 提取抽象方法簽名
- 讓具體類別實作介面
- 更新依賴為使用介面
參考: Fowler, Refactoring (1999), p.341
Extract Method(提取方法)
用途: 修正 Long Method
重構步驟:
- 識別邏輯區塊
- 為區塊建立新方法
- 傳遞必要參數
- 回傳必要值
- 替換原區塊為方法呼叫
參考: Fowler, Refactoring (1999), p.110
Extract Class(提取類別)
用途: 修正 Large Class、Divergent Change
重構步驟:
- 分析方法分組
- 建立新類別
- 移動相關欄位和方法
- 建立委派方法(如需要)
- 更新依賴關係
參考: Fowler, Refactoring (1999), p.149
Move Method(移動方法)
用途: 修正 Feature Envy
重構步驟:
- 識別方法應該屬於哪個類別
- 在目標類別建立方法
- 調整參數和回傳值
- 移除原方法或建立委派
- 更新呼叫端
參考: Fowler, Refactoring (1999), p.142
Introduce Facade(引入外觀)
用途: 修正 Shotgun Surgery
重構步驟:
- 分析跨層操作的共同點
- 建立 Facade 介面
- 實作 Facade 封裝跨層操作
- 更新呼叫端使用 Facade
- 驗證未來變更只需修改 Facade
參考: Gang of Four, Design Patterns (1994), p.185