# 程式碼規範與開發標準
## 概述
建立統一的程式碼撰寫規範和開發流程標準,確保團隊協作效率和代碼品質。
## 通用開發原則
### 代碼品質原則
- [ ] **可讀性優先**: 代碼應該容易閱讀和理解
- [ ] **一致性**: 遵循統一的命名和格式規範
- [ ] **簡潔性**: 避免過度複雜的解決方案
- [ ] **可測試性**: 代碼結構便於單元測試
- [ ] **可維護性**: 考慮未來修改和擴展的便利性
### SOLID原則遵循
- [ ] **單一職責**: 每個函數/類只負責一個明確的功能
- [ ] **開放封閉**: 對擴展開放,對修改封閉
- [ ] **里氏替換**: 子類應該能夠替換父類
- [ ] **介面隔離**: 不應該依賴不需要的介面
- [ ] **依賴倒置**: 依賴抽象而非具體實現
## C# (.NET Core) 規範
### 基本格式規則
#### EditorConfig 配置
```ini
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = crlf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
[*.{json,js,ts,tsx,css,scss,yml,yaml}]
indent_size = 2
```
#### .NET 分析器規則
```xml
net8.0
enable
true
CS1591
true
all
runtime; build; native; contentfiles; analyzers
```
### 命名規範
#### C# 命名慣例
```csharp
// ✅ 類別和方法使用PascalCase
public class UserService
{
public async Task GetUserProfileAsync(Guid userId)
{
// 方法實現
}
public decimal CalculateMonthlyInterestRate(decimal principal, decimal rate)
{
return principal * rate / 12;
}
}
// ✅ 變數和參數使用camelCase
private readonly IUserRepository _userRepository;
private const int MaxRetryAttempts = 3;
public async Task ValidateUserAsync(string email, string password)
{
var isValidEmail = IsValidEmailFormat(email);
var hashedPassword = HashPassword(password);
return isValidEmail && await _userRepository.ValidateCredentialsAsync(email, hashedPassword);
}
// ❌ 避免的命名
private string data; // 太泛化
private int u; // 太簡短
private async Task GetUserProfileDataAsync() {} // 冗餘的Data後綴
```
#### 常數和列舉
```typescript
// ✅ 常數使用SCREAMING_SNAKE_CASE
const API_ENDPOINTS = {
USER_PROFILE: '/api/v1/users/profile',
DIALOGUE_START: '/api/v1/dialogues/start',
} as const;
const MAX_DIALOGUE_DURATION_MINUTES = 30;
const DEFAULT_PAGINATION_LIMIT = 20;
// ✅ 列舉使用PascalCase
enum DialogueStatus {
InProgress = 'in_progress',
Completed = 'completed',
Abandoned = 'abandoned',
}
enum UserSubscriptionPlan {
Free = 'free',
Basic = 'basic',
Premium = 'premium',
Professional = 'professional',
}
```
#### 類型定義
```typescript
// ✅ 介面使用PascalCase,以I開頭(可選)
interface UserProfile {
userId: string;
username: string;
email: string;
createdAt: Date;
subscription: UserSubscriptionPlan;
}
interface ApiResponse {
success: boolean;
data: T | null;
message?: string;
error?: ApiError;
}
// ✅ 類型別名使用PascalCase
type DialogueAnalysis = {
grammarScore: number;
semanticScore: number;
fluencyScore: number;
overallScore: number;
feedback: string[];
};
type CreateDialogueRequest = {
scenarioId: string;
difficultyOverride?: string;
targetVocabulary?: string[];
};
```
### 函數撰寫規範
#### 函數設計原則
```typescript
// ✅ 函數應該小巧、單一職責
const validateEmailFormat = (email: string): boolean => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const calculateDialogueScore = (
grammarScore: number,
semanticScore: number,
fluencyScore: number
): number => {
const weights = { grammar: 0.3, semantic: 0.4, fluency: 0.3 };
return Math.round(
grammarScore * weights.grammar +
semanticScore * weights.semantic +
fluencyScore * weights.fluency
);
};
// ✅ 使用純函數優於副作用函數
const createUserSlug = (username: string): string => {
return username
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/-+/g, '-')
.trim();
};
// ✅ 錯誤處理明確
const fetchUserProfile = async (userId: string): Promise => {
try {
const response = await api.get(`/users/${userId}`);
if (!response.data) {
throw new Error('User profile not found');
}
return response.data;
} catch (error) {
logger.error('Failed to fetch user profile', { userId, error });
throw error;
}
};
```
#### 異步處理規範
```typescript
// ✅ 使用async/await而非Promise.then
const processDialogueAnalysis = async (
dialogueId: string
): Promise => {
const dialogue = await getDialogue(dialogueId);
const analysis = await analyzeDialogueWithAI(dialogue.messages);
const savedAnalysis = await saveAnalysisResults(dialogueId, analysis);
return savedAnalysis;
};
// ✅ 適當的錯誤處理和重試機制
const retryOperation = async (
operation: () => Promise,
maxRetries: number = 3,
delayMs: number = 1000
): Promise => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
await new Promise(resolve => setTimeout(resolve, delayMs * attempt));
}
}
throw new Error('All retry attempts failed');
};
```
### React/React Native 組件規範
#### 組件結構
```tsx
// ✅ 組件檔案結構標準
import React, { useState, useEffect, useCallback } from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { Button } from '@/components/ui';
import { DialogueService } from '@/services';
import { updateDialogueProgress } from '@/store/slices/dialogueSlice';
import type { Dialogue, DialogueMessage } from '@/types';
// ✅ Props介面定義
interface DialogueChatProps {
dialogueId: string;
onDialogueComplete: (dialogue: Dialogue) => void;
isVisible: boolean;
}
// ✅ 組件主體
export const DialogueChat: React.FC = ({
dialogueId,
onDialogueComplete,
isVisible,
}) => {
// State declarations
const [inputText, setInputText] = useState('');
const [isLoading, setIsLoading] = useState(false);
// Redux selectors
const dialogue = useAppSelector(state =>
state.dialogue.currentDialogue
);
const dispatch = useAppDispatch();
// Effects
useEffect(() => {
if (isVisible && dialogueId) {
loadDialogue();
}
}, [isVisible, dialogueId]);
// Handlers
const handleSendMessage = useCallback(async () => {
if (!inputText.trim()) return;
setIsLoading(true);
try {
const response = await DialogueService.sendMessage(dialogueId, inputText);
dispatch(updateDialogueProgress(response));
setInputText('');
} catch (error) {
// Error handling
} finally {
setIsLoading(false);
}
}, [dialogueId, inputText, dispatch]);
const loadDialogue = useCallback(async () => {
// Load dialogue logic
}, [dialogueId]);
// Early returns
if (!dialogue) {
return ;
}
// Main render
return (
{dialogue.scenarioTitle}
{/* Component content */}
);
};
// ✅ 樣式定義
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 16,
},
});
```
#### Hooks使用規範
```tsx
// ✅ 自定義Hook範例
export const useDialogueAnalysis = (dialogueId: string) => {
const [analysis, setAnalysis] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const analyzeDialogue = useCallback(async () => {
setLoading(true);
setError(null);
try {
const result = await DialogueService.getAnalysis(dialogueId);
setAnalysis(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Analysis failed');
} finally {
setLoading(false);
}
}, [dialogueId]);
useEffect(() => {
if (dialogueId) {
analyzeDialogue();
}
}, [analyzeDialogue, dialogueId]);
return {
analysis,
loading,
error,
refetch: analyzeDialogue
};
};
```
### API和服務層規範
#### API客戶端結構
```typescript
// ✅ API服務類設計
export class DialogueService {
private static readonly BASE_URL = '/api/v1/dialogues';
static async startDialogue(request: CreateDialogueRequest): Promise {
const response = await apiClient.post>(
`${this.BASE_URL}/start`,
request
);
if (!response.data.success) {
throw new ApiError(
response.data.error?.message || 'Failed to start dialogue'
);
}
return response.data.data!;
}
static async sendMessage(
dialogueId: string,
message: string
): Promise {
const response = await apiClient.post>(
`${this.BASE_URL}/${dialogueId}/message`,
{ message, message_type: 'text' }
);
return this.handleApiResponse(response);
}
private static handleApiResponse(
response: ApiResponse
): T {
if (!response.success || !response.data) {
throw new ApiError(
response.error?.message || 'API request failed'
);
}
return response.data;
}
}
// ✅ 錯誤處理類
export class ApiError extends Error {
constructor(
message: string,
public statusCode?: number,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
```
## 資料庫操作規範
### SQL查詢撰寫標準
#### 查詢可讀性
```sql
-- ✅ 好的SQL格式 - 關鍵字大寫、適當縮排
SELECT
u.user_id,
u.username,
u.total_score,
COUNT(d.dialogue_id) AS total_dialogues,
AVG(d.overall_score)::INTEGER AS avg_score
FROM users u
LEFT JOIN dialogues d ON u.user_id = d.user_id
AND d.status = 'completed'
WHERE u.status = 'active'
AND u.created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY u.user_id, u.username, u.total_score
HAVING COUNT(d.dialogue_id) >= 5
ORDER BY u.total_score DESC
LIMIT 100;
-- ❌ 避免的格式
select u.user_id,u.username,count(d.dialogue_id) from users u left join dialogues d on u.user_id=d.user_id where u.status='active' group by u.user_id,u.username;
```
#### 效能考量
```sql
-- ✅ 使用適當索引和條件
-- 確保WHERE條件中的欄位有索引
SELECT dialogue_id, created_at, overall_score
FROM dialogues
WHERE user_id = $1 -- 有索引
AND created_at >= $2 -- 有索引
AND status = 'completed' -- 有索引
ORDER BY created_at DESC
LIMIT 20;
-- ✅ 避免N+1查詢問題
WITH user_stats AS (
SELECT
user_id,
COUNT(*) as dialogue_count,
AVG(overall_score) as avg_score
FROM dialogues
WHERE status = 'completed'
GROUP BY user_id
)
SELECT
u.username,
COALESCE(us.dialogue_count, 0) as total_dialogues,
COALESCE(us.avg_score, 0) as average_score
FROM users u
LEFT JOIN user_stats us ON u.user_id = us.user_id
WHERE u.status = 'active';
```
### ORM使用規範 (以Prisma為例)
```typescript
// ✅ Prisma查詢最佳實踐
export class UserRepository {
// ✅ 使用事務處理相關操作
static async updateUserScoreAndLevel(
userId: string,
scoreIncrease: number
): Promise {
return await prisma.$transaction(async (tx) => {
const user = await tx.user.findUniqueOrThrow({
where: { userId },
});
const newTotalScore = user.totalScore + scoreIncrease;
const newLevel = this.calculateUserLevel(newTotalScore);
return await tx.user.update({
where: { userId },
data: {
totalScore: newTotalScore,
currentLevel: newLevel,
updatedAt: new Date(),
},
});
});
}
// ✅ 使用include避免N+1問題
static async getUserWithRecentDialogues(
userId: string
): Promise {
return await prisma.user.findUniqueOrThrow({
where: { userId },
include: {
dialogues: {
where: {
status: 'completed',
createdAt: {
gte: subDays(new Date(), 7)
}
},
orderBy: { createdAt: 'desc' },
take: 10,
},
subscription: true,
},
});
}
// ✅ 適當的錯誤處理
private static calculateUserLevel(totalScore: number): string {
if (totalScore < 1000) return 'A1';
if (totalScore < 3000) return 'A2';
if (totalScore < 6000) return 'B1';
if (totalScore < 10000) return 'B2';
if (totalScore < 15000) return 'C1';
return 'C2';
}
}
```
## 測試規範
### 單元測試
```typescript
// ✅ 測試結構 - AAA模式 (Arrange, Act, Assert)
import { calculateDialogueScore } from '@/utils/scoring';
describe('calculateDialogueScore', () => {
it('should calculate correct weighted average score', () => {
// Arrange
const grammarScore = 90;
const semanticScore = 80;
const fluencyScore = 85;
const expectedScore = 84; // 90*0.3 + 80*0.4 + 85*0.3 = 84
// Act
const result = calculateDialogueScore(grammarScore, semanticScore, fluencyScore);
// Assert
expect(result).toBe(expectedScore);
});
it('should handle edge cases with zero scores', () => {
// Arrange & Act
const result = calculateDialogueScore(0, 0, 0);
// Assert
expect(result).toBe(0);
});
it('should round to nearest integer', () => {
// Arrange
const result = calculateDialogueScore(85, 87, 83); // Expected: 85.4 → 85
// Assert
expect(result).toBe(85);
});
});
```
### 整合測試
```typescript
// ✅ API整合測試
import request from 'supertest';
import { app } from '@/app';
import { setupTestDatabase, cleanupTestDatabase } from '@/test/setup';
describe('POST /api/v1/dialogues/start', () => {
beforeEach(async () => {
await setupTestDatabase();
});
afterEach(async () => {
await cleanupTestDatabase();
});
it('should start new dialogue successfully', async () => {
// Arrange
const requestBody = {
scenarioId: 'SC_Restaurant_01',
difficultyOverride: 'A2',
};
// Act
const response = await request(app)
.post('/api/v1/dialogues/start')
.set('Authorization', 'Bearer valid-jwt-token')
.send(requestBody)
.expect(201);
// Assert
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('dialogueId');
expect(response.body.data).toHaveProperty('sessionToken');
expect(response.body.data.scenarioId).toBe(requestBody.scenarioId);
});
it('should return 400 for invalid scenario ID', async () => {
// Act
const response = await request(app)
.post('/api/v1/dialogues/start')
.set('Authorization', 'Bearer valid-jwt-token')
.send({ scenarioId: 'INVALID_ID' })
.expect(400);
// Assert
expect(response.body.success).toBe(false);
expect(response.body.error.code).toBe('INVALID_SCENARIO');
});
});
```
## 版本控制規範
### Git工作流程
#### 分支策略 (Git Flow)
```bash
# ✅ 分支命名規範
main # 生產環境代碼
develop # 開發整合分支
feature/JIRA-123-user-auth # 功能開發
hotfix/fix-login-bug # 緊急修復
release/v1.2.0 # 發布準備
# ✅ 功能開發工作流程
git checkout develop
git pull origin develop
git checkout -b feature/JIRA-123-dialogue-analysis
# 進行開發...
git add .
git commit -m "feat: implement dialogue analysis scoring algorithm"
git push origin feature/JIRA-123-dialogue-analysis
# 創建Pull Request到develop分支
```
#### Commit Message規範
```bash
# ✅ 使用Conventional Commits格式
# 類型(範圍): 簡短描述
feat(api): add dialogue analysis endpoint
fix(ui): resolve button click not working on iOS
docs(readme): update installation instructions
style(components): fix linting issues in DialogueChat
refactor(auth): simplify JWT token validation
test(dialogue): add unit tests for scoring algorithm
chore(deps): update React Native to 0.72.4
# ✅ 完整範例
feat(dialogue): implement AI-powered grammar scoring
- Add OpenAI integration for grammar analysis
- Implement scoring algorithm with configurable weights
- Add error handling and retry logic
- Update dialogue model to store grammar scores
Fixes #123
```
### Code Review規範
#### PR檢查清單
```markdown
## Pull Request Checklist
### 功能性檢查
- [ ] 功能按需求正確實現
- [ ] 邊界條件和錯誤處理完善
- [ ] 相關測試已添加並通過
- [ ] 不會破壞現有功能
### 代碼品質
- [ ] 代碼遵循團隊規範
- [ ] 變數和函數命名清晰
- [ ] 沒有重複代碼
- [ ] 效能考量適當
### 文檔和註釋
- [ ] 複雜邏輯有適當註釋
- [ ] API文檔已更新
- [ ] README或相關文檔已更新
### 安全性
- [ ] 沒有敏感資訊洩漏
- [ ] 輸入驗證和清理適當
- [ ] 權限檢查正確
### 其他
- [ ] 資料庫遷移腳本(如需要)
- [ ] 環境變數文檔更新
- [ ] 部署注意事項說明
```
#### Review回饋準則
```markdown
# ✅ 建設性回饋範例
## 主要問題 (Must Fix)
- 🚨 **安全問題**: SQL注入風險,請使用參數化查詢
- 🚨 **效能問題**: N+1查詢問題,建議使用JOIN或預加載
- 🚨 **邏輯錯誤**: 條件判斷有問題,會導致錯誤的計算結果
## 建議改進 (Should Consider)
- 💡 **代碼組織**: 建議將此邏輯提取到單獨函數提高可讀性
- 💡 **錯誤處理**: 考慮添加更具體的錯誤訊息
- 💡 **測試覆蓋**: 建議添加邊界條件測試
## 小問題 (Nice to Have)
- 🎨 **代碼風格**: 變數命名可以更具描述性
- 🎨 **註釋**: 複雜算法建議添加註釋說明
```
## 環境配置標準
### 開發環境設定
#### package.json腳本
```json
{
"scripts": {
"dev": "concurrently \"npm run dev:api\" \"npm run dev:mobile\"",
"dev:api": "nodemon --exec ts-node src/server.ts",
"dev:mobile": "expo start",
"build": "tsc && npm run build:mobile",
"build:mobile": "expo build:ios && expo build:android",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"type-check": "tsc --noEmit"
}
}
```
#### 環境變數管理
```bash
# ✅ .env.example - 範本文件
NODE_ENV=development
PORT=3001
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dramaling_dev
REDIS_URL=redis://localhost:6379
# External APIs
OPENAI_API_KEY=your_openai_api_key_here
STRIPE_SECRET_KEY=sk_test_your_stripe_key_here
# JWT
JWT_SECRET=your_jwt_secret_here
JWT_EXPIRES_IN=7d
# AWS
AWS_ACCESS_KEY_ID=your_aws_access_key
AWS_SECRET_ACCESS_KEY=your_aws_secret_key
AWS_REGION=ap-northeast-1
S3_BUCKET_NAME=dramaling-assets-dev
```
```typescript
// ✅ 環境變數驗證
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'staging', 'production']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string().url(),
OPENAI_API_KEY: z.string().min(1),
JWT_SECRET: z.string().min(32),
});
export const env = envSchema.parse(process.env);
```
## 文檔撰寫規範
### API文檔標準
```typescript
/**
* 開始新的對話練習
*
* @route POST /api/v1/dialogues/start
* @param {CreateDialogueRequest} request - 對話創建請求
* @param {string} request.scenarioId - 場景ID (必填)
* @param {string} [request.difficultyOverride] - 難度覆寫 (可選)
* @param {string[]} [request.targetVocabulary] - 目標詞彙列表 (可選)
* @returns {Promise} 創建的對話物件
*
* @throws {ApiError} SCENARIO_NOT_FOUND - 場景不存在
* @throws {ApiError} SUBSCRIPTION_REQUIRED - 需要訂閱權限
* @throws {ApiError} DAILY_LIMIT_EXCEEDED - 超過每日使用限制
*
* @example
* ```typescript
* const dialogue = await DialogueService.startDialogue({
* scenarioId: 'SC_Restaurant_01',
* difficultyOverride: 'A2',
* targetVocabulary: ['reservation', 'menu', 'order']
* });
* ```
*/
export const startDialogue = async (
request: CreateDialogueRequest
): Promise => {
// 實現邏輯
};
```
### README撰寫規範
```markdown
# Drama Ling - 語言學習對話練習應用
## 專案概述
Drama Ling 是一款結合AI分析的情境對話練習應用,幫助用戶在真實場景中提升語言溝通能力。
## 技術棧
- **前端**: React Native + TypeScript
- **後端**: Node.js + Express + TypeScript
- **資料庫**: PostgreSQL + Redis
- **AI服務**: OpenAI GPT-4
- **雲端**: AWS (ECS + RDS + S3)
## 快速開始
### 環境要求
- Node.js 18+
- PostgreSQL 15+
- Redis 7+
- React Native 0.72+
### 安裝步驟
1. 複製專案
```bash
git clone https://github.com/company/dramaling-app.git
cd dramaling-app
```
2. 安裝依賴
```bash
npm install
```
3. 設定環境變數
```bash
cp .env.example .env
# 編輯 .env 填入實際配置
```
4. 資料庫設置
```bash
npm run db:migrate
npm run db:seed
```
5. 啟動開發服務
```bash
npm run dev
```
## 專案結構
```
src/
├── components/ # 共用UI組件
├── screens/ # 頁面組件
├── services/ # API和業務邏輯服務
├── store/ # Redux狀態管理
├── utils/ # 工具函數
├── types/ # TypeScript類型定義
└── constants/ # 常數定義
```
## 開發流程
1. 從`develop`分支創建feature分支
2. 遵循代碼規範和測試要求
3. 創建Pull Request並等待Review
4. 合併後自動部署到staging環境
## 測試
```bash
npm run test # 執行所有測試
npm run test:watch # 監視模式
npm run test:coverage # 測試覆蓋率報告
```
## 部署
- **Staging**: 自動部署當develop分支更新時
- **Production**: 手動部署當release分支創建時
## 貢獻指南
請閱讀 [CONTRIBUTING.md](CONTRIBUTING.md) 了解詳細的開發和貢獻流程。
## 授權
本專案採用 MIT 授權 - 詳見 [LICENSE](LICENSE) 文件
```
---
## 程式碼審查檢查清單
### 自我檢查項目
- [ ] 代碼遵循團隊規範和風格指南
- [ ] 所有函數和類都有適當的類型標註
- [ ] 複雜邏輯有清楚的註釋說明
- [ ] 錯誤處理和邊界條件考慮周全
- [ ] 單元測試覆蓋新增功能
- [ ] 沒有console.log或調試代碼殘留
- [ ] 沒有TODO或FIXME未處理
- [ ] 效能考量適當(避免不必要的重新渲染等)
### 團隊審查重點
- [ ] 架構設計合理性
- [ ] API設計的一致性
- [ ] 資料流和狀態管理
- [ ] 安全性考量
- [ ] 可維護性和擴展性
- [ ] 與現有代碼的整合度
---
## 待完成任務
### 高優先級
1. [ ] 建立ESLint和Prettier配置文件
2. [ ] 設置Pre-commit hooks強制代碼格式檢查
3. [ ] 建立代碼審查模板和流程
4. [ ] 設置自動化測試的CI流程
### 中優先級
1. [ ] 建立API文檔自動生成工具
2. [ ] 設置代碼覆蓋率報告和監控
3. [ ] 建立效能測試標準和工具
4. [ ] 制定安全代碼審查檢查清單
### 低優先級
1. [ ] 研究和引入靜態代碼分析工具
2. [ ] 建立自動化代碼品質評分系統
3. [ ] 探索AI輔助代碼審查工具
4. [ ] 建立團隊技術分享和最佳實踐文檔
---
**最後更新**: 2024年9月5日
**負責人**: 技術負責人
**審查週期**: 每月檢討和更新