É 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.