架构模式与反模式 —— MVC/MVVM、微服务模式、常见反模式识别
设计模式解决的是类和对象层面的问题,架构模式解决的是系统层面的问题。它们是不同尺度上的”可复用解决方案”。同时,认识”反模式”——那些看起来像好办法但实际上会把你引入泥潭的做法——与学习正面模式同样重要。
📋 开篇自测:你已经知道多少?
- MVC 和 MVVM 的核心区别是什么?Vue 属于哪种模式?
- 微服务架构中的”API Gateway”解决什么问题?它和外观模式有什么关系?
- 你能说出三个”代码异味”(Code Smell)的名字吗?
一、MVC:经典的三层分离
1.1 MVC 的基本结构
MVC(Model-View-Controller)可能是最广为人知的架构模式了。它将应用分成三个职责分明的层:
- Model:数据和业务逻辑。不知道界面长什么样。
- View:呈现层。从 Model 获取数据进行展示,不包含业务逻辑。
- Controller:协调者。接收用户输入,调用 Model 处理业务,选择合适的 View 展示结果。
// ===== Model 层 =====
interface TodoItem {
id: string;
text: string;
completed: boolean;
createdAt: Date;
}
class TodoModel {
private todos: TodoItem[] = [];
private listeners: Array<() => void> = [];
addTodo(text: string): TodoItem {
const todo: TodoItem = {
id: crypto.randomUUID(), // 注意:浏览器中需要安全上下文(HTTPS 或 localhost)
text,
completed: false,
createdAt: new Date(),
};
this.todos.push(todo);
this.notify();
return todo;
}
toggleTodo(id: string): void {
const todo = this.todos.find(t => t.id === id);
if (todo) {
todo.completed = !todo.completed;
this.notify();
}
}
removeTodo(id: string): void {
this.todos = this.todos.filter(t => t.id !== id);
this.notify();
}
getTodos(filter?: 'all' | 'active' | 'completed'): TodoItem[] {
switch (filter) {
case 'active': return this.todos.filter(t => !t.completed);
case 'completed': return this.todos.filter(t => t.completed);
default: return [...this.todos];
}
}
onChange(listener: () => void): void {
this.listeners.push(listener);
}
private notify(): void {
this.listeners.forEach(fn => fn());
}
}
// ===== View 层 =====
class TodoView {
renderList(todos: TodoItem[]): string {
if (todos.length === 0) return '<p>暂无待办事项</p>';
const items = todos.map(todo => {
const status = todo.completed ? '✅' : '⬜';
const textStyle = todo.completed ? 'text-decoration: line-through; color: #999;' : '';
return `<li data-id="${todo.id}" style="${textStyle}">${status} ${todo.text}</li>`;
}).join('');
return `<ul>${items}</ul>`;
}
renderStats(total: number, completed: number): string {
return `<p>共 ${total} 项,已完成 ${completed} 项,剩余 ${total - completed} 项</p>`;
}
}
// ===== Controller 层 =====
class TodoController {
constructor(
private model: TodoModel,
private view: TodoView
) {
// 数据变化时自动重新渲染
this.model.onChange(() => this.render());
}
handleAddTodo(text: string): void {
if (!text.trim()) {
console.log('待办事项不能为空');
return;
}
this.model.addTodo(text.trim());
}
handleToggleTodo(id: string): void {
this.model.toggleTodo(id);
}
handleRemoveTodo(id: string): void {
this.model.removeTodo(id);
}
render(): void {
const todos = this.model.getTodos();
const completed = todos.filter(t => t.completed).length;
console.log(this.view.renderList(todos));
console.log(this.view.renderStats(todos.length, completed));
}
}
1.2 MVC 的变体和局限
经典 MVC 有一个问题:在复杂应用中,Controller 容易变成”上帝对象”——它要处理用户输入、协调 Model、管理 View,职责越来越重。这催生了多种变体:
- MVP(Model-View-Presenter):View 通过接口与 Presenter 通信,Presenter 完全掌控视图更新。
- MVVM(Model-View-ViewModel):用数据绑定取代手动的 View 更新,ViewModel 暴露响应式数据供 View 绑定。
二、MVVM:数据驱动的界面
2.1 MVVM 的核心思想
MVVM 的革命性在于引入了双向数据绑定:ViewModel 中的数据变化会自动反映到 View 上,View 上的用户输入也会自动同步到 ViewModel。开发者只需要关心数据,不需要手动操作 DOM。
Vue 是 MVVM 模式最典型的前端实现。
// 用 TypeScript 模拟 MVVM 的核心机制
// 响应式数据(简化版,模拟 Vue 的 reactive)
type ChangeCallback = () => void;
class Reactive<T extends Record<string, any>> {
private watchers: Map<string, Set<ChangeCallback>> = new Map();
private data: T;
constructor(initialData: T) {
this.data = { ...initialData };
}
get<K extends keyof T>(key: K): T[K] {
return this.data[key];
}
set<K extends keyof T>(key: K, value: T[K]): void {
if (this.data[key] === value) return;
this.data[key] = value;
this.notifyWatchers(key as string);
}
watch(key: string, callback: ChangeCallback): void {
if (!this.watchers.has(key)) {
this.watchers.set(key, new Set());
}
this.watchers.get(key)!.add(callback);
}
private notifyWatchers(key: string): void {
this.watchers.get(key)?.forEach(cb => cb());
this.watchers.get('*')?.forEach(cb => cb()); // 全局监听
}
}
// ViewModel
class SearchViewModel {
state: Reactive<{
keyword: string;
results: string[];
loading: boolean;
errorMessage: string;
}>;
constructor(private searchService: SearchAPI) {
this.state = new Reactive({
keyword: '',
results: [],
loading: false,
errorMessage: '',
});
}
setKeyword(keyword: string): void {
this.state.set('keyword', keyword);
}
async search(): Promise<void> {
const keyword = this.state.get('keyword');
if (!keyword.trim()) return;
this.state.set('loading', true);
this.state.set('errorMessage', '');
try {
const results = await this.searchService.search(keyword);
this.state.set('results', results);
} catch (err) {
this.state.set('errorMessage', '搜索失败,请重试');
} finally {
this.state.set('loading', false);
}
}
}
interface SearchAPI {
search(keyword: string): Promise<string[]>;
}
2.2 MVC vs MVVM 对比
| 维度 | MVC | MVVM |
|---|---|---|
| View 更新方式 | Controller 手动通知 View | 数据绑定自动更新 |
| View 和 Model 的关系 | Controller 做中间人 | ViewModel 做数据映射层 |
| 代码复杂度 | Controller 容易膨胀 | ViewModel 容易膨胀 |
| 适合场景 | 后端渲染、简单交互 | 前端 SPA、复杂交互 |
| 代表框架 | Express、Spring MVC | Vue、Angular、WPF |
三、微服务中的设计模式
当系统大到一个单体应用无法承载时,微服务架构将系统拆分成多个独立部署的小服务。在这个层面上,设计模式以新的形式出现。
3.1 API Gateway(API 网关)—— 系统级外观模式
// API 网关:为前端提供统一入口
class APIGateway {
private services: Map<string, string> = new Map([
['user', 'http://user-service:3001'],
['order', 'http://order-service:3002'],
['product', 'http://product-service:3003'],
['payment', 'http://payment-service:3004'],
]);
async handleRequest(path: string, method: string, body?: any): Promise<any> {
const [, serviceName, ...rest] = path.split('/');
const serviceUrl = this.services.get(serviceName);
if (!serviceUrl) {
return { status: 404, body: { error: '服务不存在' } };
}
const targetPath = '/' + rest.join('/');
console.log(`网关路由: ${method} ${path} → ${serviceUrl}${targetPath}`);
// 实际发起对内部服务的请求
return this.forwardRequest(serviceUrl + targetPath, method, body);
}
// 聚合查询:一个前端请求聚合多个微服务的数据
async getUserDashboard(userId: string): Promise<any> {
const [userInfo, recentOrders, recommendations] = await Promise.all([
this.forwardRequest(`${this.services.get('user')}/${userId}`, 'GET'),
this.forwardRequest(`${this.services.get('order')}/user/${userId}/recent`, 'GET'),
this.forwardRequest(`${this.services.get('product')}/recommendations/${userId}`, 'GET'),
]);
return {
user: userInfo,
recentOrders,
recommendations,
};
}
private async forwardRequest(url: string, method: string, body?: any): Promise<any> {
console.log(`转发请求: ${method} ${url}`);
return { status: 200, data: 'mock response' };
}
}
3.2 Circuit Breaker(熔断器)—— 系统级代理模式
当下游服务出现故障时,熔断器会暂时”断开”调用,快速返回降级响应,防止故障扩散。
enum CircuitState {
CLOSED = 'CLOSED', // 正常通行
OPEN = 'OPEN', // 熔断,直接拒绝
HALF_OPEN = 'HALF_OPEN' // 半开,试探性放行
}
class CircuitBreaker {
private state = CircuitState.CLOSED;
private failureCount = 0;
private lastFailureTime = 0;
private successCount = 0;
constructor(
private failureThreshold: number = 5, // 失败多少次触发熔断
private recoveryTimeMs: number = 30000, // 熔断持续时间
private halfOpenMaxAttempts: number = 3 // 半开状态的试探次数
) {}
async execute<T>(operation: () => Promise<T>, fallback: () => T): Promise<T> {
if (this.state === CircuitState.OPEN) {
if (Date.now() - this.lastFailureTime > this.recoveryTimeMs) {
console.log('[熔断器] 进入半开状态,允许试探请求');
this.state = CircuitState.HALF_OPEN;
this.successCount = 0;
} else {
console.log('[熔断器] 断路中,返回降级响应');
return fallback();
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
console.log(`[熔断器] 请求失败 (${this.failureCount}/${this.failureThreshold})`);
return fallback();
}
}
private onSuccess(): void {
if (this.state === CircuitState.HALF_OPEN) {
this.successCount++;
if (this.successCount >= this.halfOpenMaxAttempts) {
console.log('[熔断器] 恢复正常');
this.state = CircuitState.CLOSED;
this.failureCount = 0;
}
} else {
this.failureCount = 0;
}
}
private onFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= this.failureThreshold) {
console.log('[熔断器] 触发熔断!');
this.state = CircuitState.OPEN;
}
}
getState(): CircuitState { return this.state; }
}
使用:
const breaker = new CircuitBreaker(3, 10000);
async function callPaymentService(orderId: string): Promise<string> {
return breaker.execute(
// 正常操作
async () => {
const result = await fetch(`http://payment-service/pay/${orderId}`);
if (!result.ok) throw new Error('支付服务异常');
return '支付成功';
},
// 降级响应
() => '支付服务暂时不可用,订单已加入队列稍后重试'
);
}
3.3 Saga 模式 —— 分布式事务的编排
在微服务中,一个业务操作可能跨越多个服务。Saga 模式将长事务分解为一系列本地事务,每个本地事务都有对应的补偿操作:
interface SagaStep<T> {
name: string;
execute: (context: T) => Promise<void>;
compensate: (context: T) => Promise<void>;
}
class SagaOrchestrator<T> {
private steps: SagaStep<T>[] = [];
addStep(step: SagaStep<T>): this {
this.steps.push(step);
return this;
}
async run(context: T): Promise<void> {
const completedSteps: SagaStep<T>[] = [];
for (const step of this.steps) {
try {
console.log(`执行步骤: ${step.name}`);
await step.execute(context);
completedSteps.push(step);
} catch (error) {
console.error(`步骤 "${step.name}" 失败:`, error);
console.log('开始补偿回滚...');
// 按逆序补偿已完成的步骤
for (const completedStep of completedSteps.reverse()) {
try {
console.log(`补偿: ${completedStep.name}`);
await completedStep.compensate(context);
} catch (compensateError) {
console.error(`补偿 "${completedStep.name}" 失败:`, compensateError);
}
}
throw new Error(`Saga 失败于步骤 "${step.name}",已回滚`);
}
}
console.log('Saga 执行完成');
}
}
使用:
interface OrderContext {
orderId: string;
userId: string;
productId: string;
amount: number;
}
const orderSaga = new SagaOrchestrator<OrderContext>()
.addStep({
name: '扣减库存',
execute: async (ctx) => { console.log(`库存 -1: ${ctx.productId}`); },
compensate: async (ctx) => { console.log(`库存 +1: ${ctx.productId}`); },
})
.addStep({
name: '创建订单',
execute: async (ctx) => { console.log(`创建订单: ${ctx.orderId}`); },
compensate: async (ctx) => { console.log(`取消订单: ${ctx.orderId}`); },
})
.addStep({
name: '扣款',
execute: async (ctx) => { console.log(`扣款 ¥${ctx.amount} from ${ctx.userId}`); },
compensate: async (ctx) => { console.log(`退款 ¥${ctx.amount} to ${ctx.userId}`); },
});
await orderSaga.run({
orderId: 'ORD-001',
userId: 'U-001',
productId: 'P-100',
amount: 299,
});
四、常见反模式识别
4.1 什么是反模式
反模式(Anti-Pattern)是那些看起来像是好主意,但实际上会带来更多问题的做法。识别反模式和学习正面模式一样重要。
4.2 前端开发中的高频反模式
反模式一:上帝对象(God Object)
一个类或模块承担了太多职责,成了无所不能的”上帝”。
// 反例:一个 Service 管理所有事务
class AppService {
// 用户管理
async login(username: string, password: string) { /* 200 行 */ }
async register(data: any) { /* 150 行 */ }
async updateProfile(data: any) { /* 100 行 */ }
// 订单管理
async createOrder(data: any) { /* 180 行 */ }
async cancelOrder(id: string) { /* 80 行 */ }
// 支付
async processPayment(data: any) { /* 120 行 */ }
// 通知
async sendNotification(data: any) { /* 90 行 */ }
// ... 文件已经 3000 行了
}
解决方案:按职责拆分为 UserService、OrderService、PaymentService、NotificationService。
反模式二:魔法数字/字符串(Magic Numbers/Strings)
// 反例
if (user.role === 3) { /* 3 是什么?管理员?VIP? */ }
if (retryCount > 5) { /* 为什么是 5? */ }
setTimeout(fetchData, 86400000); // 这是多久?
// 正例
const UserRole = { VIEWER: 1, EDITOR: 2, ADMIN: 3 } as const;
const MAX_RETRY_COUNT = 5;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
if (user.role === UserRole.ADMIN) { /* 清晰 */ }
if (retryCount > MAX_RETRY_COUNT) { /* 明确 */ }
setTimeout(fetchData, ONE_DAY_MS); // 一天
反模式三:useEffect 滥用(Effect Spaghetti)
在 React 中,useEffect 的滥用是最常见的现代反模式之一——把本应是事件处理或派生计算的逻辑塞进 effect,导致组件行为难以预测。
// 反例:用 useEffect 同步派生状态
function ProductList({ category }: { category: string }) {
const [products, setProducts] = useState<Product[]>([]);
const [filtered, setFiltered] = useState<Product[]>([]);
const [sortedAndFiltered, setSortedAndFiltered] = useState<Product[]>([]);
useEffect(() => {
fetchProducts(category).then(setProducts);
}, [category]);
useEffect(() => {
setFiltered(products.filter(p => p.inStock));
}, [products]); // 链式 effect:products 变 → filtered 变 → 再触发下一个 effect
useEffect(() => {
setSortedAndFiltered(filtered.sort((a, b) => a.price - b.price));
}, [filtered]); // 三层 effect 瀑布,每层都触发一次重渲染
}
// 正例:数据获取用 effect,派生计算用 useMemo
function ProductList({ category }: { category: string }) {
const [products, setProducts] = useState<Product[]>([]);
useEffect(() => {
fetchProducts(category).then(setProducts);
}, [category]);
// 派生状态直接计算,不需要 effect
const sortedInStock = useMemo(
() => products.filter(p => p.inStock).toSorted((a, b) => a.price - b.price),
[products]
);
}
反模式四:过早优化(Premature Optimization)
// 反例:还没有性能问题就上复杂的缓存
class UserService {
private cache = new LRUCache<string, User>({ max: 10000, ttl: 300000 });
private bloomFilter = new BloomFilter(10000, 0.01);
// ... 花了 200 行实现一个其实每秒只调用 3 次的查询
}
// 正例:先写简单的版本,有性能瓶颈时再优化
class UserService {
async getUser(id: string): Promise<User> {
return this.db.query(`SELECT * FROM users WHERE id = $1`, [id]);
}
}
反模式五:Prop Drilling(属性逐层传递)
在 React 中常见:一个状态需要在深层子组件中使用,不得不一层层通过 props 传下去。
// 反例:theme 从 App 传到 Button 要经过 4 层
function App() {
return <Page theme="dark" />; // 第 1 层
}
function Page({ theme }) {
return <Section theme={theme} />; // 第 2 层
}
function Section({ theme }) {
return <Card theme={theme} />; // 第 3 层
}
function Card({ theme }) {
return <Button theme={theme} />; // 第 4 层
}
// 正例:使用 Context(依赖注入思想)
const ThemeContext = createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Page />
</ThemeContext.Provider>
);
}
function Button() {
const theme = useContext(ThemeContext); // 直接获取,不用逐层传递
}
🤔 想一想 有人说”设计模式的过度使用本身就是一种反模式”。你同意吗?你见过哪些”为了用模式而用模式”的例子?
在你的日常开发中,遇到最多的反模式是哪一个?你是怎么识别并解决的?
五、本章小结
本章从系统架构层面探讨了三个主题:
- MVC/MVVM:经典的界面分层架构。MVC 适合后端和简单前端,MVVM 适合数据驱动的 SPA。
- 微服务模式:API 网关(外观模式的系统级版本)、熔断器(代理模式的弹性增强)、Saga(分布式事务编排)。设计模式在不同尺度上重现。
- 反模式识别:上帝对象、魔法数字、useEffect 滥用、过早优化、Prop Drilling——知道什么不该做和知道什么该做同样重要。
我们已经从具体的设计模式,到前端实战模式,再到架构模式和反模式,逐步拉高了视野。但有一个最关键的问题还没有回答:这些模式,什么时候该用?什么时候不该用? 下一章是本课程的收官之作——我们不学新模式,而是学”判断力”。何时出手、何时收手,以及现代语言特性如何改变模式的实现方式。这才是从”会用”到”精通”的最后一步。
📝 结尾自测
- MVC 中 Controller 的职责是什么?为什么它容易变成”上帝对象”?
- MVVM 的”双向数据绑定”解决了什么问题?它的代价是什么?
- 微服务中的熔断器有哪三种状态?状态之间如何转换?
- Saga 模式中”补偿操作”的含义是什么?为什么补偿要按逆序执行?
- 请举出一个你认为”过度使用设计模式”的反面案例,并说明怎样简化更好。
购买课程解锁全部内容
写出优雅代码:10 章掌握 23 种设计模式
¥29.90