模块、进程与工作流:构建复用的核心三角
我第一次写Nextflow的时候,其实挺懵的。
文档里到处都是process、workflow,偶尔还蹦出个module。这仨东西到底啥关系?说白了,它们是Nextflow世界里三个不同层级的砖块。你弄清楚了,复用就变得像搭积木一样自然。
进程(Process):最小执行单元
进程是干活的最小单位。它封装了一个任务:你要跑什么命令、用什么镜像、需要多少内存。每个进程的输出就是它的产出,可以是文件、数值或元数据。
我习惯把进程看作一个工种——比如“质检员”。一个质检员只负责一件事:检查输入,产出报告。
// 进程示例:一个简单的QC步骤
process FASTQC {
input:
path(reads)
output:
path("${reads.baseName}_fastqc.html"), emit: html_report
script:
"""
fastqc ${reads} -o ./
"""
}
这里有个关键点:进程的复用粒度很细。你可以在不同工作流里反复调用同一个进程,只要它的输入输出签名匹配。我在项目中经常把FASTQC这种进程放在单独的.nf文件里,其他工作流直接include进来用,省心。
模块(Module):进程的集合与组织
模块比进程大一个层级。它是一组相关进程的集合,通常围绕同一个生物信息学步骤(比如“变异检测”)来组织。模块不定义执行逻辑,它只负责打包和导出。
你想想看,如果你在一个大项目里把所有进程都堆在一个文件里,后期维护得多痛苦。模块就是帮你分门别类的工具。
举个例子,我有个qc.nf模块,里面放了FASTQC、MultiQC、以及一个汇总日志。
// qc.nf — 模块文件,打包进程
process FASTQC {
// ...
}
process MultiQC {
// ...
}
外部工作流通过include { FASTQC as RunFastQC } from './qc'来导入。注意as关键字:你可以给导入的进程起别名,避免名字冲突。我在做一个多中心项目时,有几个中心自带了QC流程,我加了别名来区分,程序一点没乱。
工作流(Workflow):顶级编排者
工作流是最高层级的组织者。它定义数据从哪来、经过哪些进程、最终产出什么。你可以把工作流理解为“施工蓝图”。它不直接干活,但告诉所有进程什么时候启动、怎么连接。
Nextflow里可以定义多个工作流,也能嵌套子工作流。这正是复用的核心——你写一个通用的align_and_call子工作流,在多个主工作流里复用它。
// 子工作流定义
workflow ALIGN_AND_CALL {
take:
reads
reference
main:
BWA_MEM(reads, reference) | { bam ->
SAMTOOLS_SORT(bam) | { sorted_bam ->
FREEBAYES(sorted_bam, reference)
}
}
}
// 主工作流调用
workflow {
ch_reads = Channel.fromPath("data/*.fastq")
ch_ref = file("ref.fa")
ALIGN_AND_CALL(ch_reads, ch_ref)
}
你看,主工作流里只需要一行ALIGN_AND_CALL(ch_reads, ch_ref)就完成了对齐+变异检测的完整流程。子工作流的内部细节被完全隐藏了。我曾在单细胞项目中把预处理、比对、定量写成一个子工作流,然后在不同版本的分析管线里复用,改动的只是入口参数。
三者关系:一幅结构图
我把它们的关系画成一张图,你对照着看会更清楚:
图里的流向很清楚:主工作流调用子工作流,子工作流包含多个模块,每个模块打包一组进程。反过来看,进程是复用的最底层砖块;模块是进程的集合,方便批量导出;工作流是顶层蓝图,负责编排调度。
复用层级对比表
我把这三者在复用时的核心差异整理成了表格,方便你快速对比。
| 复用维度 | 进程 | 模块 | 工作流 |
|---|---|---|---|
| 复用粒度 | 单一步骤(如FASTQC) | 步骤集合(如QC模块) | 完整分析管线(如RNA-Seq) |
| 复用方式 | include { processName } |
include { process as NewName } from './module.nf' |
workflow SubWF { ... } + 直接调用 |
| 注册单元 | process关键字 |
.nf文件(无关键字) |
workflow关键字 |
| 数据抽象 | 通过input/output签名 |
无直接定义,依赖导出进程的签名 | 通过take/emit定义 |
| 开发者心智模型 | 单一功能函数 | 功能模块库 | 微服务/子应用 |
你看,复用粒度越小,越灵活,但也容易变得零碎。粒度越大,复用起来越省事,但不够灵活。我一般建议:把通用、稳定的步骤做成子工作流;把逻辑相关的几组步骤打包成模块;把最常用的小工具做成进程。这样分层,你从哪一层复用都有的放矢。
一个更具体的例子
假设你在做单细胞RNA-seq分析。你写了一个SC_PREPROCESS子工作流,里面包含CELLRANGER(进程)、QUALITY_REPORT(进程)。这个子工作流被MAIN_WORKFLOW调用。当你有新的批次数据进来,只需要改一下输入路径,复用整个子工作流就完事了。如果某天你换了细胞计数工具,只需替换SC_PREPROCESS内部进程,外部调用的地方完全不用动。
这就是我理解的工作流复用基础。弄清楚了它们的关系,后面的模块化设计才有根基。你可能会问:“那我什么时候用模块,什么时候用子工作流?” 我的经验是——当你想隐藏一组进程的实现细节时,就用子工作流;当你只想组织代码、方便查找时,就用模块。