Макрос Clojure для вызова Java-сеттеров на основе карты?
Я пишу оболочку Clojure для библиотеки Java Braintree, чтобы обеспечить более сжатый и идиоматический интерфейс. Я хотел бы предоставить функции для создания экземпляров объектов Java быстро и кратко, например:
(transaction-request :amount 10.00 :order-id "user42")
Я знаю, что могу сделать это явно, как показано в этот вопрос:
(defn transaction-request [& {:keys [amount order-id]}]
(doto (TransactionRequest.)
(.amount amount)
(.orderId order-id)))
но это повторяется для многих классов и становится более сложным, когда параметры являются необязательными. Используя отражение, можно определить эти функции гораздо более лаконично:
(defn set-obj-from-map [obj m]
(doseq [[k v] m]
(clojure.lang.Reflector/invokeInstanceMethod
obj (name k) (into-array Object [v])))
obj)
(defn transaction-request [& {:as m}]
(set-obj-from-map (TransactionRequest.) m))
(defn transaction-options-request [tr & {:as m}]
(set-obj-from-map (TransactionOptionsRequest. tr) m))
очевидно, я хотел бы избежать отражения, если это вообще возможно. Я попытался определить версию макроса set-obj-from-map
но мой макро-фу недостаточно силен. Это, вероятно, требует eval
как пояснил здесь.
есть ли способ вызвать метод Java, указанный во время выполнения, без использования отражения?
спасибо заранее!
обновленное решение:
следуя совету от Joost я смог решить проблему, используя аналогичную технику. Макрос использует отражение во время компиляции, чтобы определить, какие методы сеттера класс имеет, а затем выплевывает формы, чтобы проверить param в карте и вызвать метод со значением.
вот макрос и пример использования:
; Find only setter methods that we care about
(defn find-methods [class-sym]
(let [cls (eval class-sym)
methods (.getMethods cls)
to-sym #(symbol (.getName %))
setter? #(and (= cls (.getReturnType %))
(= 1 (count (.getParameterTypes %))))]
(map to-sym (filter setter? methods))))
; Convert a Java camelCase method name into a Clojure :key-word
(defn meth-to-kw [method-sym]
(-> (str method-sym)
(str/replace #"([A-Z])"
#(str "-" (.toLowerCase (second %))))
(keyword)))
; Returns a function taking an instance of klass and a map of params
(defmacro builder [klass]
(let [obj (gensym "obj-")
m (gensym "map-")
methods (find-methods klass)]
`(fn [~obj ~m]
~@(map (fn [meth]
`(if-let [v# (get ~m ~(meth-to-kw meth))] (. ~obj ~meth v#)))
methods)
~obj)))
; Example usage
(defn transaction-request [& {:as params}]
(-> (TransactionRequest.)
((builder TransactionRequest) params)
; some further use of the object
))
2 ответов
вы можете использовать отражение во время компиляции ~ до тех пор, пока вы знаете класс, с которым вы имеете дело к тому времени~, чтобы выяснить имена полей и генерировать "статические" сеттеры из этого. Я написал код, который делает это в значительной степени для геттеров некоторое время назад, что вы можете найти интересным. См.https://github.com/joodie/clj-java-fields (особенно, макрос def-полей в https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).
макрос может быть таким же простым, как:
(defmacro set-obj-map [a & r] `(doto (~a) ~@(partition 2 r)))
но это сделает ваш код похожим на:
(set-obj-map TransactionRequest. .amount 10.00 .orderId "user42")
думаю, это не то, что вы предпочли бы :)