基本資訊

目的: 提供基於 Ticket 粒度的 Code Smell 檢測標準和檢查清單

與其他方法論的關係:

核心理念: 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. 提升程式碼品質: 改善可讀性、可測試性、可擴展性
  3. 預防未來問題: 在問題惡化前進行修正
  4. 團隊協作: 提供統一的品質標準和溝通語言

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 節)。

特徵識別:

  1. 一個小需求需要修改 UI、Behavior、UseCase、Domain 多層
  2. 層級間缺乏適當的抽象介面
  3. 變更影響範圍不可控
  4. 檔案修改數量 > 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 轉換。

特徵識別:

  1. 外層直接存取內層的內部狀態(如 book.isbn.value
  2. 缺乏適當的 DTO 或 ViewModel 轉換
  3. 跨層級的緊耦合
  4. UI 層直接 import Domain Entity
  5. 外層存取內層內部欄位次數 > 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(不當親密關係)

定義: 層級間過度耦合,內層知道外層的存在或依賴外層,違反依賴方向規則。

特徵識別:

  1. Domain 層依賴 UseCase 或 UI 層
  2. 依賴方向錯誤(內層依賴外層)
  3. 存在循環依賴
  4. 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(抽象滲漏)

定義: 內層的實作細節透過介面洩漏到外層,介面不夠抽象。

特徵識別:

  1. Repository 介面包含資料庫特定參數(如 SQL 語句)
  2. Domain Event 包含 UI 特定資料(如 Widget 狀態)
  3. 抽象介面不夠抽象,包含實作關鍵字
  4. 介面方法名稱洩漏實作細節

層級隔離派工方法論 的關聯:

  • 違反 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)。

特徵識別:

  1. 一個 Controller 同時負責多個頁面的邏輯
  2. 一個 UseCase 處理多個不相關的業務流程
  3. 變更原因不單一(有 2+ 個變更原因)
  4. 類別方法可以明確分組(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 SmellA2, A3, A4, B2, B3
Code ReviewPR 提交時最終驗證所有 Code Smell
Phase 4 重構階段重構評估時識別需要重構的 Code SmellB1, 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)
1415檢查程式碼品質(A2, A3, A4, B2, B3)
16  ├─ 通過 → Code Review
17  └─ 失敗 → 修正程式碼
18
19Code Review
2021全面檢查所有 Code Smell
22  ├─ 通過 → 合併 PR
23  └─ 失敗 → 重構
24
25Phase 4 重構評估
2627識別需要重構的 Code Smell(B1, B2, B3, B4)
28  ├─ 有需要 → 執行重構
29  └─ 無需要 → 完成

3.2 A 類 Code Smell 檢測方法(跨層級問題)

3.2.1 A1. Shotgun Surgery 檢測

檢測指標:

  1. 檔案數量指標: 單一 Ticket 修改的檔案數
  2. 層級跨度指標: Ticket 涉及的層級數量
  3. 依賴鏈長度指標: 從 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 檢測

檢測指標:

  1. 直接依賴指標: 外層是否直接依賴內層的具體類別
  2. 欄位存取指標: 外層存取內層的內部欄位次數
  3. 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 檢測

檢測指標:

  1. 依賴方向檢查: 內層是否依賴外層
  2. 循環依賴檢查: 是否存在雙向依賴
  3. 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 檢測

檢測指標:

  1. 介面純淨度: 介面是否包含實作細節
  2. 參數檢查: 方法參數是否洩漏實作資訊
  3. 回傳類型檢查: 是否回傳 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. 變更原因數量: 有幾種不同的原因需要修改此類別
  3. 方法分組檢查: 方法是否可以明確分組

判斷標準:

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 檢測

檢測指標:

  1. 程式碼行數: 類別總行數
  2. 方法數量: public 方法數量
  3. 屬性數量: 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 檢測

檢測指標:

  1. 方法行數: 方法內程式碼行數
  2. 巢狀層級: if/for/while 的巢狀深度
  3. 區塊數量: 方法內邏輯區塊數量(用註解分隔)

判斷標準:

 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 檢測

檢測方法:

  1. 使用 dart analyze 檢測 unused 警告
  2. 使用 code coverage 工具檢測 0% 覆蓋率程式碼
  3. 手動檢查註解掉的程式碼

自動化檢測:

 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 節):

  1. 檔案修改數量: 計算 git diff 涉及的檔案數
  2. 層級跨度: 涉及幾個架構層級
  3. 預估工時: 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 檢測

