Slime异步原理(单例设计模式)4
你又提出了一个非常好的问题,这涉及到代码的组织结构和职责划分,是衡量代码设计优劣的重要方面。
将 submit_generate_tasks 函数放在 GenerateState 类下,而不是作为一个独立的全局函数,主要有以下几个原因:
1. 职责单一与高内聚(Single Responsibility & High Cohesion)
GenerateState 类的核心职责是管理所有与生成过程相关的状态 。这不仅包括像 tokenizer 和 semaphore 这样的静态资源,也包括像 pendings(待处理任务集合)和 remaining_batch_size(剩余批次大小)这样的动态状态。
submit_generate_tasks函数的核心作用是什么?- 它接收一批样本 (
samples)。 - 为这批样本创建
asyncio.Task。 - 将这些任务添加到
self.pendings集合中进行跟踪。 - 更新
self.remaining_batch_size计数。
- 它接收一批样本 (
可以看到,这个函数的操作与 GenerateState 内部的状态变量 (pendings, remaining_batch_size) 紧密相关 。将函数和它操作的数据放在一起,是"高内聚"原则的体现。这使得 GenerateState 成为一个完整、独立的组件,负责管理从任务提交到状态跟踪的整个生命周期。
反例:如果不放在类里会怎么样?
假设我们把它作为一个全局函数:
python
# 全局函数版本
def submit_generate_tasks_global(state: GenerateState, samples: list[list[Sample]]):
for group in samples:
state.pendings.add(
asyncio.create_task(...)
)
state.remaining_batch_size += len(samples)
# 在其他地方调用
state = GenerateState(args)
samples = data_source(...)
submit_generate_tasks_global(state, samples)
这样做虽然功能上可行,但有几个缺点:
- 职责不清晰 :
GenerateState变成了纯粹的数据容器,而操作这些数据的逻辑却散落在外部,破坏了类的封装性。 - 增加了调用复杂度 :每次调用都需要同时传递
state对象和samples,而作为类方法,只需要传递samples(self是隐式传递的)。 - 维护困难 :如果未来
GenerateState内部状态管理的方式发生改变(比如pendings从set变成了dict),你不仅要修改GenerateState类,还需要去查找并修改所有像submit_generate_tasks_global这样直接操作其内部状态的外部函数。
2. 封装与信息隐藏(Encapsulation & Information Hiding)
通过将 submit_generate_tasks 作为 GenerateState 的一个方法,外部调用者(比如 generate_rollout_async 函数)只需要知道"如何提交任务",而不需要关心"任务是如何被跟踪和管理的"。
- 外部视角 :
state.submit_generate_tasks(samples)------ 这是一个清晰的指令,意为"状态管理器,请帮我处理这批任务"。 - 内部实现 :这个方法内部具体是把任务加到一个
set里,还是一个list里,还是一个更复杂的数据结构里,对于外部调用者来说是透明的。这种信息隐藏使得代码更容易维护和重构。
如果未来我们想增加更复杂的逻辑,比如在提交任务时进行优先级排序,或者记录任务的提交时间,我们只需要修改 submit_generate_tasks 这个方法内部的实现,而所有调用它的代码都无需改动。
3. 代码可读性与面向对象思想
将相关的功能组织在同一个类中,使得代码结构更清晰,更符合人类的思维习惯。当我们阅读 GenerateState 类的代码时,我们可以一目了然地看到它包含了哪些状态 (__init__),以及可以对这些状态执行哪些操作(reset, submit_generate_tasks)。
这就像一个"任务管理器"对象,它有自己的属性(任务列表、剩余任务数),也有自己的行为(提交任务、重置状态)。这比将属性和行为分散在代码各处要更容易理解。
总结
将 submit_generate_tasks 放在 GenerateState 类下,是遵循良好软件设计原则的体现:
| 原则 | 在此处的体现 |
|---|---|
| 高内聚 | 函数和它紧密操作的数据(pendings, remaining_batch_size)被组织在一起。 |
| 低耦合 | 外部代码通过一个稳定的接口与状态管理器交互,而不需要了解其内部实现细节。 |
| 封装性 | 状态管理的具体逻辑被隐藏在类的方法内部,保护了内部状态的完整性。 |
| 职责单一 | GenerateState 作为一个整体,负责"管理生成过程的状态"这一单一职责。 |
所以,这个设计决策让 GenerateState 不仅仅是一个被动的数据结构,而是一个主动的、有行为的状态管理器,从而使整个系统的代码更加健壮、清晰和易于维护。