Clojure 摒弃了传统面向对象语言(如 Java、C++)中基于类继承的静态多态模型,转而采用更加灵活的 ad-hoc 多态机制。通过**多方法(Multimethod)和协议(Protocol)**两大支柱,开发者可以在不修改原有代码的情况下为任意类型添加新行为,实现真正的「开闭原则」。
1. 为什么 Clojure 选择了 Ad-hoc 多态
1.1 面向对象继承的问题
传统继承模型存在「菱形继承」、「脆弱基类」等固有问题。一旦类层次结构固定,新增行为只能通过修改现有类(违反开闭原则)或创建大量包装类来实现。
Clojure 的设计理念强调:
- 数据与代码分离:数据就是纯数据(map、vector、record),行为由独立函数定义
- 行为可扩展:为已有类型添加新行为无需修改类型定义
- 分派灵活:可以基于任意函数(不仅是类型)进行方法分派
1.2 Ad-hoc 多态 vs 面向对象多态
| 特性 | Java 面向对象 | Clojure Ad-hoc |
|---|---|---|
| 绑定方式 | 编译期静态绑定到类 | 运行时基于 dispatch 函数 |
| 扩展方式 | 继承类/实现接口 | 独立声明 defmethod/extend |
| 分派依据 | 对象类型(单分派) | 任意函数返回值(任意分派) |
| 类型修改 | 需要修改原文件 | 零侵入式扩展 |
| 组合方式 | 单继承多重接口 | 多重 mixin 自由组合 |
2. 多方法(Multimethod):任意函数动态分派
2.1 defmulti:定义分派函数
;; 定义一个基于 :shape 字段动态分派的多方法
(defmulti area :shape)
;; 为 :circle 类型定义实现
(defmethod area :circle [shape]
(* Math/PI (:radius shape) (:radius shape)))
;; 为 :rectangle 类型定义实现
(defmethod area :rectangle [shape]
(* (:width shape) (:height shape)))
;; 默认实现
(defmethod area :default [shape]
(throw (ex-info "Unknown shape" {:shape shape})))
;; 使用
(area {:shape :circle :radius 5}) ; => 78.54...
(area {:shape :rectangle :width 4 :height 5}) ; => 20
defmulti 的第二个参数是 dispatch 函数,它接收多方法的所有参数并返回一个"dispatch value"。defmethod 根据这个值进行匹配。
2.2 多参数与多重分派
;; 基于两个参数类型进行分派(类多重分派)
(defmulti encounter (fn [x y] [(:species x) (:species y)]))
(defmethod encounter [:bunny :lion] [b l]
:run-away)
(defmethod encounter [:lion :bunny] [l b]
:eat)
(defmethod encounter [:lion :lion] [l1 l2]
:fight)
(defmethod encounter :default [x y]
:ignore)
(encounter {:species :bunny} {:species :lion}) ; => :run-away
(encounter {:species :lion} {:species :lion}) ; => :fight
这比 Java 的单分派(只能基于接收者类型分派)强大得多。Clojure 的多方法相当于 Common Lisp 的 CLOS 多重分派。
2.3 分派函数可以是任意逻辑
;; 基于数据库类型的动态 SQL 生成
(defmulti generate-sql (fn [query db-config] (:dialect db-config)))
(defmethod generate-sql :mysql [query db-config]
(str "SELECT * FROM " (:table query) " LIMIT " (:limit query)))
(defmethod generate-sql :postgresql [query db-config]
(str "SELECT * FROM " (:table query) " FETCH FIRST " (:limit query) " ROWS ONLY"))
(defmethod generate-sql :oracle [query db-config]
(str "SELECT * FROM (SELECT * FROM " (:table query) ") WHERE ROWNUM <= " (:limit query)))
;; 使用
(generate-sql {:table "users" :limit 10} {:dialect :mysql})
;; => "SELECT * FROM users LIMIT 10"
2.4 分派层级与继承
;; 定义层级关系
(def shape-hierarchy
(make-hierarchy))
(def shape-hierarchy
(-> shape-hierarchy
(derive :square :rectangle)
(derive :rectangle :polygon)
(derive :circle :ellipse)))
;; 定义基于自定义层级的多方法
(defmulti compute-area
:shape
:hierarchy #'shape-hierarchy)
(defmethod compute-area :polygon [shape]
(if (= :square (:type shape))
(* (:side shape) (:side shape))
(* (:width shape) (:height shape))))
;; :square 会匹配 :polygon 方法
(compute-area {:shape :square :side 5})
; => 25
3. 协议(Protocol):结构化接口定义
3.1 defprotocol:声明接口
协议类似于 Java 的 Interface,但更轻量、更灵活:
(defprotocol Printable
"可被打印为字符串的协议"
(to-string [this] "返回对象的字符串表示"))
(defprotocol Serializable
"可序列化接口"
(to-json [this] "返回 JSON 字符串"))
3.2 extend-type:为已有类型实现协议
;; 为 Clojure 内置类型实现 Printable
(extend-type clojure.lang.PersistentVector
Printable
(to-string [this]
(str "[" (clojure.string/join ", " (map to-string this)) "]")))
;; 为 Java String 类实现(Clojure 天然支持)
(extend-type java.lang.String
Printable
(to-string [this] this))
(to-string [1 2 3]) ; => "[1, 2, 3]"
(to-string "hello") ; => "hello"
3.3 extend-protocol:批量扩展
;; 同时为多类型实现多个方法
(extend-protocol Printable
java.lang.Number
(to-string [this] (str this))
java.lang.Boolean
(to-string [this] (if this "true" "false"))
nil
(to-string [_] "nil"))
3.4 Record + Protocol:面向数据的类型
;; 定义记录(值语义的数据结构)
(defrecord Person [name age])
;; Record 自动实现了关联函数:
(:name (->Person "Alice" 30)) ; => "Alice"
(assoc (->Person "Alice" 30) :city "NYC") ; => Person{:name "Alice", :age 30, :city "NYC"}
;; 为 Record 实现协议
(defprotocol Greetable
(greet [this] "打招呼"))
(defrecord Employee [name age department]
Greetable
(greet [this]
(str "Hello, I'm " name " from " department)))
(greet (->Employee "Bob" 25 "Engineering"))
;; => "Hello, I'm Bob from Engineering"
3.5 与 Java 接口的互操作
Clojure 协议可无缝映射到 Java 接口:
(defprotocol IProcess
(start [this])
(stop [this]))
;; 实现 Java 的 Comparable 接口
(defrecord PriorityTask [priority description]
java.lang.Comparable
(compareTo [this other]
(compare (:priority other) (:priority this)))) ; 优先级高的排前面
4. 实战案例:多租户权限系统
;; 定义权限协议
(defprotocol IPermission
(can-access? [this resource action] "检查是否有权限"))
;; Record 类型定义
(defrecord User [id role tenant-id])
(defrecord Admin [id super?])
;; 普通用户实现
(defmethod can-access? User [user resource action]
(and (= (:tenant-id user) (:tenant-id resource))
(contains? (get-role-permissions (:role user)) action)))
;; 超级管理员实现
(defmethod can-access? Admin [admin resource action]
(or (:super? admin)
(= (:tenant-id admin) (:tenant-id resource))))
;; 使用多方法优化:基于资源类型+角色的多层分派
(defmulti check-permission
(fn [user resource action]
[(:role user) (:type resource)]))
(defmethod check-permission [:user :document] [user resource action]
(and (= (:owner-id resource) (:id user))
(contains? #{:read :write} action)))
(defmethod check-permission [:manager :document] [user resource action]
(contains? #{:read :write :delete} action))
;; 结合多方法与协议的完整系统
(defn authorize [user resource action]
(when-not (can-access? user resource action)
(throw (ex-info "Forbidden"
{:user user :resource resource :action action}))))
5. 协议 vs 多方法选型指南
| 场景 | 推荐机制 | 理由 |
|---|---|---|
| 基于数据类型的标准接口 | Protocol | 与 Java 互操作友好,编译期可检查 |
| 基于任意逻辑动态分派 | Multimethod | dispatch 函数自由定义 |
| 需要高性能调用 | Protocol | 通过 vtable 实现,接近原生方法调用速度 |
| 需要多重分派 | Multimethod | 协议只支持基于第一个参数的分派 |
| 扩展现有类型(String、Date等) | Protocol | 可为任意 Java/Clojure 类型扩展 |
| 构建层级分类系统 | Multimethod | derive 层级匹配 |
6. 常见模式与最佳实践
6.1 默认实现与兜底
;; 多方法的兜底
(defmethod my-method :default [x]
(println "未识别的类型:" (type x)))
;; 协议的局部实现
(extend-protocol MyProtocol
Object
(my-method [this] (default-behavior this)))
6.2 nil 处理
(extend-protocol Printable
nil
(to-string [_] ""))
Clojure 的 nil 安全设计让协议对 nil 值也能优雅处理,避免 NullPointerException。
6.3 性能优化
- 多方法在分派时会进行层级查找,高频场景可用协议替代
- 协议的实现内部使用缓存 vtable,性能接近直接方法调用
- 对确实需要多方法性能的场景,可使用
prefer-method优化分派优先级
7. 总结
Clojure 的多方法与协议共同构成了一个强大的多态系统,既保留了面向对象接口的结构性,又突破了单继承的限制。协议负责「类型的结构化接口」,多方法负责「任意逻辑的灵活分派」,二者相辅相成而非互斥。
要深入理解 Clojure 的类型系统与设计哲学,建议继续阅读 Clojure 调用 Java 深度实践 了解如何与 Java 的类型系统互操作,以及 Clojure 现代 Web 全栈开发 中 Record + Protocol 在 Web API 设计中的实践。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。