代码重构:组合和继承

最近在看公司内部17年写的代码,发现代码耦合太多,重复的地方也太多,因此可维护性变得很差。但是现在重构代码显然不太可能了,小山已经堆起来,现在已经改不动了,写笔记记录一下cc给的重构方案,希望自己以后面对相似的情况时不要写出这样的代码。

这里,我先给出参考资料里的一个观点:

最新的观点是组合的使用一般情况下比继承更为灵活,尤其是单继承的体系里,所以倾向于使用组合,否则会让子类承载很多不属于自己的职能。个人对此观点持保留意见,在我经历过的代码中,有一个小规律,我分析一下。protected abstract 这种是最值得使用继承的,父类保留扩展点,子类扩展,没什么好说的。protected final 这种方法,子类是只能使用不能修改实现的。一般有两种情况:① 抽象出主流程不能被修改的,然而一般情况下,public final更适合这个职能。如果只是流程的一部分,需要思考这个流程的类归属,大部分变成public组合到其他类里是更合适的。② 父类是抽象类无法直接对外提供服务,又不希望子类修改它的行为,这种大多数情况下属于工具方法,比较适合用另一个领域对象来承载并用组合的方式来使用。protected 这种是有争议的,是父类有默认实现但子类可以扩展的。凡是有扩展可能的,使用继承更理想一些。否则,定义成final并考虑成组合。综上所述,个人认为继承更多的是为扩展提供便利,为复用而存在的方法最好使用组合的方式。当然,更为大的原则是明确每个方法的领域划分。

而我今天给的例子,就是典型的组合大于继承的例子。

背景

先说背景,产品的首页需要展示很多维度的信息,这些不同维度的信息需要检索不同的表,前端页面的一个模块在后端可能就需要十几个维度信息查询的请求,而整个公司业务有足足四五百个维度。不同的维度需要从不同的数据库和表查询,数据源不同、字段不同、后处理不同。

创建这个项目的程序员的思路也是我能想到的思路,写了一个getDimensionList的基类,把重要的查询思路等写成了抽象方法,每一个维度的查询都继承这个类,因此有了几百个子类。

伪代码如下:

abstract class DimensionBase {
  // 模板方法:锁死"取数 → 映射 → 富化 → 返回"这个流程
  async getList(input: Input): Promise<{ total: number; items: any[] }> {
    const raw = await this.fetch(input)        // 子类实现
    const items = this.map(raw.rows)           // 子类实现
    const enriched = await this.enrich(items, input) // 子类可选覆盖
    return { total: raw.total, items: enriched }
  }

  protected abstract fetch(input: Input): Promise<{ total: number; rows: any[] }>
  protected abstract map(rows: any[]): any[]
  protected async enrich(items: any[], _input: Input): Promise<any[]> {
    return items // 默认不富化
  }
}

class ExecutedPersonDimension extends DimensionBase {
  protected async fetch(input: Input) {
    const client = createClient(ots.executedPerson) // ← 又在子类里建连、打 SDK
    return client.getRange(/* ... */)
  }
  protected map(rows: any[]) {
    return rows.map(r => ({ caseNo: r.case_no, amount: r.amount /* ... */ }))
  }
}

解决方案

cc给出的方案十分优美,首先将不同的数据源抽象成一个DataSource适配器:

// 一个统一的取数契约,四种存储各写一个适配器
interface DataSource {
  fetch(query: Query): Promise<{ total: number; rows: RawRow[] }>
}

class OtsSource implements DataSource {
  constructor(private client: OtsClient) {}
  async fetch(q: Query) {
    const data = await this.client.batchGetRow(q.pks, q.columns)
    return { total: data.length, rows: data }
  }
}
// 同理 SqlSource / EsSource / MiningSource ...

再为不同的维度查询写一份配置:

interface SimpleDimension<Raw = any, Item = any> {
  sourceName: string                    // 用哪个 DataSource
  buildQuery: (input: Input) => Query    // 怎么拼查询
  map: (row: Raw) => Item                // 纯函数:raw → item,零 IO
  enrich?: EnrichStep[]                  // 声明式富化,可选
}

