Hablaremos sobre la descomposición del código: programación contextual

Por supuesto, idealmente, es mejor no escribir demasiado código . Y si escribes, entonces, como sabes, debes pensar bien sistema de huesos arquitectura del sistema e implementación sistema de carne lógica del sistema En esta nota daremos recetas para la implementación conveniente de este último.


Daremos ejemplos para el lenguaje Clojure, sin embargo, el principio en sí puede aplicarse en otros lenguajes de programación funcionales (por ejemplo, usamos exactamente la misma idea en Erlang).


Idea


La idea en sí es simple y se basa en las siguientes afirmaciones:


  • cualquier lógica siempre consiste en pasos elementales;
  • para cada paso, se necesitan ciertos datos, a los cuales aplica su lógica y produce un resultado exitoso o no exitoso.

En el nivel de pseudocódigo, esto se puede representar de la siguiente manera:


do-something-elementary(context) -> [:ok updated_context] | [:error reason]


Donde:


  • do-something-elementary - nombre de la función;
  • context : argumento de función, estructura de datos con el contexto inicial del que la función toma todos los datos necesarios;
  • updated_context : estructura de datos con contexto actualizado, si tiene éxito, donde la función agrega el resultado de su ejecución;
  • reason : estructura de datos, causa de la falla, con falla.

Esa es toda la idea. Y luego, una cuestión de tecnología. Con 100,500 millones de partes.


Ejemplo: usuario haciendo una compra


Anotaremos los detalles en un ejemplo simple y concreto, que está disponible en GitHub aquí .
Digamos que tenemos usuarios con dinero y lotes que cuestan dinero y que los usuarios pueden comprar. Queremos escribir un código que llevará a cabo la compra del lote:


buy-lot(user_id, lot_id) -> [:ok updated_user] | [:error reason]


Para simplificar, almacenaremos la cantidad de dinero y mucho del usuario en la propia estructura del usuario.


Para la implementación, necesitamos varias funciones auxiliares.


Función until-first-error


En la gran mayoría de los casos, la lógica empresarial puede representarse como una secuencia de pasos que deben seguirse hasta que se produzca un error. Para hacer esto, crearemos una función:


until-first-error(fs, init_context) -> [:ok updated_context] | [:error reason]


Donde:


  • fs es una secuencia de funciones (acciones elementales);
  • init_context es el contexto inicial.

La implementación de esta función se puede ver en GitHub aquí .


Función with-result-or-error


Muy a menudo, una acción elemental es que solo necesita ejecutar alguna función y, si fue exitosa, agregar su resultado al contexto. Para hacer esto, tenemos una función:


with-result-or-error(f, key, context) -> [:ok updated_context] | [:error reason]


En general, el único propósito de esta función es reducir el tamaño del código.


Y finalmente, nuestra "belleza" ...


La función que implementa la compra.


 1. (defn buy-lot [user_id lot_id] 2. (let [with-lot-fn (partial 3. util/with-result-or-error 4. #(lot-db/find-by-id lot_id) 5. :lot) 6. 7. buy-lot-fn (fn [{:keys [lot] :as ctx}] 8. (util/with-result-or-error 9. #(user-db/update-by-id! 10. user_id 11. (fn [user] 12. (let [wallet_v (get-in user [:wallet :value]) 13. price_v (get-in lot [:price :value])] 14. (if (>= wallet_v price_v) 15. (let [updated_user (-> user 16. (update-in [:wallet :value] 17. - 18. price_v) 19. (update-in [:lots] 20. conj 21. {:lot_id lot_id 22. :price price_v}))] 23. [:ok updated_user]) 24. [:error {:type :invalid_wallet_value 25. :details {:code :not_enough 26. :provided wallet_v 27. :required price_v}}])))) 28. :user 29. ctx)) 30. 31. fs [with-lot-fn 32. buy-lot-fn]] 33. 34. (match (util/until-first-error fs {}) 35. 36. [:ok {:user updated_user}] 37. [:ok updated_user] 38. 39. [:error reason] 40. [:error reason]))) 

Veamos el código:


  • p. 34: match es una macro para hacer coincidir valores de una plantilla de la biblioteca clojure.core.match ;
  • p. 34-40: aplicamos la función prometida until-first-error a los pasos elementales fs , tomamos los datos que necesitamos del contexto y los devolvemos, o arrojamos el error;
  • p. 2-5: estamos construyendo la primera acción elemental (a la que solo se aplicará el contexto actual), que simplemente agrega datos sobre la clave :lot al contexto actual;
  • p. 7-29: aquí usamos la función familiar with-result-or-error , pero la acción que envuelve resultó ser un poco más complicada: en una transacción, verificamos que el usuario tenga suficiente dinero y, si tiene éxito, hacemos una compra (porque , por defecto nuestra aplicación es multiproceso (¿y quién en algún lugar la última vez vio una aplicación de subproceso único?) y debemos estar preparados para esto).

Y algunas palabras sobre las otras funciones que utilizamos:


  • lot-db/find-by-id(id) : devuelve mucho, por id ;
  • user-db/update-by-id!(user_id, update-user-fn) : aplica la función update-user-fn al usuario user_id (en una base de datos imaginaria).

¿Y para probar? ...


Pruebe esta aplicación de muestra de clojure REPL. Comenzamos REPL desde la consola desde la raíz del proyecto:


 lein repl 

¿Cuáles son nuestros usuarios con finanzas?


 context-aware-app.core=> (context-aware-app.user.db/enumerate) [:ok ({:id "1", :name "Vasya", :wallet {:value 100}, :lots []} {:id "2", :name "Petya", :wallet {:value 100}, :lots []})] 

Lo que tenemos lotes (bienes):


 context-aware-app.core=> (context-aware-app.lot.db/enumerate) [:ok ({:id "1", :name "Apple", :price {:value 10}} {:id "2", :name "Banana", :price {:value 20}} {:id "3", :name "Nuts", :price {:value 80}})] 

Vasya compra una manzana:


 context-aware-app.core=>(context-aware-app.processing/buy-lot "1" "1") [:ok {:id "1", :name "Vasya", :wallet {:value 90}, :lots [{:lot_id "1", :price 10}]}] 

Y la banana:


 context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "2") [:ok {:id "1", :name "Vasya", :wallet {:value 70}, :lots [{:lot_id "1", :price 10} {:lot_id "2", :price 20}]}] 

Y las "nueces":


 context-aware-app.core=> (context-aware-app.processing/buy-lot "1" "3") [:error {:type :invalid_wallet_value, :details {:code :not_enough, :provided 70, :required 80}}] 

No había suficiente dinero para las nueces.


Total


Como resultado, utilizando la programación contextual, ya no habrá grandes fragmentos de código (que no caben en una sola pantalla), así como "métodos largos", "clases grandes" y "largas listas de parámetros". Y da:


  • ahorrando tiempo en leer y comprender el código;
  • Prueba de código simplificada
  • la capacidad de reutilizar el código (incluido el uso de copiar-pegar + archivo dopilivanie);
  • simplificación de la refactorización de código.

Es decir todo lo que amamos y practicamos.


Source: https://habr.com/ru/post/es413579/


All Articles