9.6 反模式与代码异味

> **本节目标**:识别 RML 项目中的常见反模式,给出诊断信号与重构处方。

9.6.1 反模式 1:胖 ViewModel(God ViewModel)

症状

- 一个 ViewModel 超过 500 行 - 持有 20+ 字段,覆盖多个不相关领域 - 命令方法名出现领域前缀(`user_load`、`cart_add`、`settings_save`)

危害

- 任何字段变化触发整个视图重渲染 - 修改一处牵动测试全量 - 多人协作冲突频繁

处方:按职责拆分

// ❌ 胖 ViewModel
#[derive(IModel)]
pub struct AppViewModel {
    pub user: User,
    pub todos: Vec<Todo>,
    pub cart: Cart,
    pub settings: Settings,
    pub notifications: Vec<Notification>,
    // ... 30 个字段
}

// ✅ 拆分为多个 ViewModel,每个视图一个
#[derive(IModel)] pub struct ProfileViewModel { pub user: User, ... }
#[derive(IModel)] pub struct TodoListViewModel { pub todos: Vec<Todo>, ... }
#[derive(IModel)] pub struct CartViewModel { pub cart: Cart, ... }

跨视图共享的状态用 Context / 全局 Model,而非塞进一个 ViewModel。

9.6.2 反模式 2:上帝组件(God component)

症状

- 一个组件接受 30+ props - 组件内含大量 `if={...}` 分支决定渲染哪种形态 - 组件被用于完全不相关的场景

危害

- 复用性归零:每次使用都要传一堆无关 props - 测试组合爆炸 - 修改一处影响所有使用方

处方:拆分为职责单一的组件

<!-- ❌ 上帝组件 -->
<Card variant="user" user="{user}" showAvatar="true" showActions="true" ... />

<!-- ✅ 拆分 -->
<UserCard user="{user}" />
<ProductCard product="{product}" />
<NotificationCard notification="{notification}" />

共用部分抽为更基础的 `<Card>` 容器组件,由具体组件组合使用。

9.6.3 反模式 3:绑定爆炸(Binding explosion)

症状

- 模板中出现 5 层以上的属性访问:`{user.team.leader.projects[0].name}` - 一个插值表达式包含多个函数调用:`{format(parse(user.date), "YYYY")}` - 计算属性依赖 10+ 字段

危害

- 绑定路径变长,性能下降 - 任何中间字段变化都触发重渲染 - 模板可读性极差

处方:在 ViewModel 中收敛

// ✅ ViewModel 提供扁平的计算属性
#[computed]
pub fn current_project_name(&self) -> SharedString {
    self.user.team.leader.projects.first()
        .map(|p| p.name.clone())
        .unwrap_or_default()
}

#[computed]
pub fn formatted_date(&self) -> SharedString {
    format_date(&self.user.date, "YYYY").into()
}
<!-- ✅ 模板只读扁平字段 -->
<span>{current_project_name}</span>
<span>{formatted_date}</span>

**准则**:模板中的绑定路径不超过 2 层,复杂逻辑收敛到 `#[computed]`。

9.6.4 反模式 4:深嵌套(Deep nesting)

症状

- `.rml` 中 `div` 嵌套超过 6 层 - 每层都是为了套样式或布局

危害

- 可读性差,难以修改 - 性能损耗:每层都是 GPUI 元素 - 样式继承混乱

处方:抽组件 + 用样式组合

<!-- ❌ 深嵌套 -->
<div class="card">
  <div class="card-body">
    <div class="row">
      <div class="col">
        <div class="user-info">
          <div class="user-name">{name}</div>
        </div>
      </div>
    </div>
  </div>
</div>

<!-- ✅ 抽组件 -->
<Card>
  <UserInfo user="{user}" />
</Card>

9.6.5 反模式 5:隐式状态(Implicit state)

症状

- 用 `bool` 组合表达状态机:`is_loading && !error && has_data` - 状态字段散落在多个 ViewModel - 同一逻辑状态有多种字段组合表示

危害

- 状态机不闭合,出现“既 loading 又 error”的非法态 - 修改时漏改某个 bool,UI 卡死

处方:用枚举显式建模状态

// ❌ 隐式状态
#[derive(IModel)]
pub struct BadVM {
    pub is_loading: bool,
    pub is_error: bool,
    pub is_empty: bool,
    pub data: Vec<Item>,
}

