summaryrefslogtreecommitdiff
path: root/content/article
diff options
context:
space:
mode:
Diffstat (limited to 'content/article')
-rw-r--r--content/article/common-lisp-oop.md205
1 files changed, 205 insertions, 0 deletions
diff --git a/content/article/common-lisp-oop.md b/content/article/common-lisp-oop.md
new file mode 100644
index 00000000..4b4bf0f0
--- /dev/null
+++ b/content/article/common-lisp-oop.md
@@ -0,0 +1,205 @@
++++
+title = 'Just A Common Lisper II'
+subtitle = ''
+slug = ''
+tags = []
+categories = ''
+date = 2026-06-03T10:19:22+08:00
+draft = true
+toc = true
++++
+
+书接[上回](http://localhost:1313/article/common-lisp-start/),前一篇文章讲了 Lisp 中的基本数据结构列表的构造方式(*cons pair*),以及
+由此衍生出的关联列表(`alist`)和属性列表(`plist`),非常优雅。这之后我就跑偏了,谈起了编译器和解释器中的「特例」以及宏,为什么看起来像是函数的形式实际上不是也不能是函数,最后浅谈了函数式编程和 `let` 的实现方式。
+
+今天的文章就来关注 Common Lisp 的另一部分语言特性,即面向对象编程,也借此来谈谈我对 OOP *多少有些复杂*的看法。<!--more-->
+
+## 不属于类的多态
+
+Common Lisp 在这点上与 Clojure 类似,提供了非常相似的 `defgeneric`(CL)和 `defmulti`(Clojure),它们都能定义「抽象函数」,也就是不包含具体实现的函数。之后可以使用 `defmethod`(两门语言相同)定义方法,也就是抽象函数的具体实现。
+
+以下是来自 Common Lisp [文档](https://lisp-lang.org/learn/clos)的例子:
+
+```lisp
+(defgeneric description (object)
+ (:documentation "Return a description of an object."))
+
+(defmethod description ((object integer))
+ (format nil "The integer ~D" object))
+
+(defmethod description ((object float))
+ (format nil "The float ~3,3f" object))
+```
+
+`defgeneric` 定义了一个名为 `description`(描述)的抽象函数,或者用 CL 的术语来说,应该叫作泛函数(generic function)。后面的代码用 `defmethod` 定义了两个 `description` 的具体实现(或者说方法),分别对应整数型(`integer`)和浮点型(`float`)。这两个 `description` 方法共享一个符号名,调用时没有任何区别,但根据传入的数据类型不同,被实际调用的方法也不同。
+
+```lisp
+(description 10)
+;; => "The integer 10"
+
+(description 3.14)
+;; => "The float 3.140"
+```
+
+此时还没有类参与,如你所见,CLOS[^1] 中的方法不属于类。这只是多态(polymorphism)而已,其实 Java 等面向对象编程语言的多态也不需要类参与,只不过 Java 里的方法必须放在类里。
+
+```java
+public class Descriptor {
+ public void description(int number) {
+ System.out.format("The integer %d", number);
+ }
+
+ public void description(float number) {
+ System.out.format("The float %f", number);
+ }
+}
+```
+
+```java
+descriptor.description(10)
+// => "The integer 10"
+
+descriptor.description(3.14)
+// => "The float 3.14"
+```
+
+当然,上面这段代码是违反开闭原则的,如果要添加新的类型,就需要修改 `Descriptor` 类。大部分情况下,Java 程序其实长这样:
+
+```java
+public interface Descriptor {
+ void description();
+}
+```
+
+```java
+public class IntegerDescriptor implements Descriptor {
+ private int number;
+ public void description() {
+ System.out.format("The integer %d", number);
+ }
+}
+```
+
+```java
+public class FloatDescriptor implements Descriptor {
+ private float number;
+ public void description() {
+ System.out.format("The float %f", number);
+ }
+}
+```
+
+如此一来,添加新的类型只需要添加新的 `Descriptor` 实现类,程序对修改关闭,对拓展开放。看起来很美好,不过后果是我们写了三个文件,因为 Java 限制一个公开类只能放在一个文件里。并且,调用 `description` 方法时还需要先创建对应的 `Descriptor` 对象,把要描述的数据写入这个对象的成员变量中,然后再调用 `description` 方法。
+
+而 Common Lisp 这边只需要写 `(description data)` 就好了,方法定义可以放在一起,也可以不放在一起,还可以在运行时动态地添加新的方法定义。
+
+```lisp
+(defmethod description ((x integer) y)
+ 1)
+
+(description 1 2.0)
+;; => 1
+
+(defmethod description ((x integer) (y float))
+ 2)
+
+(description 1 2.0)
+;; => 2
+```
+
+上述代码并没有覆盖原先的方法,而是在名为 `description` 的方法集中添加了新的方法。尽管方法调用的参数完全一致,但在运行时添加了新的方法定义之后,这个方法调用被分发到了新的定义上。听起来很酷,不过看起来也容易出现时序耦合之类的问题,而且也失去了编译时的保护,需要谨慎使用,要保证系统的行为是可预测的。
+
+这是把类和方法分开的好处之一,由于方法不属于类,程序员不总是需要先实例化类再调用方法(除非是静态方法,但很少用),也不需要把方法都放在同一个地方,在编译之后还能动态地添加新的方法。
+
+再者,CLOS 是**多分发**(multiple dispatch)的系统,一般的 OOP 语言是单分发的。例如在 Java 中,我们只能根据对象的类型这单个“参数”分发方法,`floatDescriptor.description()` 对应一个方法,`integerDescriptor.description()` 对应另一个方法;在 CLOS 中,方法不绑定在对象上,只要多个参数中有一个参数的类型不同,都被视作不同的态,比如 `(description 1 1)` 对应一个方法,`(description 1 1.0)` 对应另一个方法。
+
+以上大体是 [Wikipedia](https://en.wikipedia.org/wiki/Common_Lisp_Object_System) 和一些 Reddit 用户的观点,他们貌似忽略了 Java 也可以在方法的参数层面做到多分发,在一个类中可以定义多个名字一样但形参不同的方法。调用类的方法时,先是通过对象的类型进行单分发,然后再根据形参进行多分发。不过我也在前面提到了,由于 Java 的**语言限制**,方法必须和类绑定且必须定义在类的内部,依赖多分发,容易违背开闭原则,所以 Java 编程一般使用基于类的分发,忽略了基于函数签名的多分发,*况且在 OOP 语言里通过在运行时传入类型不同的参数来分发方法,听起来有些大逆不道*。尽管最后得出的结论类似,但我想有必要澄清一下:Java 的确支持多分发。
+
+一边在学校*被迫*学习 Java 设计模式,一边自己摸索 Lisp 编程,我愈发觉得各种面向接口编程和花里胡哨的设计模式,其实都是对 Java 语言限制的妥协。这是一门很经典的 OOP 语言,但它不够灵活,而且啰唆,往往要创建很多类,以提升系统复杂度为代价去满足设计原则,但实际上这些原则可以在其他语言里用更简单的方式被满足。
+
+## To close, or to CLOS?
+
+讲完多态,接下来是 OOP 的第二个要点:封装。
+
+前文提到 Java 的啰唆、复杂是其语言限制导致的,具体来说,这个限制就是严格的封装(encapsulation)以及封闭类(closed class)。
+
+所有 Java 程序员应该都写过*在我如今看来依旧非常神经的* getter 和 setter 方法,也就是定义一个类和它的私有成员变量,保证外部类无法直接访问其变量,只能调用它的公开方法设置和获取变量,以此避免破坏类的封装性。所谓的不能破坏封装性,就是防止外部类用意想不到的方式访问其数据,比方说,商品类中包含价格这一变量,价格不能为负数,如果价格变量是公开的,那么外部类有可能把价格修改为负数,但如果不公开这个变量,而是公开 `setPrice()` 方法,那么就可以在这个方法里加上判断,如果传入的参数小于 0,就报错。
+
+听起来好像很美好,但 Java 里的实体类基本上都长这样:
+
+```java
+public class Employee {
+ private Department department;
+ private String name;
+ private String ID;
+ private Gender gender;
+ private double salary;
+
+ // --- Constructors ---
+
+ public Employee(Department department, String name, String ID, Gender gender, double salary) {
+ this.department = department;
+ this.name = name;
+ this.ID = ID;
+ this.gender = gender;
+ this.salary = salary;
+ }
+
+ public Employee() {
+ }
+
+ // --- Getters ---
+
+ public Department getDepartment() {
+ return department;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getID() {
+ return ID;
+ }
+
+ public Gender getGender() {
+ return gender;
+ }
+
+ public double getSalary() {
+ return salary;
+ }
+
+ // --- Setters ---
+
+ public void setDepartment(Department department) {
+ this.department = department;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public void setID(String ID) {
+ this.ID = ID;
+ }
+
+ public void setGender(Gender gender) {
+ this.gender = gender;
+ }
+
+ public void setSalary(double salary) {
+ this.salary = salary;
+ }
+}
+```
+
+天哪!难道 Java 程序员写这么多重复的方法不觉得烦人吗?——当然!所以他们爱用的 IDEA 里面有代码生成器,用来自动补全 getter 和 setter 方法,还可以生成包含指定参数的构造方法。我想他们使用 `Alt + Insert + Enter` 已经熟练到不觉得这有什么问题了。另外还有名为 Lombok 的库,提供了 [@Data](https://projectlombok.org/features/Data) 注解自动生成这些东西,*你知道这个用来生成模板代码的库还有付费的企业版吗?*
+
+
+
+## 继承是必要的吗?
+
+## Footnotes
+
+[^1]: Common Lisp Object System(Common Lisp 对象系统)