diff options
| -rw-r--r-- | content/article/common-lisp-oop.md | 205 |
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 对象系统) |
