《互联网产品的客户端架构·规模化与架构演进》13.3 全书收官:四卷回顾与下一站

四卷一百八十节讲完,真正要留下的是一个能自查的承诺模型,而不是一份读完的目录。本节把四卷的核心问题整理成十五项收官检查表,用覆盖率与未关闭项生成下一步建议,并给出继续演进、转入巩固与冻结架构三类停止条件,帮助读者判断什么时候该停下来、什么时候只需维护。

读完四卷,最危险的问题是「接下来做什么」

某团队用三个月读完了四卷。复盘会上,有人问:「我们算不算把客户端架构做完了?」

没有人能回答。因为「读完」和「做到」之间差着一份检查表:哪些承诺已经落地,哪些只有文档,哪些根本没开始。没有这份表,团队只有两种选择——继续加功能,或者停下来等下一次事故。

13.3 做三件事:回顾四卷各自留下的承诺模型,把承诺变成十五项收官检查表,再用一组明确的停止条件回答「什么时候可以不继续演进」。

四卷不是四个专题,而是一条承诺链

四卷看起来主题不同,实际上回答的是同一条链上的四个问题:先定义什么必须一致,再保证内容与实时状态可同步,然后保证交易不出错,最后保证多端多团队下这些承诺仍然成立。

卷核心问题留下的承诺实验工程
端形态与体验基础同一功能在五个端上如何既一致又可用?承诺分级、共享内核、能力探测base-lab
内容、社交与实时互动内容流、实时消息与弱网状态如何保持一致?数据流分层、实时排序、弱网恢复interact-lab
交易、增长与平台化一笔交易在增长实验与平台复用下如何不出错?金额与状态单一事实源、幂等、归因边界trade-lab
规模化与架构演进五端、五团队、五节奏下如何把应用做成平台?平台边界、能力矩阵、治理、迁移退出evolve-lab

这条链的顺序不能颠倒。共享内核之前没有承诺矩阵,共享出去的就是偶然一致;交易正确性之前谈增长,实验会把错误放大;平台化之前没有治理,能力矩阵只会变成一张自动打分的幻灯片。

十五项收官检查表

检查表按四卷分组,每项都写清楚承诺、要回答的问题、证据形态与状态。状态只有三种:

  • covered:有证据,且已经进入日常流程。
  • partial:部分端或部分团队做到,仍有未登记的例外。
  • missing:没有证据,或只停留在文档。

覆盖率把 partial 按半分计算。示例检查表的 15 项中有 7 项覆盖、6 项部分覆盖、2 项缺失,覆盖率约 66.67%。这不是某个团队的真实测评分,而是随代码附带的示例数据;真实使用时替换 SERIES_CHECKLIST 即可。

失分最集中的两处值得注意:弱网恢复与无障碍验收都是 missing。它们不是新技术,而是最容易在「功能优先」的节奏里被无限推迟的承诺。

把四卷的承诺写成检查表

下面的模块把四卷、十五项检查表、建议映射与停止条件都做成纯函数。decideNextStep 是收官的核心:它不评价团队好坏,只根据四个可观测条件判断下一步该继续、巩固还是停止。

// file: src/summary/series-review.ts
// tier: L1
// verify: node --test test/summary/series-review.test.ts

export type VolumeId = "foundation" | "interaction" | "trade" | "evolve";

export type PromiseId =
  | "consistency" | "shared-core" | "capability"
  | "content-flow" | "realtime" | "weak-network"
  | "money" | "idempotency" | "experiment"
  | "platform" | "governance" | "migration"
  | "observability" | "security" | "a11y";

export type Volume = {
  id: VolumeId;
  title: string;
  question: string;
  sections: number;
  lab: string;
  promises: PromiseId[];
};

export type ChecklistItem = {
  id: string;
  volume: VolumeId;
  promise: PromiseId;
  question: string;
  evidence: string;
  status: "covered" | "partial" | "missing";
};

export type SeriesSummary = {
  volumes: number;
  sections: number;
  promises: number;
  covered: number;
  partial: number;
  missing: number;
  coveragePct: number;
};

