Bien sûr, idéalement, il vaut mieux ne pas écrire trop de code du tout . Et si vous écrivez, alors, comme vous le savez, vous devez bien penser système osseux architecture système et implémentation système de viande logique du système. Dans cette note, nous donnerons des recettes pour une mise en œuvre pratique de cette dernière.
Nous donnerons des exemples pour le langage Clojure, cependant, le principe lui-même peut être appliqué dans d'autres langages de programmation fonctionnels (par exemple, nous utilisons exactement la même idée dans Erlang).
Idée
L'idée elle-même est simple et basée sur les affirmations suivantes:
- toute logique consiste toujours en étapes élémentaires;
- pour chaque étape, certaines données sont nécessaires, auxquelles il applique sa logique et produit un résultat réussi ou non.
Au niveau du pseudo-code, cela peut être représenté comme suit:
do-something-elementary(context) -> [:ok updated_context] | [:error reason]
Où:
do-something-elementary
- nom de la fonction;context
- argument de fonction, structure de données avec le contexte initial duquel la fonction prend toutes les données nécessaires;updated_context
- structure de données avec un contexte mis à jour, en cas de succès, où la fonction ajoute le résultat de son exécution;reason
- structure des données, cause de l'échec, avec échec.
Telle est l'idée. Et puis - une question de technologie. Avec 100 500 millions de pièces.
Exemple: utilisateur effectuant un achat
Nous allons écrire les détails sur un exemple simple et concret, qui est disponible sur GitHub ici .
Disons que nous avons des utilisateurs avec de l'argent et beaucoup qui coûtent de l'argent et que les utilisateurs peuvent acheter. Nous voulons écrire un code qui effectuera l'achat du lot:
buy-lot(user_id, lot_id) -> [:ok updated_user] | [:error reason]
Par souci de simplicité, nous allons stocker le montant d'argent et beaucoup de l'utilisateur dans la structure d'utilisateur elle-même.
Pour la mise en œuvre, nous avons besoin de plusieurs fonctions auxiliaires.
Fonction until-first-error
Dans la grande majorité des cas, la logique métier peut être représentée comme une séquence d'étapes qui doivent être suivies jusqu'à ce qu'une erreur se produise. Pour ce faire, nous allons créer une fonction:
until-first-error(fs, init_context) -> [:ok updated_context] | [:error reason]
Où:
fs
est une séquence de fonctions (actions élémentaires);init_context
est le contexte initial.
L'implémentation de cette fonctionnalité peut être consultée sur GitHub ici .
Fonction with-result-or-error
Très souvent, une action élémentaire est qu'il vous suffit d'exécuter une fonction et, si elle a réussi, d'ajouter son résultat au contexte. Pour ce faire, nous avons une fonction:
with-result-or-error(f, key, context) -> [:ok updated_context] | [:error reason]
En général, le seul but de cette fonction est de réduire la taille du code.
Et enfin, notre "beauté" ...
La fonction qui met en œuvre l'achat
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])))
Passons en revue le code:
- p. 34:
match
est une macro pour faire correspondre les valeurs d'un modèle de la bibliothèque clojure.core.match
; - p. 34-40: nous appliquons la fonction promise
until-first-error
aux étapes élémentaires fs
, prenons les données dont nous avons besoin du contexte et les renvoyons, ou rejetons l'erreur; - p. 2-5: nous construisons la première action élémentaire (à laquelle seul le contexte actuel sera appliqué), qui ajoute simplement des données sur la clé
:lot
au contexte actuel; - p. 7-29: nous utilisons ici la fonction familière
with-result-or-error
, mais l'action qu'elle encapsule s'est révélée un peu plus délicate: en une seule transaction, nous vérifions que l'utilisateur a suffisamment d'argent et, en cas de succès, nous effectuons un achat (car , par défaut notre application est multithread (et qui quelque part la dernière fois a vu une application monothread?) et nous devons nous y préparer).
Et quelques mots sur les autres fonctions que nous avons utilisées:
lot-db/find-by-id(id)
- retourne beaucoup, par id
;user-db/update-by-id!(user_id, update-user-fn)
- applique la fonction update-user-fn
à l'utilisateur user_id
(dans une base de données imaginaire).
Et pour tester? ...
Testez cet exemple d'application à partir de clojure REPL. Nous démarrons REPL depuis la console depuis la racine du projet:
lein repl
Quels sont nos utilisateurs avec des finances:
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 []})]
Ce que nous avons beaucoup (marchandises):
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 achète une pomme:
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}]}]
Et la banane:
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}]}]
Et les "noix":
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}}]
Il n'y avait pas assez d'argent pour les noix.
Total
Par conséquent, en utilisant la programmation contextuelle, il n'y aura plus d'énormes morceaux de code (qui ne rentrent pas dans un seul écran), ainsi que des «méthodes longues», des «grandes classes» et des «longues listes de paramètres». Et cela donne:
- gagner du temps sur la lecture et la compréhension du code;
- Test de code simplifié
- la possibilité de réutiliser le code (y compris en utilisant copier-coller + fichier dopilivanie);
- simplification du refactoring de code.
C'est-à-dire tout ce que nous aimons et pratiquons.