OOP 何必是 COP


tl;dr: 面向对象编程(Object-oriented programming)不必是面向类编程(Class-oriented programming)

像 JavaScript 这种,声明对象比声明类还方便的语言中,何必非要定义一个类,再定义实例呢?

从“和面向过程相对”的意义上说,“面向对象”指的是,封装的时候,去封装一个名词而不是封装一个动词。至于实现上,是去实现一个类,还是实现一个接口,其实都是 OOP


经常看到言论说 JavaScript 正在远离 OOP,事实上我觉得被远离的只是“OOP原教旨主义”,Java 之流那种

什么意思呢,我觉得 interface 就是定义了一个对象应该是什么 shape,比如,如果我是一个瞎子,只靠声音认狗狗🐕,只要你能汪汪叫就算你是狗 U•ェ•*U

那传统的继承式 OOP 就要求你老老实实界门纲目科属种,你得是继承了动物界、脊椎动物门、哺乳动物纲、食肉目、犬科、犬属,但接口式的 OOP 只要求你能狗叫就行了,管你是 京中有善口技者,还是音乐播放器,你都是狗。基于接口的 OOP 很显然就是更进步的、实用主义的 OOP


interface DogLike {
  bark: () => "Woff"
};

function getDog(): DogLike {
  return {
    bark() {
      return "Woff"
    }
  }
}

const dog = getDog()

dog.bark()

这是面向过程吗?这也是面向对象。只要你在使用的时候不是在使用 doBark(dog) 而是 dog.bark() 你就是在 OOP 了。不需要用 class 只要你是在操作一个主体的方法,就是在 OOP

相对的,像这种

class Dog {
  bark(): "Woff"
};

function doBark(dog: Dog) {
  return dog.bark()
}

dog = Dog()

doBark(dog)

这种才是披着面向对象外皮的面向过程编程。


是的,在我的定义中,Rust 也可以是 OOP。


说起来,前两天我一个给 Gradio 的 PR 被合了:

这个 PR 也挺好玩的,起因是我发现 Gradio 5 实现热重载了,我一直挺关心热重载的,包括我 py3.online 上也自己实现了一套热重载。我看它代码时发现它的方法差不多(都是在一个模块的 __dict__ 中 exec 它的 sources),还支持一个 NO_RELOAD 的常量,if gr.NO_RELOAD 块中可以执行一些不想被热重载重新执行的代码。我觉得还蛮有意思的。然后我就在想:这个值是怎么做到动态的呢?是在热重载的时候直接改这个值,从 True 改成 False 吗?那如果人家不是用 gr.NO_RELOAD 而是直接用 from gradio import NO_RELOAD 这样的方式导入的呢?这样不就失去响应性了吗?

后来一读源码,发现更离谱,它甚至不是动态的 bool,而是通过 AST 搜索 if gr.NO_RELOAD 这样的出现,然后把整个 if 语句都删掉。我觉得这也太 hack 了,因为这导致这个 if 语句没法用 and / or 之类的结构,也没法用别名,甚至 else 子句也删了,也没法把这个 bool 当参数传给别处。而且这些,在文档里都没有描述。害人不浅啊(具体的坏处我 PR 里描述的更清楚)

于是,我的实现,简单来说就是把它编程一个伪 bool,让它的 __bool__ 是动态的就行了。简单的话直接用 list 之类的东西就行,我是继承了 bool 类(简单来说是这样,但是 Python 里 bool 不让继承,所以其实我继承的是 int)(插播一条冷知识,Python 中 bool 也是 int 的子类哦),虽然我这样也 hack,但我至少不会造成奇奇怪怪的 bug

class DynamicBoolean(int):
    def __init__(self, value: int):
        self.value = bool(value)

    def __bool__(self):
        return self.value

    def set(self, value: int):
        self.value = bool(value)

本来以为他们会不会对这个命名有异议的,但是居然没有。看来他们对贡献挺不 opinionated 的,毕竟也是一个 utils.py 文件 1500+ 行的项目。哈哈哈