浅谈oop概念

2585 字
13 分钟
浅谈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_grades
Provider 有不同指令,所以 Provider 里写 Command

但本质上都是:

相关,不等于归属。

学生和成绩相关,不代表学生负责打印成绩单。 Provider 和某些指令相关,不代表 Provider 负责定义机器人指令。

真正的 OOP 不是看到几个名词就建几个类,也不是把原来 core.py 里的函数搬进 ProviderCommandPlatform 这些类里。真正的 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.

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

浅谈oop概念
https://cloudcyrene.icu/posts/mypost/
作者
Evernight
发布于
2026-06-20
许可协议
CC BY-NC-SA 4.0
相关文章 智能推荐
暂无相关文章
随机文章 随机推荐
Profile Image of the Author
Evernight
Evernight
公告
欢迎来到我的博客!这是一则示例公告。
音乐
封面

音乐

暂未播放

0:00 0:00
暂无歌词
分类
标签
站点统计
文章
1
分类
1
标签
2
总字数
2,185
运行时长
0
最后活动
0 天前
站点信息
构建平台
Local
博客版本
Firefly v6.12.1
文章许可
CC BY-NC-SA 4.0

文章目录