跳到主要内容
预计阅读 40 分钟

架构模式与反模式 —— MVC/MVVM、微服务模式、常见反模式识别

设计模式解决的是类和对象层面的问题,架构模式解决的是系统层面的问题。它们是不同尺度上的”可复用解决方案”。同时,认识”反模式”——那些看起来像好办法但实际上会把你引入泥潭的做法——与学习正面模式同样重要。

📋 开篇自测:你已经知道多少?

  1. MVC 和 MVVM 的核心区别是什么?Vue 属于哪种模式?
  2. 微服务架构中的”API Gateway”解决什么问题?它和外观模式有什么关系?
  3. 你能说出三个”代码异味”(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 对比

维度MVCMVVM
View 更新方式Controller 手动通知 View数据绑定自动更新
View 和 Model 的关系Controller 做中间人ViewModel 做数据映射层
代码复杂度Controller 容易膨胀ViewModel 容易膨胀
适合场景后端渲染、简单交互前端 SPA、复杂交互
代表框架Express、Spring MVCVue、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 行了
}

解决方案:按职责拆分为 UserServiceOrderServicePaymentServiceNotificationService

反模式二:魔法数字/字符串(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——知道什么不该做和知道什么该做同样重要。

我们已经从具体的设计模式,到前端实战模式,再到架构模式和反模式,逐步拉高了视野。但有一个最关键的问题还没有回答:这些模式,什么时候该用?什么时候不该用? 下一章是本课程的收官之作——我们不学新模式,而是学”判断力”。何时出手、何时收手,以及现代语言特性如何改变模式的实现方式。这才是从”会用”到”精通”的最后一步。


📝 结尾自测

  1. MVC 中 Controller 的职责是什么?为什么它容易变成”上帝对象”?
  2. MVVM 的”双向数据绑定”解决了什么问题?它的代价是什么?
  3. 微服务中的熔断器有哪三种状态?状态之间如何转换?
  4. Saga 模式中”补偿操作”的含义是什么?为什么补偿要按逆序执行?
  5. 请举出一个你认为”过度使用设计模式”的反面案例,并说明怎样简化更好。

购买课程解锁全部内容

写出优雅代码:10 章掌握 23 种设计模式

¥29.90