重构——搬移语句到调用者(Move Statements to Callers),其反向重构:搬移语句到函数(213)

发布时间 2023-04-12 20:07:13作者: bonelee

8.4 搬移语句到调用者(Move Statements to Callers)

反向重构:搬移语句到函数(213)

emitPhotoData(outStream, person.photo);

function emitPhotoData(outStream, photo) {
  outStream.write(`<p>title: ${photo.title}</p>\n`);
  outStream.write(`<p>location: ${photo.location}</p>\n`);
}

emitPhotoData(outStream, person.photo);
outStream.write(`<p>location: ${person.photo.location}</p>\n`);

function emitPhotoData(outStream, photo) {
  outStream.write(`<p>title: ${photo.title}</p>\n`);
}

动机

作为程序员,我们的职责就是设计出结构一致、抽象合宜的程序,而程序抽象能力的源泉正是来自函数。与其他抽象机制的设计一样,我们并非总能平衡好抽象的边界。随着系统能力发生演进(通常只要是有用的系统,功能都会演进),原先设定的抽象边界总会悄无声息地发生偏移。对于函数来说,这样的边界偏移意味着曾经视为一个整体、一个单元的行为,如今可能已经分化出两个甚至是多个不同的关注点。

函数边界发生偏移的一个征兆是,以往在多个地方共用的行为,如今需要在某些调用点面前表现出不同的行为。于是,我们得把表现不同的行为从函数里挪出,并搬移到其调用处。这种情况下,我会使用移动语句(223)手法,先将表现不同的行为调整到函数的开头或结尾,再使用本手法将语句搬移到其调用点。只要差异代码被搬移到调用点,我就可以根据需要对其进行修改。

这个重构手法比较适合处理边界仅有些许偏移的场景,但有时调用点和调用者之间的边界已经相去甚远,此时便只能重新进行设计了。若果真如此,最好的办法是先用内联函数(115)合并双方的内容,调整语句的顺序,再提炼出新的函数来,以形成更合适的边界。

做法

最简单的情况下,原函数非常简单,其调用者也只有寥寥一两个,此时只需把要搬移的代码从函数里剪切出来并粘贴回调用端去即可,必要的时候做些调整。运行测试。如果测试通过,那就大功告成,本手法可以到此为止。

若调用点不止一两个,则需要先用提炼函数(106)将你不想搬移的代码提炼成一个新函数,函数名可以临时起一个,只要后续容易搜索即可。

如果原函数是一个超类方法,并且有子类进行了覆写,那么还需要对所有子类的覆写方法进行同样的提炼操作,保证继承体系上每个类都有一份与超类相同的提炼函数。接着将子类的提炼函数删除,让它们引用超类提炼出来的函数。

对原函数应用内联函数(115)。

对提炼出来的函数应用改变函数声明(124),令其与原函数使用同一个名字。

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

范例

下面这个例子比较简单:emitPhotoData 是一个函数,在两处地方被调用。

  function renderPerson(outStream, person) {
 outStream.write(`<p>${person.name}</p>\n`);
 renderPhoto(outStream, person.photo);
 emitPhotoData(outStream, person.photo);
}

function listRecentPhotos(outStream, photos) {
 photos
  .filter(p => p.date > recentDateCutoff())
  .forEach(p => {
   outStream.write("<div>\n");
   emitPhotoData(outStream, p);
   outStream.write("</div>\n");
  });
}

function emitPhotoData(outStream, photo) {
 outStream.write(`<p>title: ${photo.title}</p>\n`);
 outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
 outStream.write(`<p>location: ${photo.location}</p>\n`);
}

我需要修改软件,支持 listRecentPhotos 函数以不同方式渲染相片的 location 信息,而 renderPerson 的行为则保持不变。为了使这次修改更容易进行,我要应用本手法,将 emitPhotoData 函数最后的那行代码搬移到其调用端。

一般来说,像这样简单的场景,我都会直接将 emitPhotoData 的最后一行剪切并粘贴到两个调用它的函数后面。但为了演示这项重构手法如何在更复杂的场景下运作,这里我还是遵从更详细也更安全的步骤。

重构的第一步是先用提炼函数(106),将那些最终希望留在 emitPhotoData 函数里的语句先提炼出去。

  function renderPerson(outStream, person) {
 outStream.write(`<p>${person.name}</p>\n`);
 renderPhoto(outStream, person.photo);
 emitPhotoData(outStream, person.photo);
}

function listRecentPhotos(outStream, photos) {
 photos
  .filter(p => p.date > recentDateCutoff())
  .forEach(p => {
   outStream.write("<div>\n");
   emitPhotoData(outStream, p);
   outStream.write("</div>\n");
  });
}

function  emitPhotoData(outStream, photo) {
 zztmp(outStream,  photo);
 outStream.write(`<p>location: ${photo.location}</p>\n`);
}

function zztmp(outStream, photo) {
 outStream.write(`<p>title: ${photo.title}</p>\n`);
 outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}

新提炼出来的函数一般只会短暂存在,因此我在命名上不需要太认真,不过,取个容易搜索的名字会很有帮助。提炼完成后运行一下测试,确保提炼出来的新函数能正常工作。

接下来,我要对 emitPhotoData 的调用点逐一应用内联函数(115)。先从 renderPerson 函数开始。

  function renderPerson(outStream, person) {
 outStream.write(`<p>${person.name}</p>\n`);
 renderPhoto(outStream, person.photo);
 zztmp(outStream,  person.photo);
 outStream.write(`<p>location: ${person.photo.location}</p>\n`);
}
function listRecentPhotos(outStream, photos) {
 photos
  .filter(p => p.date > recentDateCutoff())
  .forEach(p => {
   outStream.write("<div>\n");
   emitPhotoData(outStream, p);
   outStream.write("</div>\n");
  });
}

function emitPhotoData(outStream, photo) {
 zztmp(outStream, photo);
 outStream.write(`<p>location: ${photo.location}</p>\n`);
}

function zztmp(outStream, photo) {
 outStream.write(`<p>title: ${photo.title}</p>\n`);
 outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}

然后再次运行测试,确保这次函数内联能正常工作。测试通过后,再前往下一个调用点。

  function renderPerson(outStream, person) {
 outStream.write(`<p>${person.name}</p>\n`);
 renderPhoto(outStream, person.photo);
 zztmp(outStream,  person.photo);
 outStream.write(`<p>location: ${person.photo.location}</p>\n`);
}

function listRecentPhotos(outStream, photos) {
 photos
  .filter(p => p.date > recentDateCutoff())
  .forEach(p => {
   outStream.write("<div>\n");
   zztmp(outStream, p);
   outStream.write(`<p>location: ${p.location}</p>\n`);
   outStream.write("</div>\n");
  });
}

function emitPhotoData(outStream, photo) {
 zztmp(outStream, photo);
 outStream.write(`<p>location: ${photo.location}</p>\n`);
}

function zztmp(outStream, photo) {
 outStream.write(`<p>title: ${photo.title}</p>\n`);
 outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}

至此,我就可以移除外面的 emitPhotoData 函数,完成内联函数(115)手法。

function renderPerson(outStream, person) {
 outStream.write(`<p>${person.name}</p>\n`);
 renderPhoto(outStream, person.photo);
 zztmp(outStream,  person.photo);
 outStream.write(`<p>location: ${person.photo.location}</p>\n`);
}

function listRecentPhotos(outStream, photos) {
 photos
  .filter(p => p.date > recentDateCutoff())
  .forEach(p => {
   outStream.write("<div>\n");
   zztmp(outStream, p);
   outStream.write(`<p>location: ${p.location}</p>\n`);
   outStream.write("</div>\n");
  });
}

function emitPhotoData(outStream, photo) {
 zztmp(outStream, photo);
 outStream.write(`<p>location: ${photo.location}</p>\n`);
}

function zztmp(outStream, photo) {
 outStream.write(`<p>title: ${photo.title}</p>\n`);
 outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}

最后,我将 zztmp 改名为原函数的名字 emitPhotoData,完成本次重构。

function renderPerson(outStream, person) {
 outStream.write(`<p>${person.name}</p>\n`);
 renderPhoto(outStream, person.photo);
 emitPhotoData(outStream, person.photo);
 outStream.write(`<p>location: ${person.photo.location}</p>\n`);
}

function listRecentPhotos(outStream, photos) {
 photos
  .filter(p => p.date > recentDateCutoff())
  .forEach(p => {
   outStream.write("<div>\n");
   emitPhotoData(outStream, p);
   outStream.write(`<p>location: ${p.location}</p>\n`);
   outStream.write("</div>\n");
  });
}

function emitPhotoData(outStream, photo) {
 outStream.write(`<p>title: ${photo.title}</p>\n`);
 outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}