跳转到内容

搬移语句到函数 / 搬移语句到调用者

搬移语句到函数(Move Statements into Function)把每个调用点在调用前后都写着的同样几行搬进被调用的函数;搬移语句到调用者(Move Statements to Callers)反过来,把函数里只有部分调用方需要的语句搬出去,交给需要的调用方自己写。 它们解决的痛是:同样几行在每个调用点重复,改一处漏一处;或者一个函数被迫为不同的调用方做不同的事,越长越乱。

一个相册网站要在好几个页面显示照片信息:相册页、个人主页、搜索结果。每个页面都先输出一行拍摄地点,再调用 render_photo 输出标题和日期。

"""照片信息:搬移前。"""
def render_photo(photo: dict) -> list[str]:
return [f"标题:{photo['title']}", f"日期:{photo['date']}"]
def album_page(photo: dict) -> list[str]:
return [f"地点:{photo['location']}", *render_photo(photo)]
def profile_page(photo: dict) -> list[str]:
return [f"地点:{photo['location']}", *render_photo(photo)]
PHOTO = {"title": "外滩夜景", "date": "2026-10-01", "location": "上海"}
if __name__ == "__main__":
print(album_page(PHOTO))
print(profile_page(PHOTO))
# 输出:
# ['地点:上海', '标题:外滩夜景', '日期:2026-10-01']
# ['地点:上海', '标题:外滩夜景', '日期:2026-10-01']
// 照片信息:搬移前。
interface Photo {
title: string;
date: string;
location: string;
}
function renderPhoto(photo: Photo): string[] {
return [`标题:${photo.title}`, `日期:${photo.date}`];
}
function albumPage(photo: Photo): string[] {
return [`地点:${photo.location}`, ...renderPhoto(photo)];
}
function profilePage(photo: Photo): string[] {
return [`地点:${photo.location}`, ...renderPhoto(photo)];
}
const PHOTO: Photo = { title: "外滩夜景", date: "2026-10-01", location: "上海" };
console.log(JSON.stringify(albumPage(PHOTO)));
console.log(JSON.stringify(profilePage(PHOTO)));
// 输出:
// ["地点:上海","标题:外滩夜景","日期:2026-10-01"]
// ["地点:上海","标题:外滩夜景","日期:2026-10-01"]

如果现在要把“地点:“改成带地图图标的”📍 上海”,得在每个调用 render_photo 的页面里改一遍。现在是两个页面,搜索结果页上线后是三个。“显示照片信息”这件事在概念上包括地点,可代码把地点留在了每个调用方手里。这是重复代码。

"""照片信息:搬移后。"""
def render_photo(photo: dict) -> list[str]:
return [f"地点:{photo['location']}", f"标题:{photo['title']}", f"日期:{photo['date']}"]
def album_page(photo: dict) -> list[str]:
return render_photo(photo)
def profile_page(photo: dict) -> list[str]:
return render_photo(photo)
PHOTO = {"title": "外滩夜景", "date": "2026-10-01", "location": "上海"}
if __name__ == "__main__":
print(album_page(PHOTO))
print(profile_page(PHOTO))
# 输出:
# ['地点:上海', '标题:外滩夜景', '日期:2026-10-01']
# ['地点:上海', '标题:外滩夜景', '日期:2026-10-01']
// 照片信息:搬移后。
interface Photo {
title: string;
date: string;
location: string;
}
function renderPhoto(photo: Photo): string[] {
return [`地点:${photo.location}`, `标题:${photo.title}`, `日期:${photo.date}`];
}
function albumPage(photo: Photo): string[] {
return renderPhoto(photo);
}
function profilePage(photo: Photo): string[] {
return renderPhoto(photo);
}
const PHOTO: Photo = { title: "外滩夜景", date: "2026-10-01", location: "上海" };
console.log(JSON.stringify(albumPage(PHOTO)));
console.log(JSON.stringify(profilePage(PHOTO)));
// 输出:
// ["地点:上海","标题:外滩夜景","日期:2026-10-01"]
// ["地点:上海","标题:外滩夜景","日期:2026-10-01"]

搬移语句到函数:两个页面各自写的“地点”一行收进了 render_photo;搬移语句到调用者则是反方向

地点的格式只在 render_photo 里写一次,新页面调用它就自动带上地点。

