企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持知识库和私有化部署方案 广告
反向重构:搬移语句到函数(213) ![](https://box.kancloud.cn/cbf672c2c1226712e2f3e55c2f716719_463x380.jpeg) ``` emitPhotoData(outStream, person.photo); function emitPhotoData(outStream, photo) {  outStream.write(`<p>title: ${photo.title}</p>\n`);  outStream.write(`<p>location: ${photo.location}</p>\n`); } ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` 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`); } ```