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 wellbones system system architecture and implement meat systemsystem 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:


    • any logic always consists of elementary steps;
    • for each step, certain data is needed, to which it applies its logic and produces either a successful or unsuccessful result.

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


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


    Where:


    • do-something-elementary - the name of the function;
    • context - the function argument, the data structure with the initial context, from which the function takes all the necessary data;
    • updated_context - data structure with an updated context, with success, where the function adds the result of its execution;
    • reason - data structure, the reason for failure, in case of failure.

    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:


    • fs - the sequence of functions (elementary actions);
    • init_context - initial context.

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


    Function with-result-or-error


    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 (partial3.                       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-error9.                        #(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_value25.                                          :details {:code:not_enough26.                                                    :provided wallet_v
    27.                                                    :required price_v}}]))))
    28.                        :user29.                        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:


    • p. 34: match- this is a macro for matched values ​​according to a template from the library clojure.core.match;
    • pp. 34-40: we apply the promised function until-first-errorto elementary steps fs, take the data we need from the context and return them, or throw an error up;
    • pp. 2-5: we build the first elementary action (to which only the current context will apply), which simply adds data by key :lotto the current context;
    • pp. 7-29: here we use a familiar function with-result-or-error, but the action that it wraps turned out to be slightly more tricky: in one transaction we check that the user has enough money and in case of success we make a purchase (for, by default, our application is multi-threaded(and who somewhere saw the single-threaded application for the last time?) and we should be ready for this).

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


    • lot-db/find-by-id(id)- returns the lot by id;
    • user-db/update-by-id!(user_id, update-user-fn)- applies the function update-user-fnto the user user_id(in an imaginary database).

    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 {:value100}, :lots []} 
          {:id"2", :name"Petya", :wallet {:value100}, :lots []})]

    What we have lots (goods):


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

    "Vasya" buys an "apple":


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

    And "banana:


    context-aware-app.core=> (context-aware-app.processing/buy-lot "1""2")
    [:ok {:id"1", :name"Vasya", :wallet {:value70}, :lots [{:lot_id"1", :price10} {:lot_id"2", :price20}]}]

    And "Nuts":


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

    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:


    • saving time on reading and understanding the code;
    • simplified code testing;
    • the ability to reuse the code (including using copy-paste + file finishing);
    • simplified code refactoring.

    Those. all that we love and practice.



    Also popular now: