搬移语句到函数 / 搬移语句到调用者
搬移语句到函数(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 如果为了它加一个 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>']怎么一步步做
Section titled “怎么一步步做”搬移语句到函数:
- 如果重复的语句和函数调用不挨着,先用移动语句(第 25 章)把它们挪到紧挨着调用的位置。
- 如果这个函数只有这一个调用方,直接把语句剪切进函数,跑测试,结束。
- 调用方多时,用提炼函数(第 4 章)把“重复语句加原函数调用”一起提成一个新函数,起个临时名字。跑测试。
- 把每个调用方改成调用新函数,每改一个跑一次测试。
- 用内联函数把原函数内联进新函数,再把新函数改回原来的名字(第 6 章)。跑测试。
搬移语句到调用者:
- 只有一两个调用方时,直接把语句从函数里剪切出来,粘贴到每个调用方。跑测试。
- 调用方多时,先把函数里要留下的部分提炼成一个新函数,原函数变成“新函数加要搬走的语句”;再把原函数内联到每个调用方;最后把新函数改回原名。
定义与适用场景
Section titled “定义与适用场景”两个手法是一对:函数的边界应该落在“所有调用方都需要”的地方。调用方都在调用前后做同一件事,说明边界画小了,往里搬;只有部分调用方需要某段逻辑,说明边界画大了,往外搬。随着需求变化,这条边界会来回移动。
搬移语句到函数最常见于初始化和收尾:每次调用前都要先校验参数、每次调用后都要写日志、每次打开都要设置编码。搬移语句到调用者最常见于函数被复用到新场景时:新调用方想要的行为和原来的稍有不同。
真实系统里的例子
Section titled “真实系统里的例子”接口客户端里,每个调用方在请求前后都写了一行日志、算一次耗时。把它们搬进请求函数,调用方只管拿结果。下面用一个模拟的同步请求函数说明。
// 把每个调用方前后的日志搬进请求函数。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 之后,所有请求的日志一个样。
什么时候别用
Section titled “什么时候别用”几个调用点碰巧写了一样的几行,但它们是出于不同的原因,以后会朝不同方向变,这时合并进函数反而会在将来被迫拆开。合并之前问一句:这几行是不是“调用这个函数”这件事的一部分。
- 每个调用点都在调用前后写同样几行:边界画小了,把语句搬进函数。
- 只有部分调用方需要函数里的某段逻辑:边界画大了,把语句搬给调用者。
- 调用方多时都通过“提炼新函数、逐个迁移、内联、改名”几步完成,每步可测。
- 函数的边界会随需求来回移动,两个手法会交替使用。