Clojure中的面向方面编程

18

如何在Clojure中实现面向方面编程?我们需要在Clojure中使用AOP吗?
假设我们想要使用纯粹的Clojure解决方案(不使用AspectJ)。

4个回答

26

面向方面编程通常用于为代码添加横切功能,否则业务逻辑将变得混乱。一个很好的例子是日志记录-您真的不想在整个代码库中到处散布日志记录代码。

在Clojure中,您实际上不需要AOP,因为可以使用其他技术轻松实现这一点。

例如,您可以使用高阶函数来“包装”其他函数以实现横切功能:

; a simple function - the "business logic"
(defn my-calculation [a b]
  (+ a b))

; higher order function that adds logging to any other function
(defn wrap-with-logging [func]
  (fn [& args]
    (let [result (apply func args)]
      (println "Log result: " result)
      result)))

; create a wrapped version of the original function with logging added
(def my-logged-calculation (wrap-with-logging my-calculation))

(my-logged-calculation 7 9)
=> Log result:  16
=> 16

7
与AOP进行比较的这个例子的问题在于,现在开发人员必须调用新的包装方法而不是原始方法。理想情况下,通过调用原始方法就能实现记录日志行为。那样更接近AOP提供的功能,对吧? - jcrossley3
9
如果你想的话,你可以通过使用“(def my-calculation (wrap-with-logging my-calculation))”重新定义原始函数。请注意,这句话已经被翻译成中文。 - mikera
13
不过这样重新评估是不安全的。如果需要更灵活的形式化,可以参考 Robert Hooke 的 GitHub 链接:https://github.com/technomancy/robert-hooke - user61051
@jcrossley3 这取决于你如何使用AOP。例如,你可以使用around操作符来包围函数调用。这正是这里所展示的内容。 - denis631

15
AOP在我看来只是某些静态编程语言的产物。据我所知,它通常只是一堆非标准编译器扩展。我还没有看到任何AOP应用程序,这些应用程序不能在更动态的语言中更好地本地解决。Clojure足够动态,而且这还没有考虑宏。
我可能是错误的,但如果是这样,我需要看到一个实际的AOP用例,它不能像纯clojure一样很好地实现。
编辑:只是为了明确:我拒绝将elisp的advice之类的东西视为面向方面的。在动态语言中,这些只是需要时使用的技术,除了重新绑定函数定义之外,不需要语言支持-所有lisps都支持此功能。
没有必要将它们视为特殊-您可以轻松地在clojure中定义自己的defadvice类似函数。例如,请参见compojure的wrap!宏,实际上已经弃用,因为通常您甚至不需要它。

3
宏被称为动态有些奇怪,因为它们在编译时运行(它们是钩子,可以进入编译器)。当您更改宏时,需要重新编译调用它的所有代码。按我的理解,这并不是很动态... - Marek
3
此外,AOP与Common Lisp的元对象协议(MOP)密切相关(同时Pascal Constanza因倡导CL中的AOP而出名)。您认为Common Lisp也是静态的吗?这个答案似乎是一堆(不)有根据的猜测的随机集合… - Marek
2
我认为这个答案似乎暗示(对于那些不熟悉Clojure的人来说)Clojure在没有任何额外努力的情况下就能消除横切关注点。mikera的回答在这方面更清晰,并且包括了例子,我认为这应该是被接受的答案。事实上,该答案及其示例还突显了一些困难,进一步得出结论:AOP/横切关注点(无论如何命名)在Clojure中并不会自动解决。 - Sprague

10

面向切面编程是在Java中实现关注点分离的好方法。Clojure的可组合抽象非常擅长实现此目标。也可以参考这个问题。有关该主题的详细内容可以在《Clojure之乐》中找到。

至于另一种称之为Aspect Oriented Clojure的示例,请查看Ring Web框架。


1

您可以更轻松地使用Clojure进行AOP。只需在函数中使用元数据来通知您何时需要日志记录:

(defn ^:log my-calculation 
  [a b]
  (+ a b))

然后,您可以重新定义所有函数,自动用日志包装它们。以下是部分代码(以及下面的取消包装函数):
(defn logfn
  [f topic severity error-severity]
  (fn [& args]
    (try
      (if severity
        (let [r (apply f args)]
          (log* topic {:args args, :ret r} severity)
          r)
        (apply f args))
      (catch Exception e
        (if error-severity
          (let [data {:args args, :error (treat-error e), :severity error-severity}]
            (log* topic data error-severity)
            (throw e))
          (throw e))))))

(defn logfn-ns
  "Wrap function calls for logging on call or on error.

  By default, do nothing. When any :log or :log-error, enables logging. If ^:log,
  only log on error (default severity error).

  Can customize log severity w/ e.g. ^{:log info} or on error log severity likewise."
  [ns alias]
  (doseq [s (keys (ns-interns ns))
          :let [v (ns-resolve ns s)
                f @v
                log (-> v meta :log)
                log-error (-> v meta :log-error)]
          :when (and (ifn? f)
                     (-> v meta :macro not)
                     (-> v meta :logged not)  ;; make it idempotent
                     (or log log-error))]

    (let [log (if (= log true) nil log)
          log-error (or log-error "error")
          f-with-log (logfn f
                            (str alias "/" s)
                            log
                            log-error)]
      (alter-meta! (intern ns s f-with-log)
                   (fn [x]
                     (-> x
                         (assoc :logged true)
                         (assoc :unlogged @v)))))))

(defn unlogfn-ns
  "Reverts logfn-ns."
  [ns]
  (doseq [s (keys (ns-interns ns))
          :let [v (ns-resolve ns s)]
          :when (-> v meta :logged)]
    (let [f-without-log (-> v meta :unlogged)]
      (alter-meta! (intern ns s f-without-log)
                   (fn [x]
                     (-> x
                         (dissoc :logged)
                         (dissoc :unlogged)))))))

你只需要调用(log/logfn-ns 'my.namespace "some alias"),所有内容都会被包装在日志记录中(和其他一些内容)。
PS:我上面的自定义记录器有一个topic,即“一些别名/函数名称” PS2:也包装了try/catch。 PS3:不太喜欢这样做。恢复为显式记录日志。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接