"入门弟子求的是'能用',三流高手求的是'好用'。" ------ 《程序员武学心法》
前情回顾
在上一篇中,我们讲述了初学乍练的修炼------那个"Hello World"到"能跑就行"的阶段。
当你开始思考"为什么要这样写"而不是"怎么让它跑起来"的时候,恭喜你,你已经踏入了小有所成的大门,成为了江湖上的三流高手。
第一章:小有所成的特征
1.1 什么是小有所成?
小有所成,是程序员从"会写代码"到"会写好代码"的关键阶段。
就像武侠小说里,弟子从"会几招花拳绣腿"到"开始修炼内功心法"的转变,虽然还算不上一流高手,但已经能在江湖上行走了。
小有所成程序员的典型特征:
- 开始关注代码规范
- 知道什么是"代码坏味道"
- 会写单元测试(虽然经常偷懒不写)
- 开始使用设计模式(虽然经常用错)
- Git 不再只会
add、commit、push
1.2 初学乍练 vs 小有所成
javascript
// 同一个需求,不同境界的实现
// 需求:根据用户类型返回不同的折扣
// 初学乍练写法
function getDiscount(userType) {
if (userType === "normal") {
return 0
} else if (userType === "vip") {
return 0.1
} else if (userType === "svip") {
return 0.2
} else if (userType === "partner") {
return 0.3
} else {
return 0
}
}
// 小有所成写法
const DISCOUNT_MAP = {
normal: 0,
vip: 0.1,
svip: 0.2,
partner: 0.3,
}
function getDiscount(userType) {
return DISCOUNT_MAP[userType] ?? 0
}
// 区别:
// 1. 配置与逻辑分离
// 2. 新增类型只需改配置,不需改代码
// 3. 更容易测试
// 4. 更容易理解
第二章:小有所成的修炼内容
2.1 第一式:代码规范
javascript
// 小有所成必修:代码规范
// ===== 命名规范 =====
// 变量:小驼峰,名词
const userName = "张三"
const userAge = 18
const isActive = true // 布尔值用is/has/can开头
// 函数:小驼峰,动词开头
function getUserInfo() {}
function calculateTotal() {}
function validateEmail() {}
// 类:大驼峰
class UserService {}
class OrderManager {}
// 常量:全大写,下划线分隔
const MAX_RETRY_COUNT = 3
const API_BASE_URL = "https://api.example.com"
// ===== 格式规范 =====
// 缩进:2空格或4空格,团队统一即可
// 行宽:80-120字符
// 空行:逻辑块之间空一行
function processOrder(order) {
// 参数校验
if (!order) {
throw new Error("Order is required")
}
// 业务逻辑
const total = calculateTotal(order)
const discount = getDiscount(order.user)
const finalPrice = total * (1 - discount)
// 返回结果
return {
orderId: order.id,
finalPrice,
}
}
2.2 第二式:函数的艺术
javascript
// 小有所成的函数修炼
// ===== 原则1:单一职责 =====
// 不好:一个函数做太多事
function processUser(user) {
// 验证
if (!user.email) throw new Error("Email required")
if (!user.name) throw new Error("Name required")
// 格式化
user.email = user.email.toLowerCase()
user.name = user.name.trim()
// 保存
database.save(user)
// 发送邮件
sendWelcomeEmail(user.email)
// 记录日志
logger.info("User created", user)
}
// 好:每个函数只做一件事
function validateUser(user) {
if (!user.email) throw new Error("Email required")
if (!user.name) throw new Error("Name required")
}
function formatUser(user) {
return {
...user,
email: user.email.toLowerCase(),
name: user.name.trim(),
}
}
function createUser(user) {
validateUser(user)
const formattedUser = formatUser(user)
database.save(formattedUser)
sendWelcomeEmail(formattedUser.email)
logger.info("User created", formattedUser)
}
// ===== 原则2:参数不要太多 =====
// 不好:参数太多
function createOrder(
userId,
productId,
quantity,
price,
discount,
address,
phone,
note
) {
// ...
}
// 好:使用对象参数
function createOrder(options) {
const { userId, productId, quantity, price, discount, address, phone, note } =
options
// ...
}
// 或者使用TypeScript定义接口
interface CreateOrderOptions {
userId: string;
productId: string;
quantity: number;
price: number;
discount?: number;
address: string;
phone: string;
note?: string;
}
function createOrder(options: CreateOrderOptions) {
// ...
}
// ===== 原则3:避免副作用 =====
// 不好:修改了传入的参数
function addItem(cart, item) {
cart.items.push(item) // 修改了原数组
cart.total += item.price // 修改了原对象
return cart
}
// 好:返回新对象
function addItem(cart, item) {
return {
...cart,
items: [...cart.items, item],
total: cart.total + item.price,
}
}
2.3 第三式:错误处理
javascript
// 小有所成的错误处理修炼
// ===== 初学乍练的错误处理 =====
function getUser(id) {
try {
return database.findUser(id)
} catch (e) {
console.log(e) // 打印一下就完事了
return null
}
}
// ===== 小有所成的错误处理 =====
// 1. 定义明确的错误类型
class ValidationError extends Error {
constructor(message) {
super(message)
this.name = "ValidationError"
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} with id ${id} not found`)
this.name = "NotFoundError"
this.resource = resource
this.id = id
}
}
class DatabaseError extends Error {
constructor(message, originalError) {
super(message)
this.name = "DatabaseError"
this.originalError = originalError
}
}
// 2. 在合适的层级处理错误
function getUser(id) {
if (!id) {
throw new ValidationError("User ID is required")
}
try {
const user = database.findUser(id)
if (!user) {
throw new NotFoundError("User", id)
}
return user
} catch (error) {
if (error instanceof NotFoundError) {
throw error // 业务错误,继续抛出
}
// 数据库错误,包装后抛出
throw new DatabaseError("Failed to fetch user", error)
}
}
// 3. 在最外层统一处理
async function handleRequest(req, res) {
try {
const user = await getUser(req.params.id)
res.json(user)
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message })
} else if (error instanceof NotFoundError) {
res.status(404).json({ error: error.message })
} else {
// 未知错误,记录日志,返回通用错误
logger.error("Unexpected error", error)
res.status(500).json({ error: "Internal server error" })
}
}
}
2.4 第四式:单元测试
javascript
// 小有所成必修:单元测试
// 被测试的函数
function calculatePrice(basePrice, quantity, discount = 0) {
if (basePrice < 0 || quantity < 0) {
throw new Error("Price and quantity must be non-negative")
}
if (discount < 0 || discount > 1) {
throw new Error("Discount must be between 0 and 1")
}
const subtotal = basePrice * quantity
const discountAmount = subtotal * discount
return subtotal - discountAmount
}
// 单元测试
describe("calculatePrice", () => {
// 正常情况
test("should calculate price without discount", () => {
expect(calculatePrice(100, 2)).toBe(200)
})
test("should calculate price with discount", () => {
expect(calculatePrice(100, 2, 0.1)).toBe(180)
})
test("should handle zero quantity", () => {
expect(calculatePrice(100, 0)).toBe(0)
})
// 边界情况
test("should handle 100% discount", () => {
expect(calculatePrice(100, 2, 1)).toBe(0)
})
// 错误情况
test("should throw error for negative price", () => {
expect(() => calculatePrice(-100, 2)).toThrow(
"Price and quantity must be non-negative"
)
})
test("should throw error for invalid discount", () => {
expect(() => calculatePrice(100, 2, 1.5)).toThrow(
"Discount must be between 0 and 1"
)
})
})
// 测试覆盖的三个维度:
// 1. 正常路径(Happy Path)
// 2. 边界条件(Edge Cases)
// 3. 错误情况(Error Cases)
2.5 第五式:设计模式入门
javascript
// 小有所成的设计模式修炼
// ===== 模式1:单例模式 =====
// 场景:全局只需要一个实例
class Logger {
static instance = null
static getInstance() {
if (!Logger.instance) {
Logger.instance = new Logger()
}
return Logger.instance
}
log(message) {
console.log(`[${new Date().toISOString()}] ${message}`)
}
}
// 使用
const logger1 = Logger.getInstance()
const logger2 = Logger.getInstance()
console.log(logger1 === logger2) // true
// ===== 模式2:工厂模式 =====
// 场景:根据条件创建不同类型的对象
class UserFactory {
static create(type, data) {
switch (type) {
case "admin":
return new AdminUser(data)
case "vip":
return new VipUser(data)
default:
return new NormalUser(data)
}
}
}
// 使用
const admin = UserFactory.create("admin", { name: "管理员" })
const vip = UserFactory.create("vip", { name: "VIP用户" })
// ===== 模式3:策略模式 =====
// 场景:根据不同情况使用不同的算法
// 不好:if-else地狱
function calculateShipping(type, weight) {
if (type === "standard") {
return weight * 5
} else if (type === "express") {
return weight * 10
} else if (type === "overnight") {
return weight * 20
}
}
// 好:策略模式
const shippingStrategies = {
standard: (weight) => weight * 5,
express: (weight) => weight * 10,
overnight: (weight) => weight * 20,
}
function calculateShipping(type, weight) {
const strategy = shippingStrategies[type]
if (!strategy) {
throw new Error(`Unknown shipping type: ${type}`)
}
return strategy(weight)
}
// ===== 模式4:观察者模式 =====
// 场景:一个对象状态变化时通知其他对象
class EventEmitter {
constructor() {
this.listeners = {}
}
on(event, callback) {
if (!this.listeners[event]) {
this.listeners[event] = []
}
this.listeners[event].push(callback)
}
off(event, callback) {
if (!this.listeners[event]) return
this.listeners[event] = this.listeners[event].filter(
(cb) => cb !== callback
)
}
emit(event, data) {
if (!this.listeners[event]) return
this.listeners[event].forEach((callback) => callback(data))
}
}
// 使用
const emitter = new EventEmitter()
emitter.on("userCreated", (user) => {
console.log("发送欢迎邮件给", user.email)
})
emitter.on("userCreated", (user) => {
console.log("记录日志", user.name)
})
emitter.emit("userCreated", { name: "张三", email: "test@example.com" })
2.6 第六式:Git 进阶
bash
# 筑基期的Git修炼
# ===== 练气期的Git =====
git add .
git commit -m "update"
git push
# 完事!
# ===== 小有所成的Git =====
# 1. 有意义的提交信息
git commit -m "feat: 添加用户注册功能"
git commit -m "fix: 修复登录页面样式错位问题"
git commit -m "refactor: 重构订单计算逻辑"
git commit -m "docs: 更新API文档"
git commit -m "test: 添加用户服务单元测试"
# 提交信息格式:<type>: <description>
# type: feat, fix, refactor, docs, test, chore, style
# 2. 分支管理
git checkout -b feature/user-registration # 功能分支
git checkout -b bugfix/login-style # 修复分支
git checkout -b hotfix/security-patch # 紧急修复
# 3. 合并前先rebase
git checkout feature/user-registration
git rebase main # 把main的最新代码合并进来
git checkout main
git merge feature/user-registration
# 4. 交互式rebase整理提交
git rebase -i HEAD~3 # 整理最近3个提交
# 可以合并、修改、删除提交
# 5. 暂存工作区
git stash # 暂存当前修改
git stash list # 查看暂存列表
git stash pop # 恢复最近的暂存
git stash drop # 删除最近的暂存
# 6. 查看历史
git log --oneline --graph # 图形化查看提交历史
git blame file.js # 查看每行代码是谁写的
git diff HEAD~1 # 查看最近一次提交的改动
第三章:小有所成的常见瓶颈
3.1 过度设计
javascript
// 症状:简单问题复杂化
// 需求:计算两个数的和
// 过度设计版本
class Calculator {
constructor(strategy) {
this.strategy = strategy
}
setStrategy(strategy) {
this.strategy = strategy
}
execute(a, b) {
return this.strategy.calculate(a, b)
}
}
class AddStrategy {
calculate(a, b) {
return a + b
}
}
class CalculatorFactory {
static create(type) {
switch (type) {
case "add":
return new Calculator(new AddStrategy())
// ...更多策略
}
}
}
// 使用
const calculator = CalculatorFactory.create("add")
const result = calculator.execute(1, 2)
// 正常版本
function add(a, b) {
return a + b
}
const result = add(1, 2)
// 教训:不要为了用设计模式而用设计模式
// YAGNI原则:You Ain't Gonna Need It
3.2 测试覆盖率焦虑
javascript
// 症状:追求100%测试覆盖率
// 被测试的代码
function greet(name) {
return `Hello, ${name}!`
}
// 过度测试
describe("greet", () => {
test("should greet with name", () => {
expect(greet("World")).toBe("Hello, World!")
})
test("should greet with different name", () => {
expect(greet("Alice")).toBe("Hello, Alice!")
})
test("should greet with another name", () => {
expect(greet("Bob")).toBe("Hello, Bob!")
})
test("should greet with Chinese name", () => {
expect(greet("张三")).toBe("Hello, 张三!")
})
// 这些测试本质上是一样的,没有额外价值
})
// 合理的测试
describe("greet", () => {
test("should return greeting with provided name", () => {
expect(greet("World")).toBe("Hello, World!")
})
test("should handle empty name", () => {
expect(greet("")).toBe("Hello, !")
})
// 测试有意义的边界情况,而不是重复测试同样的逻辑
})
3.3 规范教条主义
javascript
// 症状:死守规范,不知变通
// 规范说:函数不超过20行
// 于是...
function processOrder_part1(order) {
// 第1-20行
}
function processOrder_part2(order) {
// 第21-40行
}
function processOrder_part3(order) {
// 第41-60行
}
function processOrder(order) {
processOrder_part1(order)
processOrder_part2(order)
processOrder_part3(order)
}
// 这样拆分毫无意义,反而更难理解
// 正确理解:规范是指导,不是法律
// 如果一个函数逻辑上是一个整体,50行也可以接受
// 关键是:代码是否清晰、是否容易理解
第四章:小有所成的突破契机
4.1 第一次重构遗留代码
javascript
// 你接手的代码
function process(d) {
var r = []
for (var i = 0; i < d.length; i++) {
if (d[i].t == 1) {
if (d[i].s == "a") {
r.push({ n: d[i].n, v: d[i].v * 1.1 })
} else {
r.push({ n: d[i].n, v: d[i].v })
}
} else if (d[i].t == 2) {
if (d[i].s == "a") {
r.push({ n: d[i].n, v: d[i].v * 1.2 })
} else {
r.push({ n: d[i].n, v: d[i].v * 1.05 })
}
}
}
return r
}
// 你的重构
const MULTIPLIERS = {
1: { a: 1.1, default: 1 },
2: { a: 1.2, default: 1.05 },
}
function calculateValue(item) {
const typeMultipliers = MULTIPLIERS[item.type]
if (!typeMultipliers) return item.value
const multiplier = typeMultipliers[item.status] || typeMultipliers.default
return item.value * multiplier
}
function processItems(items) {
return items
.filter((item) => MULTIPLIERS[item.type])
.map((item) => ({
name: item.name,
value: calculateValue(item),
}))
}
// 重构的收获:
// 1. 理解了代码的业务逻辑
// 2. 学会了如何让代码更清晰
// 3. 体会到了好代码和烂代码的区别
4.2 第一次 Code Review 别人
javascript
// 你Review的代码
function getUserData(userId) {
return fetch("/api/users/" + userId)
.then((res) => res.json())
.then((data) => {
return data
})
.catch((err) => {
console.log(err)
})
}
// 你的Review意见
/*
* 1. 使用模板字符串代替字符串拼接
* 2. .then(data => { return data }) 可以简化
* 3. 错误处理不完整,catch后应该重新抛出或返回默认值
* 4. 建议使用async/await提高可读性
*
* 建议修改为:
*/
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
} catch (error) {
console.error("Failed to fetch user:", error)
throw error // 让调用者决定如何处理
}
}
// Review别人代码的收获:
// 1. 巩固了自己的知识
// 2. 学会了从不同角度看问题
// 3. 提高了代码审美
4.3 第一次性能优化
javascript
// 问题:页面加载很慢
// 原始代码
function renderUserList(users) {
const container = document.getElementById("user-list")
users.forEach((user) => {
const div = document.createElement("div")
div.innerHTML = `
<h3>${user.name}</h3>
<p>${user.email}</p>
`
container.appendChild(div) // 每次都触发重排
})
}
// 优化后
function renderUserList(users) {
const container = document.getElementById("user-list")
const fragment = document.createDocumentFragment()
users.forEach((user) => {
const div = document.createElement("div")
div.innerHTML = `
<h3>${user.name}</h3>
<p>${user.email}</p>
`
fragment.appendChild(div) // 先添加到fragment
})
container.appendChild(fragment) // 一次性添加到DOM
}
// 进一步优化:虚拟列表
function renderUserListVirtual(users, visibleCount = 20) {
// 只渲染可见区域的元素
// 滚动时动态更新
// ...
}
// 性能优化的收获:
// 1. 理解了浏览器渲染原理
// 2. 学会了性能分析工具
// 3. 知道了"过早优化是万恶之源",但也知道了什么时候该优化
第五章:小有所成的修炼心法
5.1 心法一:代码是负债,不是资产
javascript
// 初学乍练的认知
// "我写了1000行代码,好有成就感!"
// 筑基期的认知
// "代码越少越好,能用100行解决的问题,不要写200行"
// 实践
// 不好:代码很多,但很多是重复的
function validateName(name) {
if (!name) return false
if (name.length < 2) return false
if (name.length > 50) return false
return true
}
function validateEmail(email) {
if (!email) return false
if (!email.includes("@")) return false
if (email.length > 100) return false
return true
}
function validatePhone(phone) {
if (!phone) return false
if (phone.length !== 11) return false
if (!/^\d+$/.test(phone)) return false
return true
}
// 好:抽象公共逻辑
const validators = {
name: [
{ check: (v) => !!v, message: "Name is required" },
{ check: (v) => v.length >= 2, message: "Name too short" },
{ check: (v) => v.length <= 50, message: "Name too long" },
],
email: [
{ check: (v) => !!v, message: "Email is required" },
{ check: (v) => v.includes("@"), message: "Invalid email" },
{ check: (v) => v.length <= 100, message: "Email too long" },
],
phone: [
{ check: (v) => !!v, message: "Phone is required" },
{ check: (v) => v.length === 11, message: "Phone must be 11 digits" },
{ check: (v) => /^\d+$/.test(v), message: "Phone must be numeric" },
],
}
function validate(field, value) {
const rules = validators[field]
for (const rule of rules) {
if (!rule.check(value)) {
return { valid: false, message: rule.message }
}
}
return { valid: true }
}
5.2 心法二:可读性优于性能(大多数时候)
javascript
// 初学乍练的认知
// "这样写性能更好!"
// 筑基期的认知
// "先让代码可读,性能问题等出现了再优化"
// 例子:找出数组中的最大值
// "高性能"版本
function findMax(arr) {
let max = arr[0]
for (let i = 1, len = arr.length; i < len; i++) {
if (arr[i] > max) max = arr[i]
}
return max
}
// 可读版本
function findMax(arr) {
return Math.max(...arr)
}
// 除非你处理的是百万级数据,否则可读性更重要
// 过早优化是万恶之源 ------ Donald Knuth
5.3 心法三:写代码前先想清楚
javascript
// 初学乍练的做法
// 想到哪写到哪,边写边改
// 筑基期的做法
// 先设计,再编码
// 设计步骤:
// 1. 明确需求:这个功能要做什么?
// 2. 定义接口:输入是什么?输出是什么?
// 3. 考虑边界:有哪些特殊情况?
// 4. 拆分任务:可以分成哪几个步骤?
// 5. 开始编码
// 例子:实现一个分页函数
// 1. 明确需求
// 输入:数组、页码、每页数量
// 输出:当前页的数据、总页数、是否有上/下一页
// 2. 定义接口
interface PaginationResult<T> {
data: T[];
currentPage: number;
totalPages: number;
hasNextPage: boolean;
hasPrevPage: boolean;
}
function paginate<T>(
items: T[],
page: number,
pageSize: number
): PaginationResult<T>;
// 3. 考虑边界
// - 空数组
// - 页码小于1
// - 页码大于总页数
// - pageSize为0或负数
// 4. 拆分任务
// - 参数校验
// - 计算总页数
// - 截取当前页数据
// - 组装返回结果
// 5. 开始编码
function paginate<T>(
items: T[],
page: number = 1,
pageSize: number = 10
): PaginationResult<T> {
// 参数校验
if (pageSize <= 0) pageSize = 10;
if (page < 1) page = 1;
// 计算总页数
const totalPages = Math.ceil(items.length / pageSize);
// 修正页码
if (page > totalPages && totalPages > 0) page = totalPages;
// 截取当前页数据
const start = (page - 1) * pageSize;
const data = items.slice(start, start + pageSize);
// 组装返回结果
return {
data,
currentPage: page,
totalPages,
hasNextPage: page < totalPages,
hasPrevPage: page > 1,
};
}
第六章:小有所成的出师考核
6.1 自测题
javascript
// 问题1:重构这段代码
function calc(t, a) {
if (t == "add") {
return a[0] + a[1]
} else if (t == "sub") {
return a[0] - a[1]
} else if (t == "mul") {
return a[0] * a[1]
} else if (t == "div") {
if (a[1] == 0) {
return "error"
}
return a[0] / a[1]
}
}
// 问题2:这段代码有什么问题?如何改进?
async function getUsers() {
const users = await fetch("/api/users").then((r) => r.json())
const result = []
for (const user of users) {
const orders = await fetch(`/api/orders?userId=${user.id}`).then((r) =>
r.json()
)
result.push({ ...user, orders })
}
return result
}
// 问题3:为这个函数写单元测试
function formatDate(date, format = "YYYY-MM-DD") {
const d = new Date(date)
if (isNaN(d.getTime())) {
throw new Error("Invalid date")
}
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, "0")
const day = String(d.getDate()).padStart(2, "0")
return format.replace("YYYY", year).replace("MM", month).replace("DD", day)
}
6.2 出师标准
css
┌─────────────────────────────────────────────────────────────┐
│ 小有所成出师标准 ✓ │
├─────────────────────────────────────────────────────────────┤
│ │
│ □ 代码符合团队规范,通过Code Review │
│ □ 能写出清晰、可维护的代码 │
│ □ 会写单元测试,理解测试的价值 │
│ □ 熟练使用Git进行版本控制 │
│ □ 了解常用设计模式,知道什么时候该用 │
│ □ 能独立完成中等复杂度的功能开发 │
│ □ 能重构遗留代码,提高代码质量 │
│ □ 开始关注性能,但不过早优化 │
│ │
└─────────────────────────────────────────────────────────────┘
结语:小有所成的意义
小有所成是程序员从"会写代码"到"会写好代码"的关键阶段。
在这个阶段,你会:
- 开始关注代码质量,而不只是"能跑"
- 学会用规范和模式来组织代码
- 理解测试的价值
- 开始有代码审美
小有所成的核心是:建立正确的编程习惯和思维方式。
这些习惯和思维方式,将成为你未来成长的基础。
就像武侠小说里,内功心法比招式更重要。一个内功深厚的人,学什么招式都快;一个只会花拳绣腿的人,永远只能停留在表面。
下一篇,我们将进入融会贯通------当你开始思考系统架构、开始能够独当一面的时候,你就踏入了一流高手的大门。
预告:融会贯通
在融会贯通阶段,你将学习:
- 系统设计与架构
- 技术选型与权衡
- 团队协作与沟通
- 项目管理基础
- 如何成为技术骨干
敬请期待!
本文是《程序员武学修炼手册》系列的第二篇。
如果你正处于小有所成阶段,恭喜你已经超越了大多数"能跑就行"的程序员。
继续修炼,一流高手在向你招手! 💪