24 KiB
24 KiB
程式碼規範與開發標準
概述
建立統一的程式碼撰寫規範和開發流程標準,確保團隊協作效率和代碼品質。
通用開發原則
代碼品質原則
- 可讀性優先: 代碼應該容易閱讀和理解
- 一致性: 遵循統一的命名和格式規範
- 簡潔性: 避免過度複雜的解決方案
- 可測試性: 代碼結構便於單元測試
- 可維護性: 考慮未來修改和擴展的便利性
SOLID原則遵循
- 單一職責: 每個函數/類只負責一個明確的功能
- 開放封閉: 對擴展開放,對修改封閉
- 里氏替換: 子類應該能夠替換父類
- 介面隔離: 不應該依賴不需要的介面
- 依賴倒置: 依賴抽象而非具體實現
C# (.NET Core) 規範
基本格式規則
EditorConfig 配置
# .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 分析器規則
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<WarningsNotAsErrors>CS1591</WarningsNotAsErrors>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
命名規範
C# 命名慣例
// ✅ 類別和方法使用PascalCase
public class UserService
{
public async Task<UserProfile> 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<bool> 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後綴
常數和列舉
// ✅ 常數使用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',
}
類型定義
// ✅ 介面使用PascalCase,以I開頭(可選)
interface UserProfile {
userId: string;
username: string;
email: string;
createdAt: Date;
subscription: UserSubscriptionPlan;
}
interface ApiResponse<T> {
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[];
};
函數撰寫規範
函數設計原則
// ✅ 函數應該小巧、單一職責
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<UserProfile> => {
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;
}
};
異步處理規範
// ✅ 使用async/await而非Promise.then
const processDialogueAnalysis = async (
dialogueId: string
): Promise<DialogueAnalysis> => {
const dialogue = await getDialogue(dialogueId);
const analysis = await analyzeDialogueWithAI(dialogue.messages);
const savedAnalysis = await saveAnalysisResults(dialogueId, analysis);
return savedAnalysis;
};
// ✅ 適當的錯誤處理和重試機制
const retryOperation = async <T>(
operation: () => Promise<T>,
maxRetries: number = 3,
delayMs: number = 1000
): Promise<T> => {
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 組件規範
組件結構
// ✅ 組件檔案結構標準
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<DialogueChatProps> = ({
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 <LoadingSpinner />;
}
// Main render
return (
<View style={styles.container}>
<Text style={styles.title}>{dialogue.scenarioTitle}</Text>
{/* Component content */}
<Button
title="發送訊息"
onPress={handleSendMessage}
disabled={isLoading}
/>
</View>
);
};
// ✅ 樣式定義
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
title: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 16,
},
});
Hooks使用規範
// ✅ 自定義Hook範例
export const useDialogueAnalysis = (dialogueId: string) => {
const [analysis, setAnalysis] = useState<DialogueAnalysis | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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客戶端結構
// ✅ API服務類設計
export class DialogueService {
private static readonly BASE_URL = '/api/v1/dialogues';
static async startDialogue(request: CreateDialogueRequest): Promise<Dialogue> {
const response = await apiClient.post<ApiResponse<Dialogue>>(
`${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<DialogueMessage> {
const response = await apiClient.post<ApiResponse<DialogueMessage>>(
`${this.BASE_URL}/${dialogueId}/message`,
{ message, message_type: 'text' }
);
return this.handleApiResponse(response);
}
private static handleApiResponse<T>(
response: ApiResponse<T>
): 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格式 - 關鍵字大寫、適當縮排
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;
效能考量
-- ✅ 使用適當索引和條件
-- 確保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為例)
// ✅ Prisma查詢最佳實踐
export class UserRepository {
// ✅ 使用事務處理相關操作
static async updateUserScoreAndLevel(
userId: string,
scoreIncrease: number
): Promise<User> {
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<UserWithDialogues> {
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';
}
}
測試規範
單元測試
// ✅ 測試結構 - 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);
});
});
整合測試
// ✅ 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)
# ✅ 分支命名規範
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規範
# ✅ 使用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檢查清單
## Pull Request Checklist
### 功能性檢查
- [ ] 功能按需求正確實現
- [ ] 邊界條件和錯誤處理完善
- [ ] 相關測試已添加並通過
- [ ] 不會破壞現有功能
### 代碼品質
- [ ] 代碼遵循團隊規範
- [ ] 變數和函數命名清晰
- [ ] 沒有重複代碼
- [ ] 效能考量適當
### 文檔和註釋
- [ ] 複雜邏輯有適當註釋
- [ ] API文檔已更新
- [ ] README或相關文檔已更新
### 安全性
- [ ] 沒有敏感資訊洩漏
- [ ] 輸入驗證和清理適當
- [ ] 權限檢查正確
### 其他
- [ ] 資料庫遷移腳本(如需要)
- [ ] 環境變數文檔更新
- [ ] 部署注意事項說明
Review回饋準則
# ✅ 建設性回饋範例
## 主要問題 (Must Fix)
- 🚨 **安全問題**: SQL注入風險,請使用參數化查詢
- 🚨 **效能問題**: N+1查詢問題,建議使用JOIN或預加載
- 🚨 **邏輯錯誤**: 條件判斷有問題,會導致錯誤的計算結果
## 建議改進 (Should Consider)
- 💡 **代碼組織**: 建議將此邏輯提取到單獨函數提高可讀性
- 💡 **錯誤處理**: 考慮添加更具體的錯誤訊息
- 💡 **測試覆蓋**: 建議添加邊界條件測試
## 小問題 (Nice to Have)
- 🎨 **代碼風格**: 變數命名可以更具描述性
- 🎨 **註釋**: 複雜算法建議添加註釋說明
環境配置標準
開發環境設定
package.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"
}
}
環境變數管理
# ✅ .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
// ✅ 環境變數驗證
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文檔標準
/**
* 開始新的對話練習
*
* @route POST /api/v1/dialogues/start
* @param {CreateDialogueRequest} request - 對話創建請求
* @param {string} request.scenarioId - 場景ID (必填)
* @param {string} [request.difficultyOverride] - 難度覆寫 (可選)
* @param {string[]} [request.targetVocabulary] - 目標詞彙列表 (可選)
* @returns {Promise<Dialogue>} 創建的對話物件
*
* @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<Dialogue> => {
// 實現邏輯
};
README撰寫規範
# 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
-
安裝依賴
npm install -
設定環境變數
cp .env.example .env # 編輯 .env 填入實際配置 -
資料庫設置
npm run db:migrate npm run db:seed -
啟動開發服務
npm run dev
專案結構
src/
├── components/ # 共用UI組件
├── screens/ # 頁面組件
├── services/ # API和業務邏輯服務
├── store/ # Redux狀態管理
├── utils/ # 工具函數
├── types/ # TypeScript類型定義
└── constants/ # 常數定義
開發流程
- 從
develop分支創建feature分支 - 遵循代碼規範和測試要求
- 創建Pull Request並等待Review
- 合併後自動部署到staging環境
測試
npm run test # 執行所有測試
npm run test:watch # 監視模式
npm run test:coverage # 測試覆蓋率報告
部署
- Staging: 自動部署當develop分支更新時
- Production: 手動部署當release分支創建時
貢獻指南
請閱讀 CONTRIBUTING.md 了解詳細的開發和貢獻流程。
授權
本專案採用 MIT 授權 - 詳見 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日
**負責人**: 技術負責人
**審查週期**: 每月檢討和更新