| name | engineering-principles |
| description | 重构或新增 feature(Rust / TypeScript)时遵循的软件工程基本原则: DRY(不要重复自己)、单一职责与高内聚、正交、最小暴露与接口隔离、 开闭原则、里氏替换原则、依赖反转原则、清晰注释。 用于架构决策、代码拆分、接口设计、抽象边界审查。 |
软件工程基本原则
在本项目内重构或新增 Rust(crates/)或 TypeScript(ui/)代码时,必须在设计和审查阶段遵循以下原则。每个原则包含判断标准、Rust 正反例、TypeScript 正反例。末尾的「重构执行流程」和「检查清单」确保改动严格满足所有要求。
1. DRY — 不要重复自己
判断: 同一知识/逻辑/规则在系统中有且仅有一处表示。注意区分的不是"文本相似"而是"语义相同"——两个函数写起来像但服务于不同业务目标、有不同变化节奏时,合并反而是耦合。
Rust
pub enum CoreError { WorkNotFound(String) }
pub enum ServerError { WorkNotFound(String) }
#[derive(Debug, thiserror::Error)]
pub enum AgentError {
#[error("作品不存在: {0}")]
WorkNotFound(String),
}
#[derive(Debug, thiserror::Error)]
pub enum CoreError {
#[error(transparent)]
Agent(#[from] AgentError),
}
TypeScript
function ChatPanel() {
const [msgs, setMsgs] = useState<Message[]>([]);
useEffect(() => {
const u = listen<MsgEvent>('new-msg', e => setMsgs(p => [...p, e.payload]));
return () => { u.then(f => f()); };
}, []);
}
function NotificationBar() {
const [msgs, setMsgs] = useState<Message[]>([]);
useEffect(() => {
const u = listen<MsgEvent>('new-msg', e => setMsgs(p => [...p, e.payload]));
return () => { u.then(f => f()); };
}, []);
}
function useIncomingMessages() {
const [msgs, setMsgs] = useState<Message[]>([]);
useEffect(() => {
const u = listen<MsgEvent>('new-msg', e => setMsgs(p => [...p, e.payload]));
return () => { u.then(f => f()); };
}, []);
return msgs;
}
2. 单一职责与高内聚
判断: 一个模块/函数只有一个引起变化的原因。如果描述这个函数做了什么需要 "和" 字连接,就该拆分。高内聚是其内部可观察结果——所有方法围绕同一组数据、同一个目标。
Rust
fn handle_user_message(msg: &str, db: &SessionDb) -> Result<Response> {
let parsed = parse_message(msg)?;
let llm_resp = call_deepseek(&parsed)?;
db.save_turn(&parsed, &llm_resp)?;
emit_event("turn-complete", &llm_resp)?;
Ok(llm_resp)
}
fn parse_and_call(msg: &str) -> Result<(ParsedMessage, LlmResponse)> {
let parsed = parse_message(msg)?;
let resp = call_deepseek(&parsed)?;
Ok((parsed, resp))
}
fn persist_and_emit(p: &ParsedMessage, r: &LlmResponse, db: &SessionDb) -> Result<()> {
db.save_turn(p, r)?;
emit_event("turn-complete", r)?;
Ok(())
}
TypeScript
function WorkDetail({ name }: { name: string }) {
const [detail, setDetail] = useState<WorkDetail | null>(null);
const [loading, setLoading] = useState(true);
const [status, setStatus] = useState<WorkStatus>('idle');
useEffect(() => { invoke('get_detail', { name }).then(setDetail).finally(() => setLoading(false)); }, [name]);
useEffect(() => {
const u = listen<StatusEvent>('status', e => setStatus(e.payload.status));
return () => { u.then(f => f()); };
}, [name]);
if (loading) return <Spinner />;
return <div><StatusBadge status={status} /><h1>{detail.title}</h1></div>;
}
function useWorkDetail(name: string) { return { detail, loading }; }
function useWorkStatus(name: string) { return status; }
function WorkDetail({ name }: { name: string }) {
const { detail, loading } = useWorkDetail(name);
const status = useWorkStatus(name);
if (loading) return <Spinner />;
return <div><StatusBadge status={status} /><h1>{detail.title}</h1></div>;
}
3. 正交
判断: "修改模块 A 是否需要同时修改 B、C、D?"需要则不正交。本项目中 novel-core 与 novel-server 通过 EngineCommand/Event 枚举交互,前端与后端通过 Tauri invoke 通信——双方可独立演化。
Rust
fn build_response(engine: &EngineState) -> String {
format!("turn: {}", engine.inner.turn_count)
}
impl EngineState {
pub fn turn_count(&self) -> usize { self.inner.turn_count }
}
fn build_response(engine: &EngineState) -> String {
format!("turn: {}", engine.turn_count())
}
TypeScript
function SaveButton({ name }: { name: string }) {
return <button onClick={() => invoke('save', { name })}>Save</button>;
}
function SaveButton({ onSave }: { onSave: () => void }) {
return <button onClick={onSave}>Save</button>;
}
function WorkPage({ name }: { name: string }) {
return <SaveButton onSave={() => invoke('save', { name })} />;
}
4. 最小暴露与接口隔离
判断: 从提供方问 "外部真的需要这个吗?"(最小暴露);从消费方问 "我真的需要这个接口的所有方法吗?"(接口隔离)。两者互为补充。
Rust
pub struct SessionConfig {
pub db_path: PathBuf,
pub api_key: String,
}
pub struct SessionConfig {
db_path: PathBuf,
api_key: SecretString,
}
impl SessionConfig {
pub fn db_path(&self) -> &Path { &self.db_path }
}
pub trait Worker {
fn work(&self) -> Result<()>;
fn eat(&self, food: &Food) -> Result<()>;
fn sleep(&self, hours: u8) -> Result<()>;
}
pub trait Workable { fn work(&self) -> Result<()>; }
pub trait Eatable { fn eat(&self, food: &Food) -> Result<()>; }
pub trait Restable { fn sleep(&self, hours: u8) -> Result<()>; }
TypeScript
interface HeaderProps { work: Work }
function Header({ work }: HeaderProps) {
return <h1>{work.title} - {work.author}</h1>;
}
interface HeaderProps { title: string; author: string }
function Header({ title, author }: HeaderProps) {
return <h1>{title} - {author}</h1>;
}
function useSession() { return { messages, turnNumber, compactionStatus, apiConfig }; }
function useMessages() { return { messages, setMessages }; }
function useTurn() { return { turnNumber, compactionStatus }; }
5. 开闭原则
判断: 新增功能时是否需要修改已有代码?需要则违反 OCP。扩展点通过 trait/注册机制/配置驱动打开。
Rust
fn dispatch_tool(name: &str, args: &Value) -> Result<ToolResult> {
match name {
"read_file" => read_file_tool(args),
"write_file" => write_file_tool(args),
"web_search" => web_search_tool(args),
_ => Err(anyhow!("未知工具")),
}
}
trait Tool: Send + Sync {
fn name(&self) -> &str;
fn execute(&self, args: &Value) -> Result<ToolResult>;
}
struct ToolRegistry { tools: HashMap<String, Box<dyn Tool>> }
impl ToolRegistry {
fn register(&mut self, tool: Box<dyn Tool>) { self.tools.insert(tool.name().into(), tool); }
fn dispatch(&self, name: &str, args: &Value) -> Result<ToolResult> {
self.tools.get(name).ok_or_else(|| anyhow!("未知工具"))?.execute(args)
}
}
TypeScript
function WorkStatusBadge({ status }: { status: string }) {
if (status === 'running') return <Badge color="green">运行中</Badge>;
if (status === 'paused') return <Badge color="yellow">已暂停</Badge>;
return <Badge color="gray">未知</Badge>;
}
const STATUS_MAP: Record<string, { color: string; label: string }> = {
running: { color: 'green', label: '运行中' },
paused: { color: 'yellow', label: '已暂停' },
};
function WorkStatusBadge({ status }: { status: string }) {
const cfg = STATUS_MAP[status] ?? { color: 'gray', label: '未知' };
return <Badge color={cfg.color}>{cfg.label}</Badge>;
}
6. 里氏替换原则
判断: 子类型替换基类型后,调用方能否在不知情的情况下正常工作?出现 if (x instanceof SpecialCase) 特判即违反 LSP。
Rust
pub trait Cache {
fn get(&self, key: &str) -> Option<String>;
}
struct DiskCache { db_path: PathBuf }
impl Cache for DiskCache {
fn get(&self, key: &str) -> Option<String> {
std::fs::read_to_string(self.db_path.join(key)).ok()
}
}
pub trait Cache { fn get(&self, key: &str) -> Option<String>; }
pub trait AsyncCache { async fn get(&self, key: &str) -> Option<String>; }
TypeScript
const TextInput = forwardRef<HTMLInputElement, Props>((p, ref) => <input ref={ref} {...p} />);
const SearchInput = forwardRef<HTMLDivElement, Props>((p, ref) => <div ref={ref}>...</div>);
interface FieldHandle { focus: () => void; blur: () => void; }
const TextInput = forwardRef<FieldHandle, Props>((p, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
blur: () => inputRef.current?.blur(),
}));
return <input ref={inputRef} {...p} />;
});
7. 依赖反转原则
判断: 高层模块是否直接 import 了低层模块的具体实现?是则违反 DIP。双方都应依赖抽象(trait/interface)。
Rust
pub struct SessionManager {
db: rusqlite::Connection,
}
impl SessionManager {
pub fn save_message(&self, msg: &Message) -> Result<()> {
self.db.execute("INSERT INTO messages ...", params![...])?;
Ok(())
}
}
#[async_trait]
pub trait SessionStore: Send + Sync {
async fn save_message(&self, msg: &Message) -> Result<()>;
}
pub struct SessionManager<S: SessionStore> { store: S }
TypeScript
import { invoke } from '@tauri-apps/api/core';
function WorkList() {
const [works, setWorks] = useState<Work[]>([]);
useEffect(() => { invoke<Work[]>('list_works').then(setWorks); }, []);
return <ul>{works.map(w => <li key={w.name}>{w.name}</li>)}</ul>;
}
function useWorkList() {
const [works, setWorks] = useState<Work[]>([]);
useEffect(() => { invoke<Work[]>('list_works').then(setWorks); }, []);
return { works };
}
function WorkList() {
const { works } = useWorkList();
return <ul>{works.map(w => <li key={w.name}>{w.name}</li>)}</ul>;
}
8. 清晰注释
判断: 注释解释为什么这样写,而非做了什么。代码通过命名表达 "做什么"——好的注释补充代码无法表达的信息。
i += 1;
pub fn build_system_prompt(skills: &[SkillSummary], tools: &[ToolDef]) -> String { ... }
tokio::spawn(async move { db_operation().await })
if (loading) return <Spinner />;
function useWorkStatus(workName: string): WorkStatusState { ... }
useEffect(() => { connectWebSocket(url); }, []);
重构执行流程
改动代码时按以下步骤执行——每一步通过才进入下一步,修改代码后回到步骤 1 重跑。
1. 确认范围(Preserve Functionality)
- 通读待改代码的全部调用方,确认每个调用方的使用方式
- 明确 "改动前这段代码做了什么"——用一句话描述当前行为
- 如果有测试:先
cargo nextest run -p <crate> --profile ci 确认基线绿(禁止 cargo test)。如果没有:补一个最小测试覆盖当前行为
2. 应用原则(Apply Standards)
逐条对照上方的 8 个原则做判断:
| 优先级 | 原则 | 审查切入点 |
|---|
| 1 | 单一职责 | 这个模块是否只做一件事? |
| 2 | DRY | 是否存在同一知识的重复? |
| 3 | 最小暴露 | 新增的 pub/item 外部真的需要吗? |
| 4 | 正交 + 依赖反转 | 改动是否波及无关模块? |
| 5 | 开闭 + 里氏替换 | 新增功能是否需改已有代码?子类型能否无缝替换? |
| 6 | 清晰注释 | 不能一眼看出的意图是否有注释? |
3. 简化结构(Enhance Clarity)
- 消除嵌套三元:多条件用
match / switch / if-else,不嵌套 ?:
- 显式优于紧凑:不因"少写几行"而牺牲可读性。单独的变量声明 > 内联的长表达式
- 消除死代码:注释掉的旧代码用 git 管理,不留在源码中
- 合并过度拆分:如果一个 3 行的函数只被调一次且语义不独立,内联回去
4. 自检(Verify)
- 逐行 diff:每一处修改都能说清为什么改和行为是否不变
- 重跑步骤 1 的 nextest:
cargo nextest run -p <crate> --profile ci(禁止 cargo test),确认基线仍绿
- 如果有新增逻辑:补测试覆盖新路径
重构检查清单
改动完成后逐条确认:
冲突裁决
当原则冲突时按以下顺序取舍:
- 单一职责 > DRY:错误地合并两个职责不同但文本相似的代码,比保留两段清晰独立的代码危害更大
- 功能不变 > 简化:不能为了让代码 "更好看" 而改变行为
- 显式 > 紧凑:多写几行 > 一行塞进所有逻辑