反向重构:搬移语句到调用者(217)

result.push(`<p>title: ${person.photo.title}</p>`);
result.concat(photoData(person.photo));
 
function photoData(aPhoto) {
  return [
    `<p>location: ${aPhoto.location}</p>`,
    `<p>date: ${aPhoto.date.toDateString()}</p>`,
  ];
}
 
result.concat(photoData(person.photo));
 
function photoData(aPhoto) {
  return [
    `<p>title: ${aPhoto.title}</p>`,
    `<p>location: ${aPhoto.location}</p>`,
    `<p>date: ${aPhoto.date.toDateString()}</p>`,
  ];
}

动机

要维护代码库的健康发展,需要遵守几条黄金守则,其中最重要的一条当属“消除重复”。如果我发现调用某个函数时,总有一些相同的代码也需要每次执行,那么我会考虑将此段代码合并到函数里头。这样,日后对这段代码的修改只需改一处地方,还能对所有调用者同时生效。如果将来代码对不同的调用者需有不同的行为,那时再通过搬移语句到调用者(217)将它(或其一部分)搬移出来也十分简单。

如果某些语句与一个函数放在一起更像一个整体,并且更有助于理解,那我就会毫不犹豫地将语句搬移到函数里去。如果它们与函数不像一个整体,但仍应与函数一起执行,那我可以用提炼函数(106)将语句和函数一并提炼出去。这基本就是我下面要描述的做法了,只是下面还多了内联和改名的步骤。这些清理工作通常有其必要性,可以在完成核心步骤后再择机完成。

做法

如果重复的代码段离调用目标函数的地方还有些距离,则先用移动语句(223)将这些语句挪动到紧邻目标函数的位置。

如果目标函数仅被唯一一个源函数调用,那么只需将源函数中的重复代码段剪切并粘贴到目标函数中即可,然后运行测试。本做法的后续步骤至此可以忽略。

如果函数不止一个调用点,那么先选择其中一个调用点应用提炼函数(106),将待搬移的语句与目标函数一起提炼成一个新函数。给新函数取个临时的名字,只要易于搜索即可。

调整函数的其他调用点,令它们调用新提炼的函数。每次调整之后运行测试。

完成所有引用点的替换后,应用内联函数(115)将目标函数内联到新函数里,并移除原目标函数。

对新函数应用函数改名(124),将其改名为原目标函数的名字。

如果你能想到更好的名字,那就用更好的那个。

范例

我将用一个例子来讲解这项手法。以下代码会生成一些关于相片(photo)的 HTML:

function renderPerson(outStream, person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(`<p>title: ${person.photo.title}</p>`);
  result.push(emitPhotoData(person.photo));
  return result.join("\n");
}
function photoDiv(p) {
  return [
    "<div>",
    `<p>title:  ${p.title}</p>`,
    emitPhotoData(p),
    "</div>",
  ].join("\n");
}
 
function emitPhotoData(aPhoto) {
  const result = [];
  result.push(`<p>location: ${aPhoto.location}</p>`);
  result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`);
  return result.join("\n");
}

这个例子中的 emitPhotoData 函数有两个调用点,每个调用点的前面都有一行类似的重复代码,用于打印与标题(title)相关的信息。我希望能消除重复,把打印标题的那行代码搬移到 emitPhotoData 函数里去。如果 emitPhotoData 只有一个调用点,那我大可直接把代码复制并粘贴过去就完事,但若调用点不止一个,那我就更倾向于用更安全的手法小步前进。

我先选择其中一个调用点,对其应用提炼函数(106)。除了我想搬移的语句,我还把 emitPhotoData 函数也一起提炼到新函数中。

function photoDiv(p) {
  return ["<div>", zznew(p), "</div>"].join("\n");
}
 
function zznew(p) {
  return [`<p>title: ${p.title}</p>`, emitPhotoData(p)].join("\n");
}

完成提炼后,我会逐一查看 emitPhotoData 的其他调用点,找到该函数与其前面的重复语句,一并换成对新函数的调用。

function renderPerson(outStream, person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(zznew(person.photo));
  return result.join("\n");
}

替换完 emitPhotoData 函数的所有调用点后,我紧接着应用内联函数(115)将 emitPhotoData 函数内联到新函数中。

function zznew(p) {
  return [
    `<p>title: ${p.title}</p>`,
    `<p>location: ${p.location}</p>`,
    `<p>date: ${p.date.toDateString()}</p>`,
  ].join("\n");
}

最后,再对新提炼的函数应用函数改名(124),就大功告成了。

function renderPerson(outStream, person) {
  const result = [];
  result.push(`<p>${person.name}</p>`);
  result.push(renderPhoto(person.photo));
  result.push(emitPhotoData(person.photo));
  return result.join("\n");
}
 
function photoDiv(aPhoto) {
  return ["<div>", emitPhotoData(aPhoto), "</div>"].join("\n");
}
 
function emitPhotoData(aPhoto) {
  return [
    `<p>title: ${aPhoto.title}</p>`,
    `<p>location: ${aPhoto.location}</p>`,
    `<p>date: ${aPhoto.date.toDateString()}</p>`,
  ].join("\n");
}

同时我会记得调整函数参数的命名,使之与我的编程风格保持一致。