浅谈oop概念
从一个例子开始:
class Student: def __init__(self, name: str, student_id: int): self.name = name self.student_id = student_id self.grades = { "语文":0, "数学":0, "英语":0, }
def set_grade(self, course: str ,grade: int): if cource in self.grades: self.grade[course] = grade
def print_grades(self): print(f"学生{self.name}(学号:{self.student_id})") for course in self.grades: print(f"{course}: {self.grades[course]}分")好了,如你所见,这是一个在初学OOP的时候,一个非常标准的类写法。
调用时,我们就得输入student = Student("name","1")来构造新对象,为了设置成绩,student.set_grade,为了打印成绩,你就得student.print_grades了,咋一看,没啥问题,但是,问题就来了,脱离上下文语境,我是不是可以理解成:学生自己设置成绩,学生自己打印自己成绩?是不是觉得不太对劲?确实。学生是被操作的对象,set_grade应该是管理人员来设置,print_grades应当是别人来打印。学生只是参与者。那么,答案便是:它未必已经是完整意义上的上帝类,但它已经具备上帝类的胚胎:把学生身份、成绩记录、成绩修改、展示输出混在了同一个对象里。听起来很反直觉,对吗?但事实确实如此,现实情况下,学生能打印自己成绩,设置自己成绩时,已经可以说明,这个东西设计方向就是错误的。
因此,我们换个写法
class Student: def __init__(self, name: str, student_id: int): self.name = name self.student_id = id self.grades = { "语文":0, "数学":0, "英语":0, }
def set_grade(student: Student, course: str ,grade: int): if cource in student.grades: self.grade[course] = grade
def print_grades(student: Student): print(f"学生{student.name}(学号:{student.student_id})") for course in student.grades: print(f"{course}: {student.grades[course]}分")看起来只是把类里的函数放在外面了,但是,区别就出来了:
学生成了被执行对象,语义比先前好很多了。理解成本立即降低,不要小瞧这个变化,至少,这个方法就脱离了上帝类这个危险的东西,但是,事实上,这样表达还是不够好,print_grades()还是觉得奇怪,不过作为开头用例,我觉得,这样改就够了。
有些人会说:学生能打印自己成绩,我下个成绩表,不就是打印自己成绩了?不,学生只是向教务系统发起请求打印这个成绩的人,此时成绩是记录,成绩单是报告,打印是输出行为,跟学生一点关系都没有,student.print_grades()说明这个东西和学生已经深度绑定了,显然不对。
那么OOP的实质是什么呢?不是对象有方法,而是,在给定问题情境下正确建模。
那什么是正确建模呢?
我认为,能把现实问题里的概念、关系、规则、执行者、边界分清楚,然后让代码结构服从这个划分时,就是正确建模了。
也就是:不因名词就建类,不因动词就塞方法。
我们从一个例子开始说起:
消息处理函数、机器人初始化函数、接收频道事件的类等一些其他的工具函数都写在了 core.py 下,这个文件在后面一度达到了 1000 行。
随着 NewBing、逆向 ChatGPT 的出现,并且它们适用的机器人指令不相同,我开始注意到需要对项目进行分层了。
我将大语言模型提供商抽象为了 Provider 类,并创建 Command 类用于表示机器人指令,每一个大语言模型提供商都拥有一个 Command 子类。这样,只要配合配置文件选择使用不同的提供商,消息处理函数就可以很清晰地去调用它们。此外,在后面的更新中,为了接入 QQ 群,我还引入了 Platform 类,以表示不同的消息平台。
从这个阶段开始,我其实才真正对 OOP 有一个比较“清晰”的认识。那我问:
什么是Provider?
如果 Provider 的意思是“大语言模型提供商实例”,那么它真正应该负责的事情只有一件:
接收统一的请求,调用外部模型,返回统一的结果。
也就是:
class Provider(Protocol): async def generate(self, request: GenerateRequest) -> GenerateResult: ...它是执行层,是士兵,不是长官。
可是,如果每一个大语言模型提供商都拥有一个 Command 子类,那么问题就来了:
为什么指令可以在Provider里?
机器人指令是用户和机器人系统之间的交互协议,不是某个模型提供商的私产。
NewBing 有一套指令,逆向 ChatGPT 有一套指令,这个现象说明的不是“每个 Provider 应该拥有自己的 Command 子类”,而是说明:
系统里缺少一个独立的命令层,缺少一套能力声明机制。
也就是说,Command 不应该被 Provider 吞掉。
如果 Provider 开始拥有 Command,那么 Provider 就不再只是模型适配器了。它开始知道机器人指令,知道用户入口,知道业务流程。这样继续发展下去,Provider 很快就会从“模型调用者”变成“小型朝廷”。
这和 student.print_grades() 是同一个问题。
表面上看:
学生有成绩,所以 Student 里写 print_gradesProvider 有不同指令,所以 Provider 里写 Command但本质上都是:
相关,不等于归属。
学生和成绩相关,不代表学生负责打印成绩单。 Provider 和某些指令相关,不代表 Provider 负责定义机器人指令。
真正的 OOP 不是看到几个名词就建几个类,也不是把原来 core.py 里的函数搬进 Provider、Command、Platform 这些类里。真正的 OOP 是判断:
谁是入口?谁是协议?谁是调度者?谁是执行者?谁是外部适配?谁拥有业务规则?谁只是被调用?如果这些问题没问清楚,那么所谓“我开始对 OOP 有了清晰认识”,很可能只是:
从一个 1000 行 core.py,进化成多个隐藏上帝类。
这不是建模完成了,只是混乱换了一个形状。
这个反例真正暴露的问题是:
他意识到了需要分层,但还没有真正理解每一层的权力边界。
一旦 Provider 拥有 Command,执行层就开始越权。 一旦 Platform 参与业务决策,入口层就开始越权。 一旦 core.py 塞消息处理、机器人初始化、频道事件、工具函数,core 就不再是 core,而是垃圾场。
所以 OOP 的实质不是“我抽了几个类”。
OOP 的实质是:
我是否把对象、关系、协议、执行、调度、入口放在了正确的位置。
那么,什么才是真正的 OOP?
真正的 OOP,不是 class,不是 self,不是继承,也不是把一堆函数塞进类里。
真正的 OOP 是:
用对象表达系统中的稳定概念,并让行为服从正确的职责边界。
也就是说,写 OOP 之前,第一件事不是问:
这个类要写哪些方法?而是问:
这个对象到底代表什么?它拥有哪些本体属性?哪些只是外部系统赋予它的关系?哪些行为真正属于它?哪些行为只是“和它有关”,但不该由它负责?例如,Student 表达的是学生身份。它可以有姓名、学号,但“设置成绩”不应该天然属于学生自己,因为成绩是教务系统管理的记录;“打印成绩单”也不应该属于学生自己,因为那是展示层或报告服务的工作。
所以,student.print_grades() 表面上是 OOP,实际上是把实体、成绩记录、报告生成、展示输出混在了一起。
真正的 OOP 应该先拆模型:
Student 学生身份GradeBook 成绩记录GradeManager 成绩管理GradeReport 成绩单GradeFormatter 成绩单格式化Printer / UI 输出入口这样,代码语义才会变成:
report = grade_report_service.create_report(student_id)text = format_grade_report(report)print(text)这时,学生是被管理、被查询、被展示的对象,而不是自己给自己设置成绩、自己打印成绩的上帝类。
OOP 的核心,不是“对象能点方法”,而是:
每个对象只承担它应该承担的职责。
同理,ATM 能取钱,不代表 ATM 类应该自己完成验密、扣款、风控、记账、出钞、打印凭条。
ATM 本体可能只需要一个设备编号。取款能力是协议,执行过程来自操作系统、驱动、硬件部件和银行核心系统的协作。
所以正确模型应该是:
ATMDevice ATM 设备本体ATMController 终端流程编排CardReader 读卡器PinPad 密码键盘CashDispenser 出钞模块BankService 银行核心服务Transaction 交易记录ATMDeployment ATM 部署关系这才是建模。
再看大语言模型 Provider。Provider 能生成回复,不代表它应该自己构造 payload、处理图片音频、决定工具调用、控制 retry、做 fallback、解析 stream、统计 usage、保存消息。
Provider 的职责应该很窄:
class ModelProvider(Protocol): async def generate(self, request: GenerateRequest) -> GenerateResult: ...它只是执行者。真正的调度者应该是 application / orchestrator。
所以,真正的 OOP 可以总结成几句话:
本体归本体。关系归关系。协议归协议。规则归规则。执行归执行。展示归展示。存储归存储。入口归入口。最重要的一句话是:
相关,不等于归属。
成绩和学生相关,但成绩单打印不属于学生。 支行和 ATM 相关,但支行不是 ATM 本体属性。 模型调用和 Provider 相关,但业务调度不属于 Provider。 碰撞和得分相关,但碰撞函数不应该直接渲染浮动文字。
真正的 OOP,就是不断追问:
这个行为到底属于谁?这个属性到底是谁的?这个变化以后应该改哪里?这个对象有没有越权?如果这些问题没有问清楚,那么写再多 class,也只是披着 OOP 外衣的过程式代码。
所以,OOP 的实质不是“用类组织代码”。
你能写出 class,只是语法入门。 你能判断一个行为该不该属于这个类,才是 OOP 入门。
参考文献: [1] Soulter.《AstrBot 的架构设计转变——消息处理篇》. Soulter’s Blog, 2025-01-23.
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!