代码重构:组合和继承
最近在看公司内部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只能作为一个很好的学习工具和实现工具,而真正的好代码,仍然需要我们有判断的能力。

浙公网安备 33010602011771号