export type VolumeCoverage = {
  total: number;
  covered: number;
  partial: number;
  missing: number;
  pct: number;
};

export const VOLUMES: Volume[] = [
  {
    id: "foundation", title: "端形态与体验基础", sections: 45, lab: "base-lab",
    question: "同一功能在五个端上如何既一致又可用?",
    promises: ["consistency", "shared-core", "capability"],
  },
  {
    id: "interaction", title: "内容、社交与实时互动", sections: 45, lab: "interact-lab",
    question: "内容流、实时消息与弱网状态如何保持一致?",
    promises: ["content-flow", "realtime", "weak-network"],
  },
  {
    id: "trade", title: "交易、增长与平台化", sections: 45, lab: "trade-lab",
    question: "一笔交易在增长实验与平台复用下如何不出错?",
    promises: ["money", "idempotency", "experiment"],
  },
  {
    id: "evolve", title: "规模化与架构演进", sections: 45, lab: "evolve-lab",
    question: "五端、五团队、五节奏下如何把应用做成平台?",
    promises: ["platform", "governance", "migration", "observability", "security", "a11y"],
  },
];

const check = (
  id: string, volume: VolumeId, promise: PromiseId, question: string, evidence: string,
  status: ChecklistItem["status"],
): ChecklistItem => ({ id, volume, promise, question, evidence, status });

export const SERIES_CHECKLIST: ChecklistItem[] = [
  check("f-consistency", "foundation", "consistency", "哪些行为必须逐字一致,哪些允许等价或不同?", "五端承诺矩阵与跨端契约用例", "covered"),
  check("f-shared-core", "foundation", "shared-core", "共享代码的依赖方向是否单向且可检查?", "core / protocol / adapter / ui 依赖图", "covered"),
  check("f-capability", "foundation", "capability", "端能力缺失时是降级、阻塞还是隐藏?", "能力探测表与四类处理策略示例", "partial"),
  check("i-content-flow", "interaction", "content-flow", "内容流的数据、缓存与渲染是否分段治理?", "内容流分层图与分段预算", "covered"),
  check("i-realtime", "interaction", "realtime", "实时消息在重连与多端同时在线时如何排序?", "重连补偿用例与消息序号断言", "partial"),
  check("i-weak-network", "interaction", "weak-network", "弱网与断网恢复是否验证过降级与回补?", "弱网场景时间线与恢复用例", "missing"),
  check("t-money", "trade", "money", "金额与订单状态是否只有一个事实来源?", "金额整数分用例与字段所有权表", "covered"),
  check("t-idempotency", "trade", "idempotency", "重复提交与结果未知是否先查后重试?", "幂等键用例与未知结果查询时间线", "covered"),
  check("t-experiment", "trade", "experiment", "实验分流是否可复现且变更可追溯?", "同用户同分组的复现用例", "partial"),
  check("e-platform", "evolve", "platform", "平台能力是否进入发布门禁并按缺口排序?", "能力矩阵评分与缺口排名报告", "partial"),
  check("e-governance", "evolve", "governance", "每个架构决定是否有责任人与评审记录?", "ADR 目录、所有权表与评审覆盖率", "partial"),
  check("e-migration", "evolve", "migration", "每次迁移是否有退出条件与回滚触发条件?", "迁移退出条件清单与回滚演练", "covered"),
  check("e-observability", "evolve", "observability", "线上问题能否在分钟内定位到端与版本?", "最小观测契约与告警闭环记录", "covered"),
  check("e-security", "evolve", "security", "数据分级与第三方 SDK 是否定期复核?", "数据分级表与 SDK 权限差异报告", "partial"),
  check("e-a11y", "evolve", "a11y", "无障碍验收是否进入组件契约而不是一次性检查?", "读屏、键盘与动效偏好验收用例", "missing"),
];