// ✅ 显式状态机
#[derive(Model, Clone)]
pub enum LoadState {
    Idle,
    Loading,
    Loaded(Vec<Item>),
    Error(SharedString),
}

#[derive(IModel)]
pub struct GoodVM {
    pub state: LoadState,
}

impl GoodVM {
    #[computed]
    pub fn is_loading(&self) -> bool { matches!(self.state, LoadState::Loading) }
    #[computed]
    pub fn data(&self) -> &[Item] {
        match &self.state { LoadState::Loaded(d) => d, _ => &[] }
    }
}

9.6.6 反模式 6:命令里构造 UI

症状

#[command]
pub fn show_detail(&mut self, cx: &mut ViewContext<Self>) {
    let panel = div().child(Label::new("...")); // ❌ 在 .rml.rs 构造 GPUI 树
    cx.add_child(panel);
}

危害

- 破坏关注点分离 - 热重载失效 - 无法被设计师修改

处方:用状态控制模板

#[command]
pub fn show_detail(&mut self, cx: &mut ViewContext<Self>) {
    self.is_detail_visible = true; // ✅ 只改状态
    cx.notify();
}
<!-- 模板根据状态渲染 -->
<Detail if={is_detail_visible} data={selected} />

9.6.7 反模式 7:跨视图直接修改

症状

// 视图 A 的命令里直接改视图 B 的 ViewModel
#[command]
pub fn on_login(&mut self, cx: &mut ViewContext<Self>) {
    let dashboard = cx.get_view::<DashboardViewModel>(); // ❌
    dashboard.update(cx, |d, cx| { d.user = self.user.clone(); cx.notify(); });
}

危害

- 视图耦合,无法独立测试 - 重构时牵一发动全身 - 生命周期难以管理

处方:通过 Context 事件解耦

// 视图 A 派发事件
#[command]
pub fn on_login(&mut self, cx: &mut ViewContext<Self>) {
    cx.dispatch(LoginSuccess(self.user.clone())); // ✅
}

// 视图 B 订阅事件
#[on_loaded]
pub fn on_loaded(&mut self, cx: &mut ViewContext<Self>) {
    cx.subscribe::<LoginSuccess>(|this, ev, cx| {
        this.user = ev.0.clone();
        cx.notify();
    });
}

9.6.8 反模式 8:模板里写业务逻辑

症状

<!-- ❌ 模板里塞业务判断 -->
<p if={items.len() > 0 && user.role == 'admin' && !settings.readonly}>
  {items.iter().filter(|i| i.active).count()} 个活跃项
</p>

危害

- 模板不可单测 - 设计师无法理解 - 业务规则散落各处

处方:收敛到计算属性

#[computed]
pub fn should_show_active_count(&self) -> bool {
    !self.items.is_empty()
        && self.user.role == UserRole::Admin
        && !self.settings.readonly
}

#[computed]
pub fn active_count(&self) -> usize {
    self.items.iter().filter(|i| i.active).count()
}
<p if={should_show_active_count}>{active_count} 个活跃项</p>

9.6.9 反模式速查表

| 反模式 | 信号 | 处方 | | --------- | ------------------------ | ----------------- | | 胖 ViewModel | 字段 > 20,跨多个领域 | 按职责拆分 | | 上帝组件 | props > 10,多形态分支 | 按场景拆分 | | 绑定爆炸 | 绑定路径 > 2 层,多函数调用 | 收敛到计算属性 | | 深嵌套 | div 嵌套 > 6 层 | 抽组件 | | 隐式状态 | bool 组合表达状态机 | 用枚举 | | 命令构造 UI | `.rml.rs` 出现 `div()` | 改用状态控制模板 | | 跨视图直接修改 | `cx.get_view::<Other>()` | 用 Context 事件 | | 模板写业务逻辑 | 模板里多 `&&` / 函数调用 | 收敛到计算属性 |

9.6.10 重构的节奏

反模式不是一天形成的,重构也不必一天完成。推荐节奏:

1. **识别最痛的反模式**:选影响最大的一个 2. **加测试覆盖现状**:重构前先有安全网 3. **小步重构**:每次只改一个 ViewModel / 组件 4. **保持测试通过**:每步后跑测试 5. **重复**:直到反模式消除

不要追求“完美架构”,追求“持续可维护”。架构是演进的,不是一次定型的。

下一章 → [第 10 章 · 高级技巧与工具链](../10-advanced/INDEX.md)