檢測指標:

  1. 測試檔案檢查: 是否有對應的測試檔案
  2. 驗收條件檢查: Ticket 描述是否包含驗收條件
  3. 工作日誌檢查: 是否完成 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 檢測

檢測指標:

  1. Ticket 標題格式: 是否包含層級標示
  2. 職責描述清晰度: 是否明確說明修改哪一層
  3. 驗收條件對應性: 驗收條件是否對應單一層級

判斷標準:

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 SurgeryTicket 設計層級跨度> 2 層3.1 單層修改原則
A2. Feature EnvyCode Review直接依賴 DomainUI 存取 Entity2.2 Layer 2 職責
A3. Inappropriate IntimacyCode Review依賴方向內層依賴外層2.3 依賴方向規則
A4. Leaky Abstraction介面設計介面純淨度包含實作關鍵字2.2 Layer 4 職責
B1. Divergent ChangePhase 4 重構方法分組數> 2 組3.2 SRP 理論
B2. Large ClassPhase 4 重構程式碼行數> 300 行5.2 量化指標
B3. Long MethodPhase 3 實作方法行數> 50 行5.2 量化指標
B4. Dead CodePhase 4 重構unused 警告dart analyze-
C1. God TicketTicket 設計檔案數> 10 個5.2 Ticket 粒度
C2. Incomplete TicketCode Review測試檔案缺少測試TDD 四階段
C3. Ambiguous ResponsibilityTicket 設計標題格式無層級標示5.3 Ticket 範例

第四章:重構建議和策略

4.1 重構模式對應表

每種 Code Smell 都有對應的標準重構模式(引用 Martin Fowler 的 Refactoring 書籍):

Code Smell重構模式重構策略預期效果
A1. Shotgun SurgeryExtract Interface + Introduce Facade引入抽象層隔離變更單層修改
A2. Feature EnvyMove Method + Extract ViewModel移動邏輯到正確層級職責對齊
A3. Inappropriate IntimacyIntroduce Parameter Object打破循環依賴依賴方向正確
A4. Leaky AbstractionExtract Interface重新設計抽象介面隱藏實作細節
B1. Divergent ChangeExtract Class拆分為多個單一職責類別符合 SRP
B2. Large ClassExtract Class + Move Method拆分大類別降低複雜度
B3. Long MethodExtract Method拆分長方法提升可讀性
B4. Dead CodeRemove Dead Code直接刪除程式碼簡潔
C1. God TicketSplit Ticket拆分為多個單層 Ticket降低風險
C2. Incomplete TicketAdd Missing Tests補充測試和文件完整性
C3. Ambiguous ResponsibilityClarify 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,不依賴 Entity

4.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. 影響範圍: 影響多少檔案和層級(1-5 分)
  2. 業務風險: 是否影響核心業務流程(1-5 分)
  3. 累積速度: 不修正會多快惡化(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 Intimacy45326
Shotgun Surgery54225
God Ticket53324
Feature Envy33315
Large Class23416
Long Method1238
Dead Code1114

4.4 重構風險控制策略

風險控制原則:

  1. 測試覆蓋率要求: 重構前必須確保測試覆蓋率 100%
  2. 漸進式重構: 每次只重構一個 Code Smell
  3. 回滾計畫: 準備 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 節)

  • 檔案路徑檢查: 所有修改檔案都屬於同一層級?

  • 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 完成)

檢測項目:

  1. Ticket 粒度檢查

    • 計算預估修改檔案數
    • 判斷層級跨度
    • 評估預估工時
  2. God Ticket 檢測

    • 檔案數 > 10 → 警告並建議拆分
    • 層級跨度 > 2 → 強制拆分
  3. Ambiguous Responsibility 檢測

    • 檢查 Ticket 標題是否包含 [Layer X]
    • 檢查職責描述是否明確

Hook 行為:

1# 檢測通過 → 允許進入 Phase 2
2# 檢測失敗 → 提示修正並阻止進入下一階段

7.1.2 Phase 3 實作階段 Hook

Hook 名稱: Code Smell Detection Hook

觸發時機: 程式碼修改後(PostEdit Hook)

檢測項目:

  1. dart analyze 執行

    • 檢測 unused 警告(Dead Code)
    • 檢測語法錯誤
  2. 檔案行數檢查

    • 類別行數 > 300 → 警告 Large Class
    • 方法行數 > 50 → 警告 Long Method
  3. 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 時