const ADVICE: Record<PromiseId, string> = {
  consistency: "先固化三级承诺矩阵,再谈共享代码",
  "shared-core": "收敛共享内核,画出 core → protocol → adapter → ui 依赖图",
  capability: "补齐能力探测的四类处理策略,并给出缺省降级",
  "content-flow": "把内容流拆成数据、缓存、渲染三段,分别设预算",
  realtime: "为实时通道补多端顺序与重连后一致性用例",
  "weak-network": "补齐弱网下的降级与恢复验证,不要只测有网路径",
  money: "金额与订单状态继续以服务端为唯一事实来源",
  idempotency: "把幂等键与未知结果先查后重试写进发布门禁",
  experiment: "实验分流结果要可复现,配置变更留版本记录",
  platform: "把能力矩阵接入发布门禁,缺口排名决定下一迭代",
  governance: "补齐 ADR、所有权表与技术债台账的责任人",
  migration: "每次迁移先写退出条件与回滚触发条件",
  observability: "最小观测契约覆盖五端,告警闭环有响应人",
  security: "数据分级与 SDK 清单每季度复核",
  a11y: "把无障碍验收纳入组件契约,避免迭代退化",
};

function ratio(covered: number, partial: number, total: number): number {
  return total === 0 ? 0 : Math.round(((covered + partial * 0.5) / total) * 10000) / 100;
}

export function summarizeSeries(items: ChecklistItem[]): SeriesSummary {
  const covered = items.filter((i) => i.status === "covered").length;
  const partial = items.filter((i) => i.status === "partial").length;
  const missing = items.filter((i) => i.status === "missing").length;
  return {
    volumes: VOLUMES.length,
    sections: VOLUMES.reduce((acc, volume) => acc + volume.sections, 0),
    promises: items.length,
    covered, partial, missing,
    coveragePct: ratio(covered, partial, items.length),
  };
}

export function checklistByVolume(items: ChecklistItem[]): Record<VolumeId, ChecklistItem[]> {
  const grouped = {} as Record<VolumeId, ChecklistItem[]>;
  for (const volume of VOLUMES) grouped[volume.id] = items.filter((i) => i.volume === volume.id);
  return grouped;
}

export function coverageByVolume(items: ChecklistItem[]): Record<VolumeId, VolumeCoverage> {
  const grouped = checklistByVolume(items);
  const result = {} as Record<VolumeId, VolumeCoverage>;
  for (const volume of VOLUMES) {
    const scoped = grouped[volume.id];
    const covered = scoped.filter((i) => i.status === "covered").length;
    const partial = scoped.filter((i) => i.status === "partial").length;
    const missing = scoped.filter((i) => i.status === "missing").length;
    result[volume.id] = {
      total: scoped.length, covered, partial, missing,
      pct: ratio(covered, partial, scoped.length),
    };
  }
  return result;
}

export function openItems(items: ChecklistItem[]): ChecklistItem[] {
  return items.filter((item) => item.status !== "covered");
}

export function validateChecklist(items: ChecklistItem[]): string[] {
  const issues: string[] = [];
  const seen = new Set<string>();
  const volumes = new Map(VOLUMES.map((volume) => [volume.id, volume]));
  for (const item of items) {
    if (seen.has(item.id)) issues.push(`检查项 id 重复:${item.id}`);
    seen.add(item.id);
    if (item.evidence.trim() === "") issues.push(`缺少证据:${item.id}`);
    const volume = volumes.get(item.volume);
    if (volume === undefined) issues.push(`未知卷:${item.id}`);
    else if (!volume.promises.includes(item.promise)) issues.push(`承诺 ${item.promise} 不属于卷 ${item.volume}`);
  }
  return issues;
}

export function adviceFor(promises: PromiseId[]): string[] {
  const out: string[] = [];
  const seen = new Set<PromiseId>();
  for (const promise of promises) {
    if (seen.has(promise)) continue;
    seen.add(promise);
    out.push(ADVICE[promise]);
  }
  return out;
}

export function nextSteps(items: ChecklistItem[]): string[] {
  return adviceFor(openItems(items).map((item) => item.promise));
}

export type EvolutionState = {
  highRiskOpen: number;
  platformScore: number;
  changeFailureRate: number;
  evidenceCoverage: number;
  teamReady: boolean;
};

export type StopDecision = { decision: "continue" | "consolidate" | "stop"; reasons: string[] };