// 一个真实维度,现在只是一份"数据"
const executedPerson: SimpleDimension = {
  sourceName: 'ots',
  buildQuery: (input) => ({
    table: 'executed_person_by_eid',
    pks: [{ eid: input.eid }],
    columns: ['case_no', 'amount', 'court', 'case_date'],
    skip: input.start,
    limit: input.hit,
  }),
  map: (r) => ({          // ← 纯函数,列名映射全在这
    caseNo: r.case_no || '-',
    amount: r.amount || '-',
    court: r.court || '-',
    caseDate: r.case_date || '-',
  }),
  enrich: [attachTagsStep],  // 声明"我需要附标签",怎么批处理交给引擎
}

将所有的配置注册起来,查询的时候只需要去配置中心检索对应的配置就可以,这样批量查询也就不需要再多次写查询代码:

class DimensionEngine {
  constructor(
    private registry: Map<string, SimpleDimension | CompositeDimension>,
    private sources: Record<string, DataSource>,
  ) {}

  async run(name: string, input: Input, ctx: Context) {
    const dim = this.registry.get(name)
    if (!dim) throw new Error(`未注册的维度: ${name}`)

    // 复杂维度:走编排(见下)
    if ('compose' in dim) return dim.compose(this, input, ctx)

    // 简单维度:固定流程,但流程只在这一处
    const source = this.sources[dim.sourceName]
    let result: DimensionResult = { total: 0, items: [] }
    try {
      const raw = await source.fetch(dim.buildQuery(input))
      const items = raw.rows.map(dim.map)               // 纯映射
      const enriched = await this.applyEnrich(dim.enrich, items, ctx)
      result = { total: raw.total, items: enriched }
    } catch (err) {
      ctx.logger.error(`${name} 失败`, err)             // 韧性策略上收到这里,
      result = { total: 0, items: [] }                  // 不用每个维度手写 try/catch
    }
    return result
  }

  private async applyEnrich(steps: EnrichStep[] = [], items: any[], ctx: Context) {
    for (const step of steps) items = await step.run(items, ctx)
    return items
  }
}

同时,跨维度批合并的问题也可以得到解决:

interface EnrichStep {
  run(items: any[], ctx: Context): Promise<any[]>
}

// pid 反查:一次性收集所有 name,批量查一次,再回填
const attachPidStep: EnrichStep = {
  async run(items, ctx) {
    const names = items.filter(i => !i.pid).map(i => ({ eid: i.eid, pName: i.stockName }))
    if (!names.length) return items
    // ctx.loaders.getPids 是 DataLoader 式的:同一请求内多次调用会自动合并成一次
    const pidMap = await ctx.loaders.getPids.loadMany(names)
    return items.map(i => ({ ...i, pid: pidMap[i.stockName] || '' }))
  },
}

interface CompositeDimension {
  compose(engine: DimensionEngine, input: Input, ctx: Context): Promise<any>
}

const debtAnalysis: CompositeDimension = {
  async compose(engine, input, ctx) {
    // 编排的是维度,不是裸 API —— 每个子查询的取数/映射/富化都复用上面的成果
    const [executed, execution, freeze] = await Promise.all([
      engine.run('executedPerson', input, ctx),
      engine.run('executionP', input, ctx),
      engine.run('judicialFreeze', input, ctx),
    ])
    return summarize(executed, execution, freeze) // summarize 是纯函数
  },
}

结语

虽然是cc给的方案,但是这是由于cc已经看到了现在的所有业务,而我们在写程序的时候,往往还不知道后面的业务有哪些,所以即使用AI辅助,也很容易写成原来的代码逻辑。我仍然认为AI只能作为一个很好的学习工具和实现工具,而真正的好代码,仍然需要我们有判断的能力。

参考资料

posted @ 2026-08-06 19:53  ZCry  阅读(3)  评论(0)    收藏  举报