当然,理想情况下,最好不要编写太多代码 。 如果您写的话,就如您所知,您需要认真思考 骨骼系统 系统架构与实现 肉类系统 系统逻辑。 在本说明中,我们将给出方便实现后者的方法。
我们将以Clojure语言为例,但是该原理本身也可以应用在其他函数式编程语言中(例如,我们在Erlang中使用的概念完全相同)。
主意
这个想法本身很简单,并基于以下陈述:
- 任何逻辑总是由基本步骤组成;
- 对于每个步骤,都需要某些数据,然后对其应用其逻辑并产生成功或失败的结果。
在伪代码级别,这可以表示如下:
do-something-elementary(context) -> [:ok updated_context] | [:error reason]
其中:
do-something-elementary
-函数名称;context
函数参数,具有初始上下文的数据结构,函数从该上下文中获取所有必要的数据;updated_context
具有成功上下文的数据结构(如果成功),其中函数将其执行结果相加;reason
-数据结构,失败原因以及失败。
这就是整个想法。 然后-技术问题。 拥有1005亿个零件。
示例:用户进行购买
我们将在一个具体的简单示例上写下详细信息,该示例可在GitHub上找到 。
假设我们有很多有钱的用户,还有很多花钱的用户可以购买。 我们想编写一个代码来执行该批次的购买:
buy-lot(user_id, lot_id) -> [:ok updated_user] | [:error reason]
为简单起见,我们将在用户结构本身中存储金钱和大量用户。
为了实现,我们需要几个辅助功能。
until-first-error
在绝大多数情况下,业务逻辑可以表示为在发生错误之前需要采取的一系列步骤。 为此,我们将创建一个函数:
until-first-error(fs, init_context) -> [:ok updated_context] | [:error reason]
其中:
fs
是一系列功能(基本动作);init_context
是初始上下文。
可以在GitHub上查看此功能的实现。
with-result-or-error
通常,基本操作是您只需要执行一些功能,如果成功,就将其结果添加到上下文中。 为此,我们有一个功能:
with-result-or-error(f, key, context) -> [:ok updated_context] | [:error reason]
通常,此功能的唯一目的是减小代码大小。
最后,我们的“美丽” ...
实现购买的功能
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])))
让我们看一下代码:
- 第34页:
match
是一个宏,用于匹配clojure.core.match
库中模板的值; - p。34-40:我们将承诺的
until-first-error
函数应用于基本步骤fs
,从上下文中获取我们需要的数据并返回它,或者抛出错误; - 第2-5页:我们正在构建第一个基本操作(仅将当前上下文应用于该基本操作),该操作仅将key
:lot
数据添加到当前上下文; - 第7-29页:这里我们使用了熟悉的功能
with-result-or-error
,但包装起来的操作却变得有些棘手:在一次交易中,我们检查用户是否有足够的钱,如果成功,我们进行购买(因为,默认情况下,我们的应用程序是多线程的 (最后一次在哪个地方看到单线程应用程序?) 我们必须为此做好准备)。
关于我们使用的其他功能的几句话:
lot-db/find-by-id(id)
-通过id
返回很多;user-db/update-by-id!(user_id, update-user-fn)
-将update-user-fn
函数应用于用户user_id
(在虚拟数据库中)。
并进行测试?...
从clojure REPL测试此样本应用程序 。 我们从项目根目录从控制台启动REPL:
lein repl
我们的财务用户是什么:
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 []})]
我们有很多(货):
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买一个苹果:
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}]}]
还有香蕉:
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}]}]
和“坚果”:
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}}]
没有足够的钱买坚果。
合计
结果,使用上下文编程,将不再有大量的代码(不适合一个屏幕),也不再是“长方法”,“大类”和“长参数列表”。 它给出了:
- 节省阅读和理解代码的时间;
- 简化的代码测试
- 重用代码的能力(包括使用复制粘贴+ dopilivanie文件);
- 简化代码重构。
即 我们热爱和实践的一切。