我認為在 AI 時代下,軟體開發的原則反而變得更加重要。
因為如果不懂這些開發原則,放任 AI 自主開發,就會導致整個程式碼快速膨脹,甚至變得雜亂無章,遲早會累積出一個沒人敢動的爛攤子。
在 Code Complete 這本書中,就有提到幾種優良的程式設計原則,這篇文章挑了 5 個我覺得最實用的,並附上範例讓你對照。
程式設計原則一:最小複雜度
Code Complete 對最小複雜度的定義是:設計應該讓你在專注某一個部分時,能安全地忽略其他大多數部分。
如果你打開一個檔案,腦中要同時處理的東西越少,這個設計就越成功。
也因此,我們其實要避免那些過於聰明的設計,因為聰明的設計通常很難理解,真正好的設計反而是看起來簡單、直白的
以建立訂單的 API handler 為例:
export async function postOrder(request: Request) {
const body = await request.json();
if (!body.userId || !body.items || body.items.length === 0) {
return Response.json({ ok: false, message: '缺少欄位' }, { status: 400 });
}
const user = await db.query.users.findFirst({
where: eq(users.id, body.userId),
});
if (!user) {
return Response.json(
{ ok: false, message: '找不到使用者' },
{ status: 404 }
);
}
const payment = await stripeClient.charge(body.cardToken, body.amount);
const order = await db
.insert(orders)
.values({
id: nanoid(),
userId: body.userId,
amount: body.amount,
paymentId: payment.id,
})
.returning();
await resend.emails.send({
to: user.email,
subject: '訂單成立',
});
return Response.json({ ok: true, orderId: order[0].id });
}這個 handler 同時負責驗證、查資料庫、處理付款、寫入訂單、寄通知信五件事。
任何一個環節要改,都得打開這個函式,而且改動時要小心不要影響其他環節。
export async function postOrder(request: Request) {
const body = await request.json();
const parsed = newOrderSchema.safeParse(body);
if (!parsed.success) {
return Response.json({ ok: false, message: '表單錯誤' }, { status: 400 });
}
const result = await createOrder(parsed.data);
return Response.json(result, { status: result.ok ? 201 : 400 });
}handler 只負責解析 HTTP 請求和回傳結果,建立訂單的流程交給 createOrder 函式處理,他介於 HTTP 層和資料庫層之間的業務邏輯層,並不關心資料從哪裡來或要回傳什麼格式,只負責操作具體要做哪些事情。
這樣讀 handler 時就不需要理解付款邏輯,讀 createOrder 時不需要理解 HTTP 格式,每個地方都只需要知道自己該知道的事。
程式設計原則二:高扇入、High Fan-in
Code Complete 的定義是:同一個底層模組被越多地方使用,代表設計越健康。
簡單說就是共通邏輯要收在同一個地方,不要各處各寫一份。
底層的東西被越多人用,代表你做的抽象是有價值的,而不是每個人各自造了一個輪子。
以訂單建立的驗證規則為例:
export async function createOrderFromCheckout(input: unknown) {
return checkoutOrderSchema.parse(input);
}
export async function createOrderFromAdmin(input: unknown) {
return adminOrderSchema.parse(input);
}
export async function createOrderFromApi(input: unknown) {
return apiOrderSchema.parse(input);
}三個入口各自定義了三套幾乎相同的 schema,只要驗證規則需要調整,就得同時修改三個地方,容易改漏或改不一致。
所以應該抽離成一個驗證規則,讓彼此共用:
export const orderSchema = z.object({
userId: z.string(),
items: z.array(z.object({ productId: z.string(), quantity: z.number() })),
cardToken: z.string(),
});orderSchema.parse(checkoutInput);
orderSchema.parse(adminInput);
orderSchema.parse(apiInput);這樣之後驗證邏輯有任何調整,只需要改一個地方。
程式設計原則三:低到中度扇出、Low-to-medium Fan-out
Code Complete 的定義是:一個模組只依賴少量的其他模組,意思是一個函式直接認識的外部東西越少越好。
如果一個函式同時依賴七個以上的外部服務或模組,通常代表它承擔了太多責任,已經開始變得過於複雜。
以課程頁面同步為例:
export async function syncCoursePage(courseId: string) {
const course = await db.query.courses.findFirst(...);
const lessons = await db.query.courseLessons.findMany(...);
const faq = await db.query.courseFaqItems.findMany(...);
const comments = await db.query.comments.findMany(...);
await cache.set(`course:${courseId}`, { course, lessons, faq, comments });
await searchService.sync(courseId);
await cdnService.purge(`/courses/${course?.slug}`);
}這個函式直接碰了資料庫四張表、快取、搜尋服務、CDN,共七個外部依賴。
任何一個服務的介面變動,都可能要回來改這個函式,所以我們可以將快許和發布的細節收到自己的模組當中:
export async function syncCoursePage(courseId: string) {
const pageData = await coursePageService.load(courseId);
await coursePageCache.save(courseId, pageData);
await coursePagePublisher.publish(pageData);
}上層只看到三個動作,依賴模組從七個降回三個,之後要替換其中一段實作也比較容易處理。
程式設計原則四:良好分層性(Stratification)
白話說就是:每一層只做自己的事,不需要跑去看另一層才能理解現在在做什麼。改畫面時只看畫面層,改業務邏輯時只看 use case 層,每一層都有自己清楚的位置。
以前端 UI 為例:
export async function CourseCard({ courseId }: { courseId: string }) {
const course = await db.course.findUnique({
where: { id: courseId },
include: { teacher: true },
});
return (
<div>
<h2>{course?.title}</h2>
<p>{course?.teacher.name}</p>
</div>
);
}這個 component 同時做了查資料庫和渲染畫面兩件事,閱讀時必須同時理解 Prisma 查詢語法和 JSX 結構,層次混在一起。
export async function CourseCardContainer({ courseId }: { courseId: string }) {
const course = await getCourseCardData(courseId);
return <CourseCard title={course.title} teacherName={course.teacherName} />;
}type CourseCardProps = {
title: string;
teacherName: string;
};
export function CourseCard({ title, teacherName }: CourseCardProps) {
return (
<div>
<h2>{title}</h2>
<p>{teacherName}</p>
</div>
);
}改完後,畫面層只負責畫面,資料層只負責資料。
改 UI 時不需要看資料庫查詢,改查詢邏輯時不需要看 JSX 結構。每一層都有自己清楚的職責範圍。
架構層也是同樣道理:route 只負責進出 HTTP,use case 只負責業務流程,repository 只負責資料存取。
層與層之間的邊界越清楚,出問題時才知道該往哪一層找。
程式設計原則五:低耦合(Loose Coupling)
Code Complete 的定義是:把程式各部分之間的連結降到最低。透過良好的介面抽象、封裝和資訊隱藏,讓模組之間的直接相連減少,連結越少,整合、測試、維護的成本就越低。
以訂單建立流程為例:
export async function createOrder(input: CreateOrderInput) {
const payment = await stripeClient.charge(input.cardToken, input.amount);
const order = await db
.insert(orders)
.values({
id: nanoid(),
userId: input.userId,
amount: input.amount,
paymentId: payment.id,
})
.returning();
await resend.emails.send({
to: input.email,
subject: '訂單成立',
});
return order[0];
}這個函式直接寫死了 Stripe SDK、Drizzle 查詢語法、Resend API,三個外部服務的實作細節全部暴露在主流程裡。
之後只要其中任何一個服務要換,主流程就得跟著改。
export async function createOrder(input: CreateOrderInput) {
const payment = await paymentGateway.charge({
token: input.cardToken,
amount: input.amount,
});
const order = await orderRepository.create({
userId: input.userId,
amount: input.amount,
paymentId: payment.id,
});
await orderNotifier.sendCreated(order);
return order;
}主流程只認識 paymentGateway、orderRepository、orderNotifier 這三個介面,
不在乎背後是 Stripe 還是別家、是 Drizzle 還是其他 ORM。之後要換掉任何一個服務,改動只需要在各自的 adapter 那層,主流程完全不需要動。
總結
這五個原則背後其實只有一件事:控制複雜度。
Code Complete 說,軟體開發最核心的技術挑戰就是管理複雜度。
程式難以維護、難以擴充,根本原因通常不是技術選型錯誤,而是複雜度失控,讓人沒辦法在改一個地方的時候忽略其他地方。
而控制複雜度的方法,就是維持良好的軟體開發習慣,尤其是在 AI 大量開發的情況下更要維護好品質。
