一、引言:为什么"深度 vs 广度"是核心问题?
在多数公司里,你大概率经历过这样的讨论:
- 产品在说:
"我们要不要先把这个场景做深?还是先多覆盖几个行业、多做几个功能点?" - 架构在说:
"现在做太复杂的平台化和中台化,会不会过度设计?但又怕后面扩展不了。" - 开发在说:
"要不要为这个小需求抽一层通用组件?还是先写死,等后面有更多场景再重构?"
本质上,这都是在回答同一个问题:
在有限资源下,产品、架构、开发如何在业务场景的深度与广度之间做决策?
- 深度:在少数关键场景上打磨得极致,追求体验、效率、稳定性、智能化程度。
- 广度:覆盖更多客户类型、更多行业、更多功能模块,提高整体机会面和营收天花板。
本文从产品视角、架构视角、开发视角系统讨论"如何权衡深与广",并给出:
- 可操作的决策框架
- 技术与架构设计上的具体做法(含伪代码、示例)
- 实战中的踩坑与建议
适合的读者包括:产品经理、技术负责人、架构师、资深/主程开发。
二、问题背景:深与广的不对称成本
2.1 深 vs 广 的典型冲突场景
一些典型矛盾:
-
ToB 项目交付
- 销售:这个客户还有 5 个个性化需求,做了就能签单。
- 产品:这些需求都不是通用能力,做进去会污染产品。
- 技术:每加一个需求,当前架构都要堆一层 if/else,后面难维护。
-
平台型产品
- 业务:要快速扩展到 5 个新行业,先把模板和页面复制改改就能上线。
- 架构:如果不抽象领域模型,后面每个行业都要单独维护一套。
- 开发:现在抽象成本高,做不好又会搞成"大一统屎山"。
-
创业公司 MVP 阶段
- 需求变动快,功能不稳定。
- 是否要一开始就搞复杂的插件体系、多租户、可插拔引擎?
这些争论背后,其实是:业务不确定性 + 资源有限 + 技术债不可见。
2.2 一个统一的决策维度
可以将"深度 vs 广度"抽象到三个维度:
-
业务确定性:
- 是否已经验证过有稳定付费意愿?
- 需求是否在收敛,而不是每天都变?
-
能力复用度(潜在可复用性):
- 当前能力将来能不能在多个客户 / 多个行业 / 多条产品线上复用?
- 是通用共性,还是高度定制?
-
技术演化成本:
- 如果一开始不做深,将来重构成本是不是可以接受?
- 当前技术债是否可控、可局部替换?
这三个维度决定你应当"先广后深"还是"先深后广"。
三、产品视角:场景分层与路线图设计
3.1 用"场景分层"来决定先深还是先广
建议产品将业务场景分为三层:
-
基础共性层(Common Layer)
- 几乎所有客户都会用到的功能:账号体系、权限、基础报表、消息通知、工单流转等。
- 特点:高复用、高稳定、高长期价值。
-
行业通用层(Industry Layer)
-
某个行业里常见的通用能力:
- 如 CRM 中的"销售漏斗、客户跟进、合同管理",
- 电商里的"商品、订单、库存、发货"。
-
-
客户个性化层(Customization Layer)
- 某个大客户特有的流程、审批链、表单字段等。
- 特点:收益与成本高度依赖单个客户,复用率低。
基本策略:
- 对 基础共性层:优先做"深",接口、体验、稳定性都要打磨好。
- 对 行业通用层:先"广度覆盖",快速验证需求,再对稳定下来的场景做"纵向深挖"。
- 对 客户个性化层:尽量通过"配置化 / 插件化 / 脚本化"解决,避免硬编码深挖。
可以用一个简化矩阵:
| 场景类型 | 业务确定性 | 复用潜力 | 建议策略 |
|---|---|---|---|
| 基础共性层 | 高 | 高 | 优先做深,再适度扩展广度 |
| 行业通用层 | 中 | 中/高 | 小步快跑:先广后深 |
| 客户个性化层 | 低 | 低 | 限制开发:尽量配置化 |
3.2 路线图:深与广的阶段性切换
以一个 SaaS 产品为例:
-
0→1 阶段(MVP)
-
目标:验证是否有人愿意为核心功能付费。
-
策略:
- 对最小闭环核心场景做"足够深",确保可用、好用。
- 广度被压缩------少做几个模块,但做完一个模块的关键闭环。
-
-
1→10 阶段(规模化)
-
目标:扩展更多客户与行业,打造销售故事。
-
策略:
- 核心能力继续迭代深度(性能、稳定性、体验优化)。
- 同时以低成本方式铺广:模块化模板、配置能力、行业包。
-
-
10→100 阶段(平台化)
-
目标:成为平台,减少人肉服务,构建生态。
-
策略:
- 收缩个性化开发,强化产品"能力平台":流程引擎、规则引擎、表单引擎、报表引擎等。
- 将广度构建在平台之上,由合作伙伴和客户自服务扩展。
-
四、架构视角:如何用技术手段同时支撑深与广
4.1 能力抽象:从"场景特化"到"领域模型"
错误的做法是:
"每来一个新场景,就复制一套代码,稍微 tweak 一下字段和流程。"
更好的做法是:
识别出可抽象的能力边界,做到:
- 底层:一套相对稳定的"领域模型 + 能力引擎"。
- 上层:通过配置 / 元数据 / DSL(领域特定语言)来支撑不同场景。
示例:审批流程场景的抽象
坏的实现(每个流程一个 if/else) :
ini
if processType == "leave" {
// 请假审批流程逻辑
} else if processType == "expense" {
// 报销审批流程逻辑
} else if processType == "purchase" {
// 采购审批流程逻辑
}
这样短期看开发快,但:
- 每加一个流程就要改代码。
- 逻辑散落在各个服务里,测试和维护困难。
更通用的实现:基于流程引擎的抽象模型
核心抽象可以是:
javascript
class ProcessDefinition {
id: String
name: String
nodes: List<Node>
transitions: List<Transition>
}
class Node {
id: String
type: "start" | "task" | "gateway" | "end"
assigneeRule: AssigneeRule
}
class Transition {
fromNodeId: String
toNodeId: String
conditionExpression: String // 如 amount > 5000
}
class ProcessInstance {
id: String
definitionId: String
businessKey: String
currentNodeId: String
variables: Map<String, Any>
}
不同业务流程(请假 / 报销 / 采购)都变成数据配置 ,而不是写死逻辑:
json
{
"id": "expense_approval",
"nodes": [
{ "id": "start", "type": "start" },
{ "id": "manager", "type": "task", "assigneeRule": "manager_of(applicant)" },
{ "id": "finance", "type": "task", "assigneeRule": "role:FINANCE" },
{ "id": "end", "type": "end" }
],
"transitions": [
{ "fromNodeId": "start", "toNodeId": "manager", "conditionExpression": "" },
{ "fromNodeId": "manager", "toNodeId": "finance", "conditionExpression": "amount > 5000" },
{ "fromNodeId": "manager", "toNodeId": "end", "conditionExpression": "amount <= 5000" },
{ "fromNodeId": "finance", "toNodeId": "end", "conditionExpression": "" }
]
}
这样实现后:
- "深度"体现在:你可以在引擎层不断打磨性能、容错、监控、可视化。
- "广度"体现在:仅通过配置新增 N 种流程,不再线性消耗研发人力。
4.2 技术实现模式:插件化 & 配置化
常用的技术手段:
-
插件化架构(Plugin Architecture)
-
典型实现:
- 后端通过 SPI、接口 + 反射加载插件。
- 前端通过微前端 / 动态组件加载扩展页面。
-
适合:
- 对同一类能力有多种实现的场景(不同支付渠道、不同风控策略)。
-
伪代码示例(Java/Spring 风格) :
typescript
public interface RiskRule {
String getCode();
RiskDecision evaluate(RiskContext context);
}
// 某个客户的自定义规则插件
@Component
public class CustomerABlackListRule implements RiskRule {
@Override
public String getCode() { return "CUST_A_BLACKLIST"; }
@Override
public RiskDecision evaluate(RiskContext context) {
if (context.getUserId() in BlackListRepo.of("customerA")) {
return RiskDecision.reject("blacklist");
}
return RiskDecision.pass();
}
}
// 规则引擎通过配置加载规则组合
@Service
public class RiskEngine {
@Autowired
private List<RiskRule> allRules;
public RiskResult evaluate(String sceneCode, RiskContext ctx) {
List<String> enabledRuleCodes = configCenter.getRules(sceneCode);
for (RiskRule rule : allRules) {
if (enabledRuleCodes.contains(rule.getCode())) {
RiskDecision decision = rule.evaluate(ctx);
if (!decision.isPass()) return RiskResult.fail(decision);
}
}
return RiskResult.pass();
}
}
-
元数据驱动(Metadata-Driven)
-
通过 JSON/YAML 等方式定义:表单字段、校验规则、显示逻辑等。
-
适合:
- 报表、表单、搜索条件、列表页面等高频变化但结构相似的场景。
-
前端表单的元数据定义示例(Vue/React 通用) :
json
[
{
"field": "amount",
"label": "金额",
"type": "number",
"required": true,
"rules": [
{ "type": "min", "value": 0, "message": "金额必须大于 0" }
]
},
{
"field": "reason",
"label": "事由",
"type": "textarea",
"required": true,
"rules": [
{ "type": "length", "max": 200, "message": "最多 200 字" }
]
}
]
前端统一渲染引擎:
javascript
function renderForm(formConfig) {
return formConfig.map(field => {
switch(field.type) {
case 'number': return <NumberInput {...fieldProps(field)} />;
case 'textarea': return <TextArea {...fieldProps(field)} />;
// ...
}
});
}
当你通过这种抽象实现后,"广度扩展"只需新增配置,而不必修改代码;
接下来,你可以有更多时间在"引擎层"做性能优化、国际化、多终端适配等"深度打磨"。
4.3 渐进式架构演化:避免"一步到位的完美平台"
常见坑:
一开始就试图设计一个能适配"所有可能业务场景"的超级平台,结果研发一年,业务还没跑通,就先把自己架死了。
推荐做法是渐进式演化:
-
第一阶段:简单分层 + 封装关键点
- Controller / Service / Repository 基本分层。
- 明确领域边界(User、Order、Inventory 等),避免业务逻辑写在 Controller。
-
第二阶段:在高变更点引入"引擎化"
- 发现某个地方需求变动频繁(如审批、规则、表单),才引入 DSL / 引擎。
- 通过"代理 + 适配器"方式让旧逻辑逐步迁移。
-
第三阶段:平台化与生态化
- 将稳定的引擎向外暴露为平台能力(SDK/开放 API)。
- 提供可观察性、沙箱环境、版本管理,支持第三方扩展。
关键词是:演化 ,而不是猜测所有未来需求。
五、开发视角:在代码层面如何落地"可深可广"
5.1 代码层面的"防腐"与边界意识
开发个人经常面临的具体选择:
- 值不值得为这个需求写单元测试?
- 要不要为一个看似小的功能抽接口?
- 是否需要现在就做异步、分布式锁、缓存层?
一个简单可用的"决策 checklist":
- 这个逻辑是否有跨模块复用的潜力?
- 业务方是否明确说,这是"试试水"的一次性需求?
- 如果不抽象,将来要改它的影响面有多大?
- 这块逻辑是否非常接近外部系统(支付、物流、第三方 API)?
经验规则:
- 对高复用 + 靠近核心领域的逻辑:大胆抽象、写测试、做封装(偏"深")。
- 对高度试验性 + 外围业务:先简单实现、保留重构空间(偏"快"和"广")。
5.2 一个具体例子:订单导出功能要不要平台化?
场景:
产品提一个需求:"支持订单列表导出为 Excel,后面可能还会支持客户列表、库存列表等。"
不好的做法(一次性实现) :
ini
@GetMapping("/orders/export")
public void exportOrders(HttpServletResponse response) {
List<Order> orders = orderService.query(...);
HSSFWorkbook wb = new HSSFWorkbook();
HSSFSheet sheet = wb.createSheet("订单");
// ... 一堆写 Excel 代码 ...
wb.write(response.getOutputStream());
}
这样实现,当后面需要导出客户、库存时,只能再复制一份类似代码。
更好的做法:抽象一个简单的"导出引擎"
- 抽象导出列:
typescript
public class ExportColumn<T> {
private String header;
private Function<T, String> valueProvider;
}
- 抽象导出请求:
swift
public class ExportRequest<T> {
private String fileName;
private List<ExportColumn<T>> columns;
private Supplier<Stream<T>> dataProvider;
}
- 通用导出服务:
scss
@Service
public class ExcelExportService {
public <T> void export(ExportRequest<T> request, OutputStream os) {
Workbook wb = new HSSFWorkbook();
Sheet sheet = wb.createSheet("Sheet1");
// header
Row headerRow = sheet.createRow(0);
for (int i = 0; i < request.getColumns().size(); i++) {
headerRow.createCell(i).setCellValue(request.getColumns().get(i).getHeader());
}
// data
AtomicInteger rowIndex = new AtomicInteger(1);
request.getDataProvider().get().forEach(item -> {
Row row = sheet.createRow(rowIndex.getAndIncrement());
for (int i = 0; i < request.getColumns().size(); i++) {
String value = request.getColumns().get(i).getValueProvider().apply(item);
row.createCell(i).setCellValue(value);
}
});
wb.write(os);
}
}
- 订单导出层调用:
less
@GetMapping("/orders/export")
public void exportOrders(HttpServletResponse response) {
ExportRequest<Order> req = new ExportRequest<>();
req.setFileName("订单列表");
req.setColumns(List.of(
new ExportColumn<>("订单号", o -> o.getOrderNo()),
new ExportColumn<>("金额", o -> o.getAmount().toString()),
new ExportColumn<>("下单时间", o -> format(o.getCreatedAt()))
));
req.setDataProvider(() -> orderService.streamAll(...));
excelExportService.export(req, response.getOutputStream());
}
这就是在一个**看起来"小需求"**上,适度投入,获得后续"广度扩展"的能力。
你不需要一开始就造一个复杂的报表平台,但可以把导出能力抽象成一个轻量的可复用组件。
5.3 如何控制"过度设计":三个约束
防止自己掉进"架构师病"的三个自检问题:
-
有没有真实的、明确的第二个使用场景?
- 没有第二个场景时,抽象往往不靠谱。
-
复杂度是否仍然被团队多数人理解?
- 如果只有你一个人能看懂这套架构,长远来看维护成本很高。
-
是否留有简化路径?
- 设计时考虑"如果业务失败了/转向了,这一块能否被直接丢弃或替换?"
六、综合权衡框架:产品 × 架构 × 开发如何协同决策
可以给团队建立一个统一决策流程,避免每次都吵架靠拍脑袋:
6.1 决策步骤
-
标记场景类型
- 产品先判断:这个需求属于哪一层(基础共性 / 行业通用 / 客户个性化)?
-
评估业务确定性
- 看:是否有多个客户提过这个需求?是否与公司长期战略强相关?
-
评估技术复用 potential
- 架构 & 开发:这个能力抽象后,能被哪些模块 / 未来产品线使用?
-
估算两种路径的成本
- 方案 A:为了"深度"进行抽象设计的成本(开发时间、风险)。
- 方案 B:直接"写死/复制"以快速覆盖的成本 + 未来重构成本(粗估)。
-
选择策略并记录决策
-
决定是:
- 做平台化抽象(偏深),还是
- 快速堆功能验证业务(偏广)。
-
写下一个短的 ADR(Architecture Decision Record),为将来复盘留证据。
-
6.2 简化版 ADR 模板(可落地)
markdown
# ADR-2026-03-04-001: 订单导出能力是否平台化
## 背景
业务提出订单导出需求,未来可能扩展到客户、库存列表导出。
## 选项
1. 仅为订单列表做一次性导出实现。
2. 抽象为通用导出组件,订单/客户/库存等共享。
## 分析
- 业务确定性:高。导出是多个团队共同诉求。
- 复用潜力:中-高。多个模块都有导出需求。
- 当前成本:
- 方案1:1 人日,几乎无设计。
- 方案2:3 人日,含设计/开发/简单测试。
- 未来成本:
- 方案1:每新增一个导出,再 1 人日,逻辑分散、维护成本高。
- 方案2:新增导出仅需拼装配置,0.5 人日以内。
## 决策
选择方案2:抽象一个通用导出组件,先支持 Excel,保留未来扩展 CSV/PDF 接口。
## 跟进
- 由 A 负责设计与实现
- 下周技术例会复盘导出组件使用情况