半年后需求变了:搜索结果页要求地点显示在最后,并且用小字。render_photo 如果为了它加一个 location_last=True 参数,就开始为不同调用方做不同的事。这时该做反向手法,把地点那一行搬回调用者,render_photo 只管标题和日期,每个页面自己决定地点放哪、怎么显示:

"""照片信息:地点搬回调用者。"""
def render_photo(photo: dict) -> list[str]:
return [f"标题:{photo['title']}", f"日期:{photo['date']}"]
def album_page(photo: dict) -> list[str]:
return [f"地点:{photo['location']}", *render_photo(photo)]
def search_result(photo: dict) -> list[str]:
return [*render_photo(photo), f"<small>{photo['location']}</small>"]
PHOTO = {"title": "外滩夜景", "date": "2026-10-01", "location": "上海"}
if __name__ == "__main__":
print(album_page(PHOTO))
print(search_result(PHOTO))
# 输出:
# ['地点:上海', '标题:外滩夜景', '日期:2026-10-01']
# ['标题:外滩夜景', '日期:2026-10-01', '<small>上海</small>']

搬移语句到函数:

  1. 如果重复的语句和函数调用不挨着,先用移动语句(第 25 章)把它们挪到紧挨着调用的位置。
  2. 如果这个函数只有这一个调用方,直接把语句剪切进函数,跑测试,结束。
  3. 调用方多时,用提炼函数(第 4 章)把“重复语句加原函数调用”一起提成一个新函数,起个临时名字。跑测试。
  4. 把每个调用方改成调用新函数,每改一个跑一次测试。
  5. 用内联函数把原函数内联进新函数,再把新函数改回原来的名字(第 6 章)。跑测试。

搬移语句到调用者:

  1. 只有一两个调用方时,直接把语句从函数里剪切出来,粘贴到每个调用方。跑测试。
  2. 调用方多时,先把函数里要留下的部分提炼成一个新函数,原函数变成“新函数加要搬走的语句”;再把原函数内联到每个调用方;最后把新函数改回原名。

两个手法是一对:函数的边界应该落在“所有调用方都需要”的地方。调用方都在调用前后做同一件事,说明边界画小了,往里搬;只有部分调用方需要某段逻辑,说明边界画大了,往外搬。随着需求变化,这条边界会来回移动。

搬移语句到函数最常见于初始化和收尾:每次调用前都要先校验参数、每次调用后都要写日志、每次打开都要设置编码。搬移语句到调用者最常见于函数被复用到新场景时:新调用方想要的行为和原来的稍有不同。

接口客户端里,每个调用方在请求前后都写了一行日志、算一次耗时。把它们搬进请求函数,调用方只管拿结果。下面用一个模拟的同步请求函数说明。

// 把每个调用方前后的日志搬进请求函数。
const logs: string[] = [];
let clock = 0;
const now = () => (clock += 12);
function fakeFetch(url: string): number {
return url.includes("missing") ? 404 : 200;
}
function request(url: string): number {
const start = now();
logs.push(`→ GET ${url}`);
const status = fakeFetch(url);
logs.push(`← ${status} ${url}(${now() - start}ms)`);
return status;
}
function loadUser(): number {
return request("/api/user");
}
function loadMissing(): number {
return request("/api/missing");
}
console.log(loadUser(), loadMissing());
console.log(logs.join("\n"));
// 输出:
// 200 404
// → GET /api/user
// ← 200 /api/user(12ms)
// → GET /api/missing
// ← 404 /api/missing(12ms)

原来 loadUser、loadMissing 各自写着开始和结束两行日志,日志格式不统一,有的忘了记耗时。搬进 request 之后,所有请求的日志一个样。

几个调用点碰巧写了一样的几行,但它们是出于不同的原因,以后会朝不同方向变,这时合并进函数反而会在将来被迫拆开。合并之前问一句:这几行是不是“调用这个函数”这件事的一部分。

  • 每个调用点都在调用前后写同样几行:边界画小了,把语句搬进函数。
  • 只有部分调用方需要函数里的某段逻辑:边界画大了,把语句搬给调用者。
  • 调用方多时都通过“提炼新函数、逐个迁移、内联、改名”几步完成,每步可测。
  • 函数的边界会随需求来回移动,两个手法会交替使用。