Vamos colocar uma palavra sobre decomposição de código: programação contextual

É claro que, idealmente, é melhor não escrever muito código . E se você escreve, então, como sabe, precisa pensar bem sistema de ossos arquitetura e implementação do sistema sistema de carne lógica do sistema. Nesta nota, daremos receitas para a implementação conveniente deste último.


Daremos exemplos para a linguagem Clojure, no entanto, o próprio princípio pode ser aplicado em outras linguagens de programação funcional (por exemplo, usamos exatamente a mesma idéia em Erlang).


Idéia


A ideia em si é simples e baseada nas seguintes declarações:


  • qualquer lógica sempre consiste em etapas elementares;
  • para cada etapa, são necessários certos dados, aos quais aplica sua lógica e produz um resultado bem-sucedido ou malsucedido.

No nível do pseudo-código, isso pode ser representado da seguinte maneira:


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


Onde:


  • do-something-elementary - nome da função;
  • context - argumento da função, estrutura de dados com o contexto inicial do qual a função obtém todos os dados necessários;
  • updated_context - estrutura de dados com contexto atualizado, se bem-sucedido, onde a função adiciona o resultado de sua execução;
  • reason - estrutura de dados, causa da falha, com falha.

Essa é a ideia toda. E então - uma questão de tecnologia. Com 100.500 milhões de peças.


Exemplo: usuário fazendo uma compra


Anotaremos os detalhes em um exemplo simples e concreto, disponível no GitHub aqui .
Digamos que temos usuários com dinheiro e muito que custam dinheiro e que usuários podem comprar. Queremos escrever um código que realize a compra do lote:


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


Para simplificar, armazenaremos a quantidade de dinheiro e muito do usuário na própria estrutura do usuário.


Para implementação, precisamos de várias funções auxiliares.


Função until-first-error


Na grande maioria dos casos, a lógica de negócios pode ser representada como uma sequência de etapas que precisam ser executadas até que ocorra um erro. Para fazer isso, criaremos uma função:


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


Onde:


  • fs é uma sequência de funções (ações elementares);
  • init_context é o contexto inicial.

A implementação desse recurso pode ser visualizada no GitHub aqui .


Função with-result-or-error


Muitas vezes, uma ação elementar é que você só precisa executar alguma função e, se tiver sido bem-sucedida, adicionar seu resultado ao contexto. Para fazer isso, temos uma função:


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


Em geral, o único objetivo desta função é reduzir o tamanho do código.


E, finalmente, a nossa "beleza" ...


Função que implementa uma 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]))) 

Vamos analisar o código:


  • 34: match é uma macro para combinar valores de um modelo da biblioteca clojure.core.match ;
  • 34-40: aplicamos a função prometida until-first-error nas etapas elementares fs , pegamos os dados de que precisamos no contexto e os retornamos, ou lançamos o erro;
  • p. 2-5: estamos construindo a primeira ação elementar (à qual somente o contexto atual será aplicado), que simplesmente adiciona dados sobre a chave :lot ao contexto atual;
  • 7-29: aqui usamos a função familiar with-result-or-error , mas a ação que ela envolveu foi um pouco mais complicada: em uma transação, verificamos se o usuário tem dinheiro suficiente e, se for bem-sucedido, fazemos uma compra (porque , por padrão, nosso aplicativo é multithread (e quem em algum lugar da última vez viu um aplicativo de thread único?) e devemos estar preparados para isso).

E algumas palavras sobre as outras funções que usamos:


  • lot-db/find-by-id(id) - retorna muito, por id ;
  • user-db/update-by-id!(user_id, update-user-fn) - aplica a função update-user-fn ao usuário user_id (em um banco de dados imaginário).

E para testar? ...


Teste este aplicativo de amostra do clojure REPL. Iniciamos o REPL a partir do console a partir da raiz do projeto:


 lein repl 

Quais são nossos usuários com finanças:


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

O que temos lotes (mercadorias):


 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 uma maçã:


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

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

E as "nozes":


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

Não havia dinheiro suficiente para nozes.


Total


Como resultado, usando a programação contextual, não haverá mais grandes partes de código (não cabem em uma tela), além de “métodos longos”, “grandes classes” e “longas listas de parâmetros”. E dá:


  • economizando tempo lendo e entendendo o código;
  • Teste de código simplificado
  • a capacidade de reutilizar o código (incluindo o uso de copiar e colar + arquivo dopilivanie);
  • simplificação da refatoração de código.

I.e. tudo o que amamos e praticamos.


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


All Articles