Files
obsidian-notes/InBox/milky_BV1ArVU62Eac.md

435 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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