2012-02-28 9 views
10

Estoy escribiendo un contenedor Clojure para la biblioteca Braintree Java para proporcionar una interfaz más concisa e idiomática. Me gustaría ofrecer funciones para crear instancias de los objetos Java de forma rápida y concisa, como:¿Macro de Clojure para llamar a los instaladores de Java basados ​​en un mapa?

(transaction-request :amount 10.00 :order-id "user42") 

Sé que puedo hacer esto de manera explícita, como se muestra en this question:

(defn transaction-request [& {:keys [amount order-id]}] 
    (doto (TransactionRequest.) 
    (.amount amount) 
    (.orderId order-id))) 

Pero esto es repetitiva para muchas clases y se vuelve más complejo cuando los parámetros son opcionales. El uso de la reflexión, es posible definir estas funciones mucho más concisa:

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

Obviamente, me gustaría evitar la reflexión, si es posible. Traté de definir una versión de macro de set-obj-from-map pero mi macro-fu no es lo suficientemente fuerte. Probablemente requiera eval como se explica en here.

¿Hay alguna manera de llamar a un método Java especificado en tiempo de ejecución, sin usar el reflejo?

¡Gracias de antemano!

solución actualizada:

Siguiendo el consejo de Joost, yo era capaz de resolver el problema utilizando una técnica similar. Una macro utiliza la reflexión en tiempo de compilación para identificar qué métodos de establecimiento tiene la clase y luego escupe formularios para buscar el parámetro en un mapa y llamar el método con su valor.

Aquí está la macro y un ejemplo el uso:

; 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] 
     [email protected](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 
)) 
+0

Sin ¿reflexión? Casi seguro que no. –

+1

Bueno, es posible traducir un mapa en llamadas a métodos _sin reflejo_ usando una macro. Solo utilicé la reflexión al darme cuenta de que la macro no podía tomar un símbolo que sostenía el mapa, sino solo el mapa en bruto. Probablemente debería haber sido más claro al decir que me gustaría evitar la reflexión en _runtime_, como @ joost-diepenmaat se describe a continuación. – bkirkbri

Respuesta

8

Usted puede utilizar la reflexión en tiempo de compilación ~ como siempre que se sepa la clase que usted está tratando con para entonces ~ de averiguar los nombres de los campos, y generar setters "estáticos" a partir de eso. Hace algún tiempo, escribí un código que hace bastante esto para getters que puede resultar interesante. Consulte https://github.com/joodie/clj-java-fields (especialmente, la macro def-fields en https://github.com/joodie/clj-java-fields/blob/master/src/nl/zeekat/java/fields.clj).

+0

Este es un enfoque ordenado. – amalloy

+0

Gracias, esto fue realmente útil. Necesitaba dar vuelta mi enfoque al revés y en lugar de tomar cualquier parámetro de mapa y luego tratar de reflejar en tiempo de ejecución, enumerar setters en tiempo de compilación en su lugar. Una ventaja adicional es que solo se verifican los parámetros válidos. – bkirkbri

1

La macro podría ser tan simple como:

(defmacro set-obj-map [a & r] `(doto (~a) [email protected](partition 2 r))) 

Pero esto podría hacer que su código como:

(set-obj-map TransactionRequest. .amount 10.00 .orderId "user42") 

cual supongo que no es lo que usted prefiere :)

+0

Es cierto que esta sintaxis no es ideal y tampoco puede ser utilizada por quienes llaman a su función. De todos modos, necesitaría asignar los argumentos de la persona que llama a esta sintaxis. – bkirkbri

+0

guau increíble. esta es la razón por la que amo aprender clojure. – Core

Cuestiones relacionadas