开发者

Aspect-Oriented Programming in Clojure

开发者 https://www.devze.com 2023-02-23 03:23 出处:网络
How开发者_StackOverflow中文版 to achieve Aspect-Oriented Programming in Clojure? Do we need AOP in Clojure?

How开发者_StackOverflow中文版 to achieve Aspect-Oriented Programming in Clojure? Do we need AOP in Clojure?

Let's say we want plain vanilla Clojure solution (no AspectJ).


Aspect-Oriented Programming is typically used to add cross-cutting functionality to code that would otherwise get hopelessly intertwined with business logic. A great example is logging - you don't really want logging code scattered everywhere in your code base.

You don't really need AOP in Clojure because it's easy to achieve this with other techniques in Clojure.

For example, you can use higher-order functions to "wrap" other functions with cross cutting functionality:

; 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


AOP IMHO is just an artifact of certain kinds of static programming languages. AFAIKS it's usually just a bunch of non-standard compiler extensions. I've not yet seen any application of AOP that can't be solved better & natively in more dynamic languages. Clojure is certainly dynamic enough, and that's without even considering macros.

I may be wrong, but if so, I'd need to see an actual AOP use case that can't be implemented just as well in pure clojure.

Edit: just to be clear: I refuse to see things like elisp's advice as aspect oriented. In dynamic languages, those are just techniques to be used whenever you need them, with no need for language support other than rebinding of function definitions - which all lisps support anyway.

There's no need to treat them as special - you can easily define your own defadvice-like function in clojure. See for example, compojure's wrap! macro, which is actually deprecated since you generally don't even need it.


Aspect oriented programming is a great way to achieve seperation of concernes in Java. Clojure's composable abstractions achieve this very well. See this question also. The topic is covered really well in The Joy Of Clojure.

as for an example of Aspect Oriented Clojure by another name check out the Ring web framework


Well you could be more AOP w/ Clojure easily. Just use metadata in functions to informe when you want logs:

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

Then you can redefine all functions, wrapping them w/ logging automatically. Part of this code (together w/ unwrap functions bellow):

(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)))))))

You just call (log/logfn-ns 'my.namespace "some alias") and all is wrapped w/ logging (and some).

PS: My custom logger above have a topic which is "some alias/function name" PS2: Also wrapped w/ try/catch. PS3: Didn't like this so much. Reverted to have explicit logging.

0

精彩评论

暂无评论...
验证码 换一张
取 消