summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoreltrac <hi@eltr.ac>2026-06-04 19:23:46 +0800
committereltrac <hi@eltr.ac>2026-06-04 19:24:43 +0800
commit9ad5530153811b0b9a050deba8804bad587482dc (patch)
treec09f0c8d0b5f7fe4ad11c474b7aff44133cf4e2e
parent9b8237aa19ce966c55f24c34e2c4bbcfdf95d534 (diff)
[publish] /article/clos
[edit] rename common-lisp-oop.md -> clos.md
-rw-r--r--content/article/clos.md (renamed from content/article/common-lisp-oop.md)52
1 files changed, 31 insertions, 21 deletions
diff --git a/content/article/common-lisp-oop.md b/content/article/clos.md
index a8c3c3ac..800037fb 100644
--- a/content/article/common-lisp-oop.md
+++ b/content/article/clos.md
@@ -1,18 +1,16 @@
+++
-title = 'Just A Common Lisper II'
-subtitle = 'or an enthusiatic Java hater'
-slug = ''
-tags = []
-categories = ''
-date = 2026-06-03T10:19:22+08:00
-draft = true
+title = '从 CLOS 审视 Java 面向对象编程'
+subtitle = 'Just A Common Lisper II'
+tags = ["Lisp","编程"]
+categories = '代码炼金术'
+date = 2026-06-04 19:23:18
+draft = false
toc = true
+++
-书接[上回](http://localhost:1313/article/common-lisp-start/),前一篇文章讲了 Lisp 中的基本数据结构列表的构造方式(*cons pair*),以及
-由此衍生出的关联列表(`alist`)和属性列表(`plist`),非常优雅。这之后我就跑偏了,谈起了编译器和解释器中的「特例」以及宏,为什么看起来像是函数的形式实际上不是也不能是函数,最后浅谈了函数式编程和 `let` 的实现方式。
+书接[上回](http://localhost:1313/article/common-lisp-start/),前一篇文章讲了 Lisp 中的基本数据结构列表的构造方式(*cons pair*),以及由此衍生出的关联列表(`alist`)和属性列表(`plist`),之后我就跑偏了,谈起了编译器和解释器中的「特例」以及宏,为什么看起来像是函数的形式实际上不是也不能是函数,最后浅谈了函数式编程和 `let` 的实现方式。
-今天的文章就来关注 Common Lisp 的另一部分语言特性,即面向对象编程,也借此来谈谈我对 OOP *多少有些复杂*的看法。<!--more-->
+今天的文章~~继续跑偏~~,来关注 Common Lisp 的另一部分语言特性,即面向对象编程,也借此来谈谈我对 OOP *多少有些复杂*的看法。我将逐个审视 OOP 的三大要素——多态、封装和多态——再来反思面向对象设计模式。<!--more-->
## 不属于类的多态
@@ -202,7 +200,7 @@ public class Employee {
你可能会说,不想这么做的话,把实体类的属性设置成 `public` 就好了,不提供 getter 和 setter 方法不行吗?
-不行。这种拥有完整构造函数和 getter/setter 方法,封装良好的 Java 类,有一个专门的名字:Java Bean。主流的框架 Spring 就基于 Bean 构建(例如 [BeanFactory](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/BeanFactory.html) 接口),不用 Bean 的话就用不了 Spring 框架,不用 Spring 框架的话…… *天哪,那你为什么还要在 Java 生态里受苦?*
+不行。这种拥有完整构造函数和 getter/setter 方法,封装良好的 Java 类,有一个专门的名字:Java Bean。主流的框架 Spring 就基于 Bean 构建(例如 [BeanFactory](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/factory/BeanFactory.html) 接口),不用 Bean 的话就用不了 Spring 框架,不用 Spring 框架的话…… *天哪,那你为什么还要在 Java 生态里受苦?~~除非你要开发 Minecraft Mod,那样的话,我表示敬佩。~~*
为什么我讨厌 Java Bean 呢?封装性有什么坏处吗?在回答这个问题之前,需要反思封装是用来干什么的。
@@ -235,7 +233,7 @@ public class Employee {
(person-name p1)
;; => "Tom"
-(person-age)
+(person-age p1)
;; => 0 (初始值)
```
@@ -269,7 +267,7 @@ public class Employee {
之所以需要单独的备忘录类,而且必须让源发器自己创建备忘录,正是因为「不能破坏类的封装性」。源发器的属性是私有的,其他类不能访问,所以需要 `createMemento()` 方法来创建备忘录,其他类也不能给它的私有变量赋值,所以需要 `restore()` 方法。
-值得注意的是,这里的 `state` 仅仅是简化描述,实际上源发器可能有许多内部状态,这些内部状态要被保存在备忘录类里的话,备忘录就要有一模一样的成员变量才行。这不仅涉及到「复制代码」,源发器和备忘录还是紧密耦合的。
+值得注意的是,这里的 `state` 仅仅是简化描述,实际上源发器可能有许多内部状态,这些内部状态要被保存在备忘录类里的话,备忘录就要有一模一样的成员变量才行。这不仅涉及到**重复代码**,源发器和备忘录还是紧密耦合的。
所有这些复杂性的引入,仅仅是因为:我们不能破坏类的封装性。
@@ -307,9 +305,9 @@ CLOS 中方法与类分离、不强制封装的设计,使得模块之间能保
因为在那本《设计模式》里,重要的原则之一就是:[Composition over inheritance](https://en.wikipedia.org/wiki/Composition_over_inheritance)(组合优先于继承)——也就是 CRP(组合复用原则)。
-一方面的原因是继承不够灵活,行为和结构是预先定义好的,不能像接口那样组合复用。比方说,假设有多种类型的账户,这些账户在计算价格时有不同的算法逻辑,比起创建 `Account` 类和各种类型的子类,更利于复用的模式是将算法和数据分离开来,创建 `Strategy` 接口和各种实现类,计算价格时由 `Account` 提供 `Strategy` 的实例,再调用其中的方法进行计算——这个叫[策略模式](https://en.wikipedia.org/wiki/Strategy_pattern)。
+一方面的原因是继承不够灵活,行为和结构是**自上而下**预先定义好的,所有数据和所有操作都绑定在一起,不利于复用其中的某些部分。比方说,假设有多种类型的账户,这些账户在计算价格时有不同的算法逻辑,比起创建 `Account` 类和各种类型的子类,更利于复用的模式是将算法和数据分离开来,创建 `Strategy` 接口和各种实现类,计算价格时由 `Account` 提供 `Strategy` 的实例,再调用其中的方法进行计算——这个叫[策略模式](https://en.wikipedia.org/wiki/Strategy_pattern)(Strategy Pattern)。
-另一方面,也是更重要的方面,是 Java 的**另一个语言限制**:不支持多继承。从某种程度上讲,策略模式的出现也是因为单继承的限制,组合复用比单向的继承更灵;另一个原因是 Java 不是函数式编程语言,不支持将函数作为第一公民(而且反射机制更麻烦吧)。
+另一方面,也是更重要的方面,是 Java 的**另一个语言限制**:不支持多继承。从某种程度上讲,策略模式的出现也是因为单继承的限制,组合复用更灵活;另一个原因是旧版本 Java 不支持函数式编程,而现在有了 Lambda 表达式也很少使用,因为 Lambda 不能用类定义。
既然在专门写给面向对象编程的书里,都出于复用性考虑,拒绝使用继承,那继承对 OOP 真的还必要吗?Go 语言支持 OOP,但并不支持继承(也因此有人认为 Go 不是面向对象编程语言),并且 Go 语言创始人 Rob Pike 本人也强调[用更小的接口实现更强的抽象](https://www.youtube.com/watch?v=PAAkCSZUG1c&t=5m17s)。
@@ -323,11 +321,11 @@ CLOS 中方法与类分离、不强制封装的设计,使得模块之间能保
使用继承时若是稍不注意,就容易违背里氏替换原则(LSP),比如经典的圆形/椭圆形问题(也叫正方形/矩形问题),指的是圆形和正方形作为椭圆形和矩形的子类,会违背里氏替换原则。具体的解释可以阅读[这篇文章](http://localhost:1313/library/2026/clean-architecture/#:~:text=%E6%89%80%E8%B0%93%E7%9A%84%E9%87%8C%E6%B0%8F,assert%20%E4%BC%9A%E5%A4%B1%E8%B4%A5%E3%80%82)。
-继承的目的其实是行为复用(behavior reuse),子类可以复用(或重写,或拓展)父类的行为。在经典的四人帮设计模式中,继承被基于接口的组合复用取代了,他们更倾向于把行为从类中分离出来。在一些动态语言中,类的概念被取消了,取而代之的是原型(prototype),可以现有的对象作为原型,复用其属性和行为,不必先有类才能实例化,这叫做[基于原型的编程](https://zh.wikipedia.org/wiki/%E5%9F%BA%E4%BA%8E%E5%8E%9F%E5%9E%8B%E7%BC%96%E7%A8%8B),可以理解为没有类的面向对象。实际上 JavaScript 就支持原型编程。
+继承的目的是**行为复用**(behavior reuse),子类可以复用(或重写,或拓展)父类的行为。在经典的四人帮设计模式中,继承被基于接口的组合复用取代了,他们更倾向于把行为从类中分离出来。在一些动态语言中,类的概念被取消了,取而代之的是原型(prototype),可以现有的对象作为原型,复用其属性和行为,不必先有类才能实例化,这叫做[基于原型的编程](https://zh.wikipedia.org/wiki/%E5%9F%BA%E4%BA%8E%E5%8E%9F%E5%9E%8B%E7%BC%96%E7%A8%8B),可以理解为没有类的面向对象。实际上 JavaScript 就支持原型编程。
-看起来,继承难用到不少人都在找新的解决方案以避免使用继承。
+看起来,继承难用到不少人都在找新的解决方案以避免和它打交道。
-话说回 Common Lisp,这门语言的对象系统实际上支持继承,而且是多继承,子类可以组合复用多个父类的行为,这实际上也和四人帮的「组合优先于继承」不谋而合。多继承的语法也简单得要命。
+话说回 Common Lisp,这门语言的对象系统实际上支持继承,而且是多继承,子类可以组合复用多个父类的行为,这实际上也和四人帮的「组合优先于(单)继承」不谋而合,**只要类足够小且内聚**。多继承的语法也简单得要命。
```lisp
(defclass baby (child person)
@@ -343,14 +341,26 @@ CLOS 中方法与类分离、不强制封装的设计,使得模块之间能保
(set-window-title))
```
-这段方法定义实际上覆盖了 `(setf url)` 函数,当 `url` 这个槽被更新(`setf`),就调用更新 UI 的函数。这有点像面向切面编程,在 Java 等 OOP 语言里,子类也经常对父类做这种操作。行为复用的目的经常是拓展原有的行为。
+这段方法定义实际上覆盖了 `(setf url)` (但只会分发给以这个类的 `url` 槽为参数的方法),当 `url` 这个槽被更新(`setf`),就调用更新 UI 的函数。这有点像面向切面编程,在 Java 等 OOP 语言里,子类也经常对父类做这种操作。行为复用的目的经常是拓展原有的行为。
-简单来说,行为复用不要求继承一定存在,更不是只有 Java 的单继承模式才能做到。甚者,经典的面向对象设计模式常常绕过继承,通过接口实现组合复用,而在 CLOS 里,多继承和 `:around` 等语言特性,使得行为复用变得更简单灵活。
+如果要在 Java 里实现「更新 URL 后也同时刷新 UI」,由于要避免继承,就只能求助于[装饰器模式](https://en.wikipedia.org/wiki/Decorator_pattern)和[观察者模式](https://en.wikipedia.org/wiki/Observer_pattern)了。
+
+简单来说,行为复用不要求继承,更不是只有 Java 的单继承模式才能做到。甚者,经典的面向对象设计模式常常绕过继承,通过接口实现组合复用,而在 CLOS 里,多继承和 `:around` 等语言特性,使得行为复用变得更简单灵活。
## 设计模式,还是规避方案?
+这个观点可能不新了,在 Peter Norvig 的 [Design Patterns in Dynamic Languages](https://norvig.com/design-patterns/)(动态语言中的设计模式)演讲中,他提到在包括 Common Lisp 在内的多种动态语言里,23 中设计模式中的 16 种要么消失了,要么被简化——我得提醒读者,这些设计模式里还有[解释器模式](https://en.wikipedia.org/wiki/Interpreter_pattern),用于嵌入脚本语言,属于和架构关联较弱的模式,这是剩下的 7 种之一。
+
+这个演讲第一次呈现是 1996 年五月,而刚好三十年过去了,这二十多种面向对象设计模式仍然被无数的 Java(以及其他传统 OOP 语言)开发者奉为圭臬。我想这并不是他们想不到这个观点,而是因为他们没有或者很少接触其他的语言。我也在和人打交道的过程中逐渐意识到,大部分求职者和从业者对编程并无热情,Java 也好,C++ 也好,都是他们求职的工具而已,编程语言对本身而言并不有趣,所以不会去探索更多的语言;或者,另一部分人更关注业务,成为了管理者,与技术保持着微妙的距离。
+
+而在他们眼里,设计模式的确有帮助,甚至是「优雅」的(不得不说,「优雅」是非常主观的评价),因为这的确让编写软件变得方便了。只不过方便的源头,是本身就存在的语言限制,而设计模式仅仅是提供了这些限制的规避方案而已——这些限制在一开始可以不存在。
+
+不过论题可能会被转移到语言的动态与静态上,动态语言的名声在我看来是两极分化的。吹捧者(包括我)认为动态语言限制很少,写起来令人舒适,语法也往往更简洁,需要维护的代码量更少;反对者可能认为动态语言容易出错,不少类型检查都发生在运行时,缺少编译时保护——我对此的反驳是,要保证软件质量和安全性,[写测试就好了](/library/2026/functional-design/#%E8%AE%BA%E7%B1%BB%E5%9E%8B%E5%AE%89%E5%85%A8);况且静态类型不代表类型安全,这是两个概念,同理,动态语言也可以做到类型安全。
+
+话说到最后,究竟是选用灵活但效率和安全性可能更低的动态语言,还是在静态语言系统里用复杂的设计模式规避限制,都是开发者自己的权衡取舍。
+---
-## Footnotes
+Happy Lisping!
[^1]: Common Lisp Object System(Common Lisp 对象系统)