About code decomposition let's say a word: contextual programming

Of course, ideally, it’s better not to write extra code at all . And if you write, then, as you know, you need to think well bones system system architecture and implement meat system system logic. In this article we present recipes for convenient implementation of the latter.


We will provide examples for the Clojure language, but the principle itself can be applied in other functional programming languages ​​(for example, we use exactly the same idea in Erlang).


Idea


The idea itself is simple and is based on the following statements:



At the pseudo-code level, this can be represented as:


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


Where:



That's the whole idea. And then - the matter of technology. With 100,500 million parts.


Example: user buying


Let's write the details on a specific simple example, which is available on GitHub here .
Suppose that we have users with money and lots that cost money and which users can buy. We want to write the code that will conduct the purchase of the lot:


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


For simplicity, we will keep the amount of money and user lots in the user structure itself.


For implementation, we need several auxiliary functions.


Function until-first-error


In the overwhelming number of cases, business logic can be represented as a sequence of steps that need to be done before an error has occurred. For this we will create a function:


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


Where:



You can see the implementation of this function on GitHub here .


with-result-or-error function


Very often, the elementary action is that you just need to perform some function and, if it succeeds, add its result to the context. To do this, let's get the function:


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


In general, the sole purpose of this function is to reduce the size of the code.


And finally, our "beauty" ...


The function that implements the purchase


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

Go through the code:



And a few words about the other functions that we used:



And to test? ...


Let's test this sample application from clojure REPL. We start the REPL from the console from the project root:


 lein repl 

What we have users with 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 []})] 

What we have lots (goods):


 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" buys an "apple":


 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}]}] 

And "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}]}] 

And "Nuts":


 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}}] 

On the "nuts" did not have enough money.


Total


As a result, using contextual programming, there will no longer be huge pieces of code (not fit into one screen), as well as “long methods”, “large classes” and “long lists of parameters”. And it gives:



Those. all that we love and practice.


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


All Articles