輸出模板
各步驟輸出的標準模板。依脈絡調整,但這些是預設。
標準輸出檔案:spec/{feature}/{feature}.ooa.md
全域編碼:對外契約面(Domain Service / Calculator / Strategy 介面 / Domain Exception 等公開型別)是本階段「對外」項目,於詞彙表/類別圖該型別處掛全域碼
§{SHORT}-OA-{N}(N=per-spec 連續整數,無既有本地號故新編)。{SHORT}查 spec-codes.md。引用上游 FS 邊界條件時用其全域碼§{SHORT}-FS-{n};引用本地內部節點維持原樣。格式見 workflow §可追溯性「全域編碼」。
把步驟 1~3 的產物組裝成單一檔案,結構如下:
# {功能名稱} OOA
> 來源:[fp]({feature}.fp.md)
> 上游消費:{列出消費的上游型別+其全域碼,如 `Resource`/`WorkCapability`(`§DM-OA-4/8`);無則略}
> 下游:[pseudo]({feature}.pseudo.md)(尚未建立)
> 性質:{單一 feature OOA/跨 feature 共用 OOA(Published Language 的行為面);一句話講消費關係}
> 狀態:待簽核 / 已簽核
---
## 1. 領域詞彙(Ubiquitous Language)
(步驟 1 的詞彙表)
## 2. 關係草圖
(步驟 1 的純文字關係圖)
## 3. 類別圖
(步驟 2 的 Mermaid `classDiagram`,含必要的子圖)
## 4. SOLID 審查 + 設計模式
(步驟 3 的 SOLID 檢查與模式採用結論)
## 5. functional spec 對照
| Use Case / 邊界條件 | 對應物件職責 |
|---|---|
| UC A1 行為步驟 1~3 | `Scheduler.run()`(`§{SHORT}-OA-1`) |
| `§{SHORT}-FS-2` 數量 ≤ 0 | `Workorder.validateQuantity()` |
## 簽核
- **編輯者**:____ / 日期:____
- **Reviewer**:____ / 日期:____步驟 4(程式碼骨架)不寫進
spec/{feature}/{feature}.ooa.md,產出獨立 Java 檔。
連結慣例(GFM,寫檔時遵循)
完整規則見 product-module-development-workflow.md §可追溯性「連結慣例(GFM)」。本階段重點:
- breadcrumb / 跨檔:相對路徑 markdown 連結
[文字](../path.md)(本檔在spec/{feature}/(兩層深);同 feature 的 fp/pseudo 為同層 sibling、直接用檔名)。 - 同檔章節跳轉(如 §類別圖、§SOLID 審查):markdown 連結
[§類別圖](#3-類別圖)。 - functional spec 對照表裡回指 spec 的 UC / 邊界條件:可用 markdown 連結到 spec 檔的對應區段,如
[UC A1]({feature}.fp.md#use-cases);§Bn 在表內無法逐列定位,行內維持純文字即可。 - 標籤一律含非數字字元;純數字
#1不是合法 tag。
步驟 1:詞彙表模板
領域詞彙拆成兩張表:沿用既有(消費上游、不重定義,掛上游全域碼)與本模組新建模(本階段對外型別掛 §{SHORT}-OA-{N}、純內部 VO 維持本地號)。領域術語對齊 vault 根 glossary.md 的標準形;遇 glossary 沒有的新領域術語,提醒使用者登記。
## 1. 領域詞彙(Ubiquitous Language)
### 沿用既有(消費、不重定義)
| 詞彙 | 英文 | 出處 | 在本模組的角色 |
|---|---|---|---|
| 工序節點 | `VSMNode` | `§DM-OA-3` | 篩選輸入 key |
| 資源 | `Resource` | `§DM-OA-4` | 候選與輸出主體 |
### 本模組新建模
| 詞彙 | 英文 | 全域碼 | 說明 | 對應 FS |
|---|---|---|---|---|
| 資源篩選器 | `ResourceSelector` | `§{SHORT}-OA-1` | Domain Service 門面 | 整份 |
| 篩選 key | `ResourceSelectionKey`(VO) | —(內部) | 純內部導航 VO,維持本地號、不掛全域碼 | §{SHORT}-FS-6 |純內部導航的 VO 全域碼欄填
—(內部),維持本地號(兩層並存,見 spec-codes.md)。
接著用純文字呈現關係草圖:
WorkCenter
▲
│ belongs to
│
Resource ──has 1──> Schedule
│
├── has N ──> Capability
│
└── located at ──> Location
最後,攤開 2-3 個設計分歧,讓使用者表態。
步驟 2:Mermaid 類別圖模板
classDiagram direction TB %% ========== Core Abstraction ========== class Resource { <<abstract>> -resourceId: ResourceId -name: String -schedule: Schedule +isOnDuty(dateTime) boolean +hasCapability(process) boolean } class Machine { -machineType: String -automationLevel: AutomationLevel } Resource <|-- Machine %% ========== Enumerations ========== class AutomationLevel { <<enumeration>> MANUAL SEMI_AUTO FULL_AUTO } Machine --> AutomationLevel %% ========== Schedule ========== class Schedule { -baselineShifts: List~RecurringShift~ -exceptions: List~CalendarException~ +getWorkingSlots(date) List~TimeSlot~ +addException(exception) void } Resource "1" *-- "1" Schedule %% ========== Specifications ========== class ResourceRequirement { <<interface>> +isSatisfiedBy(resource) boolean +and(other) ResourceRequirement }
關鍵慣例:
- 一律
direction TB,除非有理由用LR - 用
%% ==========註解把相關類別分組 - 在關係上標出多重性(
"1"、"*"、"1..*") - 泛型用
~Type~(因為<>在某些情境會讓 Mermaid 混淆) - 靜態方法在某些 Mermaid 渲染器中加
$後綴:+of(start, end)$ TimeSlot
步驟 3:SOLID 審查模板
## SOLID Audit
### ✅ 設計穩固之處
- **SRP**:`Schedule` 只處理時間;`Capability` 只描述能力。
- **OCP**:`CalendarException` 子類別允許新增例外型別而不修改 `Schedule`。
- **DIP**:`MatchingRule` 依賴 `ResourceRepository` 介面,而非具體資料儲存。
### ⚠️ 潛在風險
1. **`Resource.hasCapability(Process)` 的 ISP 風險**
- 對 Worker 而言,「capability」語意上很怪——工人是以技能在思考。
- **修法選項 A**:改名成更抽象的詞
- **修法選項 B**:只為 Machine/Mold 萃取 `ProcessExecutor` 介面
2. **`Resource.isAvailable()` 的 SRP 風險**
- 可能被解讀為「是否在班」或「目前是否未被排程」——這是不同的關注點。
- **建議**:改名為 `isOnDuty()`,並把佔用追蹤留在排程模組。
## 建議的設計模式
| # | 模式 | 用於 | 必要性 |
|---|---------|----------|-----------|
| 1 | Specification | 可組合的 `ResourceRequirement` | 🔥 強烈推薦 |
| 2 | Repository | `ResourceRepository` 介面 | ✅ DDD 標準 |
| 3 | Template Method | `CalendarException.applyTo()` | ✅ 自然契合 |
| 4 | Composite | 操作關係(machine + mold + worker) | ⭐ 僅在需要時 |
### 要採用哪些?
[在此使用 ask_user_input_v0]步驟 4:Java 程式碼骨架模板
純 POJO 風格(無框架、無 Lombok、無 record)
package mydomain.calendar;
import java.time.*;
import java.util.List;
// 班次:星期幾的某段工作時間(含休息)
public class RecurringShift {
// 欄位:
// String shiftId
// DayOfWeek dayOfWeek
// LocalTime startTime
// Duration duration
// List<BreakPeriod> breaks
// ShiftType shiftType
// 是否跨日(startTime + duration 超過 24:00)
public boolean isOvernight() {
return startTime.toSecondOfDay() + duration.getSeconds() > 86400L;
}
// 在指定日期上展開:去除休息時段後的 TimeSlot 列表
public List<TimeSlot> materialize(LocalDate date) {
// 1. 起始時段
// 2. 扣除每個休息時段
// 3. 回傳
}
}Specification 模式(帶 default method 的介面)
public interface ResourceRequirement {
boolean isSatisfiedBy(Resource resource);
default ResourceRequirement and(ResourceRequirement other) {
return new AndRequirement(this, other);
}
default ResourceRequirement or(ResourceRequirement other) {
return new OrRequirement(this, other);
}
default ResourceRequirement not() {
return new NotRequirement(this);
}
}
public class AndRequirement implements ResourceRequirement {
private final ResourceRequirement left;
private final ResourceRequirement right;
public AndRequirement(ResourceRequirement left, ResourceRequirement right) {
this.left = Objects.requireNonNull(left);
this.right = Objects.requireNonNull(right);
}
@Override
public boolean isSatisfiedBy(Resource resource) {
return left.isSatisfiedBy(resource) && right.isSatisfiedBy(resource);
}
}帶前置條件檢查的 Aggregate Root
public class Schedule {
// 欄位:
// List<RecurringShift> baselineShifts
// List<CalendarException> exceptions
public void addException(CalendarException exception) {
for (CalendarException existing : exceptions) {
if (existing.conflictsWith(exception)) {
throw new CalendarConflictException(
"新增的例外 " + exception.getExceptionId()
+ " 與既有例外 " + existing.getExceptionId() + " 衝突");
}
}
exceptions.add(exception);
}
}帶 factory method 的不可變 Value Object
public final class TimeSlot {
private final LocalDateTime start;
private final LocalDateTime end;
public static TimeSlot of(LocalDateTime start, LocalDateTime end) {
if (!start.isBefore(end)) {
throw new IllegalArgumentException("時段起點必須早於終點");
}
return new TimeSlot(start, end);
}
private TimeSlot(LocalDateTime start, LocalDateTime end) {
this.start = start;
this.end = end;
}
public LocalDateTime start() { return start; }
public LocalDateTime end() { return end; }
public Duration duration() { return Duration.between(start, end); }
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TimeSlot other)) return false;
return start.equals(other.start) && end.equals(other.end);
}
@Override public int hashCode() { return Objects.hash(start, end); }
}JUnit 5 測試模板
package mydomain.calendar;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class RecurringShiftMaterializeTest {
@Test
void overnightShiftWithBreakShouldSplitCorrectly() {
// 情境:週五 20:00 起 7 小時(跨到週六 03:00),中間 23:00 休 30 分
// 期望:兩段 TimeSlot
BreakPeriod brk = new BreakPeriod(
LocalTime.of(23, 0), Duration.ofMinutes(30), BreakType.REST);
RecurringShift shift = new RecurringShift(
"FRI-NIGHT", DayOfWeek.FRIDAY,
LocalTime.of(20, 0), Duration.ofHours(7),
List.of(brk), ShiftType.NIGHT);
assertTrue(shift.isOvernight());
List<TimeSlot> slots = shift.materialize(LocalDate.of(2026, 5, 22));
assertEquals(2, slots.size());
assertTrue(slots.get(1).spansMidnight());
}
}Markdown 文件模板(步驟 4 的替代選項)
若使用者想要 markdown 文件,而非(或同時要)程式碼,組織如下:
project-docs/
├── CLAUDE.md 主入口 + 開發指引
├── README.md 模組總覽
├── docs/
│ ├── 01-domain-vocabulary.md
│ ├── 02-class-diagram.md
│ ├── 03-design-decisions.md
│ └── 04-package-structure.md
└── specs/
└── <subdomain>.md 每個子領域一份實作 spec
CLAUDE.md 模板
# CLAUDE.md — [Module Name]
> 給 Claude Code 的開發指引。請先讀完本檔,再依任務需求閱讀對應的 spec。
## 專案定位
[這個模組是什麼、做什麼、不做什麼]
## 技術棧
- Java [version]
- [framework or "no framework"]
- [other dependencies]
## 開發原則
1. [風格規則,例如 POJO、不要 getter/setter]
2. [命名慣例,例如程式碼英文、註解中文]
3. [依賴隔離規則]
4. [測試慣例]
## 開發順序建議
[建議的實作順序,逐 package 列]
## 文件導覽
[「我想做 X」→「讀 Y」的對照表]子 package spec 模板
# Spec: [package.name]
> [一行描述]
## 套件: `package.name`
依賴:
- [列出依賴]
---
## ClassName
**性質**:[Entity / Value Object / Aggregate Root / Service]
**欄位**
- `Type fieldName` — 說明
**方法**
- `method(param) ReturnType` — 行為說明
**規則**
- [不變式與約束]
**範例**(如有需要)
[程式碼範例]
---
## 測試重點
### `TestClassName`
[帶註解的測試方法骨架]何時產生檔案 vs. 行內輸出
| 情況 | 輸出方式 |
|---|---|
| 使用者要求行內審查/分析 | 在對話中行內輸出 markdown |
| 使用者說「整理成 markdown」/「我要檔案」/「幫我做文件」 | 用 create_file 產生 .md 檔 |
| 使用者要可編譯的程式碼 | 產生 .java 檔 |
| 只是在討論設計 | 行內輸出(別過早產生檔案) |
產生檔案時,使用上方所示的目錄結構,並在最後呼叫 present_files 把它們分享給使用者。