435 lines
14 KiB
Markdown
435 lines
14 KiB
Markdown
---
|
||
title: "[Milky] 为您整理《面向架构编程范式,超越OOP和MVC》笔记 | BV1ArVU62Eac"
|
||
source: "milky@4ueo.com"
|
||
date: 2026-06-09 21:17
|
||
tags: [milky, bilibili, notes]
|
||
email_id: 2062
|
||
---
|
||
|
||
Milky 为您整理了《面向架构编程范式,超越OOP和MVC》 | BV1ArVU62Eac 笔记。
|
||
|
||
面向架构编程范式:超越 OOP 和 MVC
|
||
|
||
一、编程范式的本质
|
||
|
||
1.1 什么是编程范式
|
||
|
||
编程范式(Programming Paradigm)是指程序的表达方式,它不决定程序能实现什么功能,只决定能否方便、易懂地表达功能。
|
||
|
||
`
|
||
过程式表达:
|
||
f1(); f2(); f3();
|
||
|
||
面向对象表达:
|
||
obj.f1(); obj.f2(); obj.f3();
|
||
`
|
||
|
||
两段代码功能完全相同,都是先执行 F1,再执行 F2、F3。表达方式不同,但实现的功能是一样的。
|
||
|
||
关键结论:
|
||
面向对象能写的程序,过程式也能写
|
||
反过来也一样
|
||
无论用哪种范式,你的程序都能实现买东西这个功能
|
||
|
||
1.2 编程范式的真正目的
|
||
|
||
编程范式的根本目的是为了大规模代码和大规模团队分工合作。
|
||
|
||
当代码量膨胀到:
|
||
成千上万行
|
||
几十万行
|
||
上百万行
|
||
|
||
当团队有上百号程序员时,需要将整个项目拆分成多个模块,每个组负责一个模块。程序员需要调用其他人开发的模块,这就引出了模块封装隔离的概念。
|
||
|
||
1.3 模块封装隔离的概念
|
||
|
||
把模块的运行原理封装在模块内,只暴露接口。模块的调用者只需要会操作接口,不需要理解模块内部的运行原理,就能使用这个模块。
|
||
|
||
如果做不好封装隔离:
|
||
调用其他模块时,需要充分理解那个模块的内部原理
|
||
整个项目可能有成百上千个模块
|
||
需要懂上百个模块的内部原理,才能写自己的程序
|
||
这在实际项目中是不现实的
|
||
|
||
二、面向对象与模块封装
|
||
|
||
2.1 面向对象的封装优势
|
||
|
||
面向对象提供 class 语法,程序员可以很方便地把程序拆分成多个部分:
|
||
|
||
`python
|
||
class A:
|
||
def interface1(self): ...
|
||
def interface2(self): ...
|
||
|
||
class B:
|
||
def interface1(self): ...
|
||
def interface3(self): ...
|
||
|
||
class C:
|
||
def interface2(self): ...
|
||
def interface3(self): ...
|
||
`
|
||
|
||
每个类是一个模块,每个模块下有若干接口(可外部调用的函数)。实际项目中,每个类可能有几百上千行代码,好几十个函数接口,分别由不同程序员开发。
|
||
|
||
2.2 不只是面向对象在做模块封装
|
||
|
||
`python
|
||
过程式模块封装
|
||
module_a.py
|
||
def f1(): ...
|
||
def f2(): ...
|
||
|
||
module_b.py
|
||
def f3(): ...
|
||
|
||
main.py
|
||
import module_a
|
||
import module_b
|
||
module_a.f1()
|
||
module_b.f3()
|
||
`
|
||
|
||
效果和 class 一样。但这种方案太笨重了,比用类要复杂。
|
||
|
||
2.3 工程成本原则:"太麻烦,没人用"
|
||
|
||
工程问题和数学问题不同:
|
||
数学算法不需要考虑成本
|
||
工程问题最看重的就是成本
|
||
|
||
如果一个东西的使用成本大于收益,那根本就不会有人去用它。即使这是个好东西。
|
||
|
||
这会导致:
|
||
只有那些最重要的功能才能采用该方案
|
||
一些边边角角的小功能则完全没法用
|
||
用它反而会让程序更复杂
|
||
|
||
结论:面向对象比过程式高级,因为面向对象提供了一种方便的用于封装接口的语法。
|
||
|
||
2.4 class 的本质澄清
|
||
|
||
class 的本质是借口(Interface)。
|
||
|
||
一个 class 定义得好不好,取决于:
|
||
有没有做好功能的封装
|
||
有没有暴露出正确的接口
|
||
|
||
至于汽车有几个轮子、像不像鸭子,根本无关紧要。
|
||
|
||
正确的类比:手机开机按钮就是一个接口。按下开机键后,手机进行一系列复杂的初始化过程,而用户不需要懂这个过程,只要会按开机键就可以了。
|
||
|
||
三、面向对象的设计缺陷
|
||
|
||
3.1 继承的问题:代码无法复用
|
||
|
||
使用继承进行代码复用的问题:
|
||
|
||
`
|
||
A
|
||
/ \
|
||
B C
|
||
`
|
||
|
||
假设 F1、F2、F3 的代码写在 A 类中。用户只需要 F1 和 F3,无法复用这段代码——因为继承会把 F2 也带进来。
|
||
|
||
3.2 组合优于继承
|
||
|
||
组合思想:
|
||
`
|
||
Main
|
||
/ | \
|
||
A B C
|
||
`
|
||
|
||
子模块拆分成 A 类、B 类、C 类。写 Main 类的程序员需要 F1、F2、F3 中的哪个就组哪个,不需要的就不组。既简单又灵活,完胜继承链条。
|
||
|
||
组合是一种思想,并不局限于某种特定语法。
|
||
|
||
3.3 多重继承的致命问题:重名冲突
|
||
|
||
这是一个记账器例子,子模块 A 和子模块 C 有一个重名的函数 F1,这会造成冲突。
|
||
|
||
有人会说:把其中一个函数重命名为其他名字不就得了?
|
||
|
||
现实项目里没这么简单:
|
||
A 模块和 C 模块可能分别是两家公司开发的开源库
|
||
每个库可能有几万行代码
|
||
F1 函数的调用链可能非常复杂
|
||
如果要改名,就要改动整条调用链
|
||
项目还可能存在其他库依赖这两个库的命名
|
||
要改的话就要连同其他库一起改
|
||
|
||
真正要命的是:原库可能已经在业界运行过多年,安全性和稳定性有保证。改了源代码就要承担出 bug 的风险。
|
||
|
||
3.4 这种改动要求是不合理的
|
||
|
||
对比其他领域的例子:
|
||
显卡上有一个电容
|
||
主板上的电容可能和显卡上的电容重名或型号相同
|
||
不会产生命名冲突
|
||
|
||
工业产品的拆分逻辑都是组合。子模块中存在重名零件,根本不影响任何东西。这才是正确的封装逻辑。
|
||
|
||
全天底下各行各业,就面向对象搞特殊。
|
||
|
||
四、MVC 方案的局限
|
||
|
||
4.1 MVC 就是组合
|
||
|
||
MVC 把功能拆分成最细的 Model(数据)和 View(函数),再通过 Controller 组合起来:
|
||
|
||
`python
|
||
class MainController:
|
||
def init(self):
|
||
self.modelA = ModelA()
|
||
self.modelB = ModelB()
|
||
self.viewC = ViewC()
|
||
`
|
||
|
||
用哪个 model 或 view 函数就组哪个,不用的就不组。
|
||
|
||
虽然 A 和 B 存在重名函数,但是并不会产生冲突。
|
||
|
||
4.2 MVC 的问题:抽象泄露
|
||
|
||
在 Main 模块中初始化 A 和 B,并交换两者的指针时,存在一个问题:
|
||
|
||
A 和 B 的初始化逻辑泄露到 Main 这一层了。
|
||
|
||
在实际项目中:
|
||
A 和 B 可能是别的程序员写的底层库
|
||
Main 模块由业务层程序员写
|
||
底层库的逻辑本不应该泄露到业务层
|
||
|
||
正确的封装:每一层程序员将本层的逻辑封装在本层。不应该要求使用者理解本层逻辑,只要使用者会用即可。
|
||
|
||
4.3 初始化逻辑的复杂性
|
||
|
||
实际项目中初始化可能存在非常复杂的交互关系:
|
||
|
||
`
|
||
A 先初始化几步
|
||
把中间结果传给 B 初始化
|
||
再把中间结果传回给 A,继续初始化
|
||
`
|
||
|
||
另外,实际项目中子模块可能不止两个,很可能存在多个子模块初始化过程相互交织的情况。这要求 Main 程序员必须非常理解 A 和 B 等子模块的内部实现,才能写出 Main 层。
|
||
|
||
4.4 抽象泄露导致的问题
|
||
|
||
问题 1:模块无法替换
|
||
|
||
库存程序员无法写出一个模块 C 去替换模块 B,因为 Main 层耦合了模块 B 的初始化逻辑。模块 C 只要初始化逻辑稍有不同,就无法替换 B。
|
||
|
||
问题 2:中间层方案要么灵活性差,要么代码极为复杂
|
||
|
||
一种方案是加一个中间层(Middle Layer),中间层由 A 或 B 这一层的程序员编写。Main 层程序员无需理解中间层的内部实现。
|
||
|
||
但存在两种情况:
|
||
中间层代码最为简单,但灵活性差:Main 程序员无法选择组合哪几个子模块或者组合其他同接口的子模块。比如 Main 实现不了组合 A、C、D 三个模块,因为中间层只实现了 A、B 组合
|
||
让中间层适配,由 Main 层程序员自由选择组合哪些子模块:中间层的代码变得极为复杂
|
||
|
||
记住"太麻烦,没人用"原则。
|
||
|
||
4.5 依赖注入和微服务
|
||
|
||
依赖注入或微服务确实解决了抽象泄露问题。这两个方案都能进行模块替换。
|
||
|
||
衡量封装得好不好,可以看此模块能不能替换成同类模块。
|
||
|
||
唯一的问题是:这俩方案太重型了。使用成本太高。
|
||
|
||
五、时间悖论问题
|
||
|
||
5.1 时间悖论的根源
|
||
|
||
回到第一种方案,分析根本原因:
|
||
|
||
`
|
||
A 模块 ←→ B 模块
|
||
↓
|
||
Main 模块
|
||
`
|
||
|
||
这里存在一个时间悖论:
|
||
A 模块和 B 模块是先写出来的
|
||
Main 模块是在之后的某个时间点写出来的
|
||
|
||
如果想避免抽象泄露到 Main 层,程序员需要在 Main 类出现前表达交换 A 和 B 的指针这件事。然而,这做不到,因为 Main 只有在 A 和 B 存在后才出现。
|
||
|
||
5.2 语法限制导致的困境
|
||
|
||
但这是因为语法限制。假设编译器支持使用抽象名称作为指针名,其实是不存在时间悖论的。
|
||
|
||
六、新语法设计方案
|
||
|
||
6.1 核心语法设计
|
||
|
||
语法基本沿袭继承,但在调用函数或成员变量时,使用该成员的整个名称链:
|
||
|
||
| 符号 | 含义 |
|
||
|------|------|
|
||
| ..(两个点) | 向上级寻找 |
|
||
| ...(三个点) | 向平级寻找 |
|
||
| .(一个点) | 向下级寻找 |
|
||
|
||
也可以使用上斜杠、横杠和下斜杠表示。符号不重要,重要的是原理。
|
||
|
||
6.2 use 关键字
|
||
|
||
当名称列太长时,可以使用 use 关键字进行缩写:
|
||
|
||
`python
|
||
use module_a.b.c as abc
|
||
`
|
||
|
||
实际上,这个 use 关键字本质上和传统语言里包管理里的 include 或 import 是相同的东西。
|
||
|
||
6.3 super 关键字
|
||
|
||
代码中有个 super 关键字。ABCD 模块可能是先写出来的,Main 模块是未来的某个时间点写出来的。
|
||
|
||
所以写 ABCD 的时候,程序员是不知道未来那个上级模块叫什么名字。这时可以用 super 关键字表示任意名称的上级模块:
|
||
|
||
`python
|
||
class A:
|
||
def method(self):
|
||
super..call_something() # 向上级寻找
|
||
`
|
||
|
||
在这种写法下:
|
||
底层模块的逻辑封装在底层
|
||
每一层程序员无需理解底层模块,只要会组合就行了
|
||
可以任意替换同类模块
|
||
|
||
6.4 from 和 to 关键字
|
||
|
||
`python
|
||
from ModuleC import xxx as xxx_renamed
|
||
to ModuleX use some_function
|
||
`
|
||
|
||
from 和 to 关键字比看上去更重要,类似于主板设计师在 PCB 板上连同导线。这个动作是本编程范式下的基石。
|
||
|
||
6.5 模块重命名
|
||
|
||
有时子模块可能需要同类。那么可以像下面这样重命名:
|
||
|
||
`python
|
||
class MainB:
|
||
from ModuleA as mod_a
|
||
from ModuleB as mod_b
|
||
# 两个模块都有相同的接口,但名称不同时进行重命名
|
||
`
|
||
|
||
6.6 新语法的优势
|
||
|
||
在抽象泄露的情况下,是无法做到自由替换子模块的,因为上级模块耦合了下层实现。每个下层实现的逻辑不同,替换时要连带替换,泄露到内层的逻辑代码。
|
||
|
||
使用新语法后:
|
||
每层程序员可以任意组合 B 或 C
|
||
两者名称不同时,使用 from 关键字重命名即可
|
||
模块可以自由替换
|
||
|
||
七、核心原理:工业流水线思想
|
||
|
||
7.1 职能隔离的比喻
|
||
|
||
本语法的核心思想是仿照工业流水生产线生产工业品:
|
||
|
||
不同工段的工人是职能隔离的
|
||
不同工段的工人无需理解前一个工段
|
||
只需要会组装上一个工段传输过来的零件即可
|
||
同时,本工段的工人也应该将封装好的零件提供给后一个工段的工人,让其无需理解本工段逻辑
|
||
|
||
7.2 空间位置关系
|
||
|
||
拿主板和显卡的例子来做说明:
|
||
主板上的一个电容和显卡上的一个电容重名或型号相同时,不会产生名称冲突
|
||
原因在于,主板和显卡是通过三维空间位置寻找子零件
|
||
|
||
本语法中的全路径巡境表达的是对象成员的空间位置关系,相当于主板设计师在 PCB 板上按空间位置连同导线。
|
||
|
||
7.3 编译期依赖注入
|
||
|
||
本语法相当于在面向对象的继承语法的基础上,在编译期实现依赖注入。
|
||
|
||
本语法是对面向对象的语法的发展或改进,而非否定。
|
||
|
||
八、API 关键字与调用链耦合
|
||
|
||
8.1 基本语法存在的问题
|
||
|
||
但刚刚的基本语法存在一个问题:调用链耦合了其他模块的名称链。
|
||
|
||
看回例子:
|
||
模块 A 再调用模块 B 时,调用链耦合了 B 到 D 这个名称链
|
||
在实际项目中,模块 B 可能是其他公司开发的开源库
|
||
它写的名称链可能是 B → E → D
|
||
模块 A 的程序员是不可能要求 B 程序员按 A 的要求改名的
|
||
|
||
所以这种基本语法仅适用于模块内部,程序员可以完全控制代码的情况下。
|
||
|
||
8.2 API 关键字解决方案
|
||
|
||
当模块间进行交互时,需要另外一种封装机制:API 关键字。
|
||
|
||
`python
|
||
class ModuleA:
|
||
@API # 标记为公开接口
|
||
def public_interface(self): ...
|
||
|
||
def privatemethod(self): ... # 内部方法
|
||
`
|
||
|
||
本语法中不存在其他面向对象语言中的 public 和 private 关键字,而是使用 API 关键字控制可见性。
|
||
|
||
8.3 API 关键字的作用
|
||
|
||
当模块 M 的未标记为 API 时:
|
||
该 M 的成员变量和成员函数全是 public 公有
|
||
可被外部任意访问
|
||
|
||
使用 API 关键字可以:
|
||
精确控制哪些接口对外暴露
|
||
将调用链耦合封装在模块内部
|
||
允许外部替换同接口的模块而不影响内部实现
|
||
|
||
九、总结
|
||
|
||
9.1 编程范式演进路径
|
||
|
||
| 阶段 | 特点 | 问题 |
|
||
|------|------|------|
|
||
| 过程式 | 简单直接 | 代码膨胀后难以维护 |
|
||
| 面向对象 | class 语法便于封装 | 继承导致代码无法复用、重名冲突 |
|
||
| MVC | 组合思想 | 抽象泄露、初始化逻辑耦合 |
|
||
| 依赖注入/微服务 | 解决抽象泄露 | 太重型、成本高 |
|
||
|
||
9.2 新范式的核心要点
|
||
|
||
全路径巡境:通过名称链完整表达模块间的空间位置关系
|
||
编译期依赖注入:在编译阶段完成依赖关系的绑定
|
||
职能隔离:仿照工业流水线,每层程序员无需理解底层
|
||
模块可替换:衡量封装好不好的标准是能否自由替换同类模块
|
||
API 关键字:控制模块间交互的接口暴露
|
||
|
||
9.3 对面向对象的态度
|
||
|
||
本语法是对面向对象的语法的发展或改进,而非否定。它解决了:
|
||
继承的代码复用问题
|
||
多重继承的重名冲突问题
|
||
MVC 的抽象泄露问题
|
||
时间悖论导致的初始化耦合问题
|
||
|
||
|
||
相关讨论补充(来自弹幕):
|
||
边界条件处理和输入检查在任何范式下都需要考虑
|
||
用函数做 API 和用类做 API 区别挺大——类可以维护状态,函数式更纯粹
|
||
有些成员变量要维护的话,面向对象还是最合适的
|
||
|
||
──────────────────────────────
|
||
Generated by MilkyAi@Bilibili: https://space.bilibili.com/3461574540921489 |