export function decideNextStep(state: EvolutionState): StopDecision {
  const reasons: string[] = [];
  if (state.highRiskOpen > 0) reasons.push(`仍有 ${state.highRiskOpen} 个高风险项未关闭`);
  if (state.platformScore < 3.5) reasons.push(`平台验收分 ${state.platformScore.toFixed(2)} 低于 3.5`);
  if (state.evidenceCoverage < 0.8) {
    reasons.push(`证据覆盖率 ${Math.round(state.evidenceCoverage * 100)}% 低于 80%`);
  }
  if (reasons.length > 0) return { decision: "continue", reasons };

  if (!state.teamReady) {
    return { decision: "consolidate", reasons: ["平台达标但团队尚未准备好接管日常演进"] };
  }
  if (state.changeFailureRate > 0.2) {
    return {
      decision: "consolidate",
      reasons: [`变更失败率 ${Math.round(state.changeFailureRate * 100)}% 高于 20%,先还债`],
    };
  }
  return {
    decision: "stop",
    reasons: ["高风险清零、平台达标、证据充分且变更稳定,可以冻结架构转入维护"],
  };
}

下一站建议与停止条件

检查表的未关闭项会自动生成建议。示例数据给出八条,顺序与检查表一致:能力探测 → 实时顺序 → 弱网恢复 → 实验复现 → 平台门禁 → 治理责任 → SDK 复核 → 无障碍契约。这些建议不是排期,而是下一轮评审的输入;每条都要先补证据,再谈完成。

停止条件则回答另一个问题:什么时候可以不再做架构演进?模块把它分成三类决策:

决策触发条件动作
continue有高风险未关闭、平台分低于 3.5、或证据覆盖低于 80%按缺口排名继续演进
consolidate平台达标但团队未准备好,或变更失败率高于 20%暂停新能力,先还债与补责任
stop高风险清零、平台达标、证据充分、团队就绪且变更稳定冻结架构,转入维护与小步优化

stop 最容易误读。它不是「产品停止迭代」,而是停止架构层面的扩张:不再新增平台抽象、不再改共享内核边界、不再引入新端形态。业务照常迭代,架构只做维护性修复。没有这个停止条件,平台化会变成无限扩张,最后没人能说清哪一版是稳定基线。

判据与反例

反例一:把读完当做到。 检查表全部标成 covered,但证据栏写的是「已完成」。这种表能让复盘会好看,却无法回答任何一个具体问题。证据必须是可复查的产物:矩阵、用例、流水线记录、演练报告。

反例二:永远 continue。 如果团队从不设定停止条件,每次评审都会找出新缺口,架构演进就永远不会结束。停止条件不是懒惰,而是承认「当前架构已经足够支撑业务」。

还有一个边界:四卷不是唯一路径。读者可以只做卷一与卷三,把内容与实时互动留给更专业的团队;也可以用四卷的检查表审视一套已经存在的架构。检查表是判据,不是课程表。

小结

四卷留下的不是四个专题,而是一条承诺链:先定义一致,再保证同步,再保证交易正确,最后保证多端多团队下承诺仍然成立。十五项检查表把这条链变成可自查的清单,覆盖率与未关闭项给出下一站建议。

收官真正要回答的问题是:哪些承诺已经有证据,哪些还没有,以及什么时候可以停止扩张。 能回答这三个问题,四卷才算读完。

阅读导航:上一节:13.2 平台化验收:能力矩阵与治理清单 · 下一节:目录与学习路径 。

常见错误与边界

  • 把「读完四卷」当成「架构已经做完」,没有检查表。
  • 检查表全部标绿,证据栏只写「已完成」「已确认」。
  • 覆盖率只算 covered,把大量 partial 当成缺失或当成完成。
  • 未关闭项直接变成排期,不先补证据与责任人。
  • 停止条件只看平台分,不看高风险项、证据覆盖与变更失败率。
  • 把 stop 理解成产品停止迭代,导致团队不敢设定停止条件。
  • 把检查表当课程表,要求每个团队按同一顺序做完十五项。
  • 一次收官之后不再复查,半年后检查表与实际架构脱节。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查