檢測項目:

  1. 層級隔離檢查

    • 執行完整的 A 類 Code Smell 檢測
    • 檢查所有修改檔案的層級定位
  2. 測試覆蓋率檢查

    • 執行 flutter test --coverage
    • 確保覆蓋率 ≥ 95%
  3. 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_setters

7.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-comma

7.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.info

7.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) ──┐
1617AddBookToFavoriteUseCase (Layer 3) 內層依賴外層
18
19重構後:
20BookDetailController (Layer 2)
2122AddBookToFavoriteUseCase (Layer 3)
2324Book (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 粒度檢測的優勢:

  1. 及早發現問題(設計階段 vs 實作階段)

    • 設計階段發現 → 修正成本低(只需調整設計)
    • 實作階段發現 → 修正成本中(需要重寫程式碼)
    • 維護階段發現 → 修正成本高(需要大規模重構)
  2. 預防勝於治療

    • Ticket 設計時檢測到 God Ticket → 拆分為多個 Ticket
    • 避免實作後才發現範圍過大
  3. 與 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 都要消除」根據優先級評估決定修正時機
「重構會降低開發速度」及早重構降低長期維護成本

正確理解:

  1. 量化指標是參考,不是絕對

    • 方法行數 > 50 行 → 「建議」拆分,不是「強制」
    • 特殊情況(如配置檔載入)可以例外
  2. 檢查清單是輔助,不是束縛

    • 幫助發現潛在問題
    • 提供重構方向
    • 不限制創新設計
  3. 重構是投資,不是成本

    • 短期投入時間重構
    • 長期降低維護成本
    • 提升團隊生產力

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合理的跨層修改

特殊場景(可能需要跨層修改):

  1. 架構遷移(一次性重構)

    • 情境:從舊架構遷移到 Clean Architecture
    • 允許:臨時性的大規模修改
    • 要求:完整的測試覆蓋率、詳細的遷移計畫
  2. Hotfix(緊急修復)

    • 情境:生產環境緊急 Bug 修復
    • 允許:臨時性跨層修改
    • 要求:事後必須重構、補充測試
  3. 新增核心欄位(影響多層的基礎資料)

    • 情境:新增影響整個系統的核心欄位
    • 建議:使用 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. 系統化檢測更快(不依賴回憶)
  2. 問題更早發現(減少往返次數)
  3. 檢測標準統一(減少討論時間)

實測數據:

 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
29done

CI/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-high

Q12: dart_code_metrics 和本檢查清單的關係?

: 互補關係 - dart_code_metrics 提供量化指標,本檢查清單提供架構檢測。

工具定位:

工具dart_code_metricsCode 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]

使用方式:

  1. Ctrl+Shift+S 執行 Code Smell 檢測
  2. 問題面板顯示檢測結果
  3. 點擊問題項目跳轉到對應程式碼

Q15: 測試覆蓋率工具與 Code Smell 檢測的關係?

: 測試覆蓋率工具檢測測試完整性,輔助識別 Dead CodeIncomplete 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/html

Dead 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

重構步驟:

  1. 分析具體類別的公開方法
  2. 建立介面定義
  3. 提取抽象方法簽名
  4. 讓具體類別實作介面
  5. 更新依賴為使用介面

參考: Fowler, Refactoring (1999), p.341


Extract Method(提取方法)

用途: 修正 Long Method

重構步驟:

  1. 識別邏輯區塊
  2. 為區塊建立新方法
  3. 傳遞必要參數
  4. 回傳必要值
  5. 替換原區塊為方法呼叫

參考: Fowler, Refactoring (1999), p.110


Extract Class(提取類別)

用途: 修正 Large Class、Divergent Change

重構步驟:

  1. 分析方法分組
  2. 建立新類別
  3. 移動相關欄位和方法
  4. 建立委派方法(如需要)
  5. 更新依賴關係

參考: Fowler, Refactoring (1999), p.149


Move Method(移動方法)

用途: 修正 Feature Envy

重構步驟:

  1. 識別方法應該屬於哪個類別
  2. 在目標類別建立方法
  3. 調整參數和回傳值
  4. 移除原方法或建立委派
  5. 更新呼叫端

參考: Fowler, Refactoring (1999), p.142


Introduce Facade(引入外觀)

用途: 修正 Shotgun Surgery

重構步驟:

  1. 分析跨層操作的共同點
  2. 建立 Facade 介面
  3. 實作 Facade 封裝跨層操作
  4. 更新呼叫端使用 Facade
  5. 驗證未來變更只需修改 Facade

參考: Gang of Four, Design Patterns (1994), p.185