善良和糟糕的酏剂

2021-06-11 03:02:30

我此时见过很多酏剂,两者都很好。通过所有代码,我已经看到类似的模式,往往会导致更糟糕的代码。所以我以为我会把其中一些文件和这些模式的更好的替代品。

map.get / 2和关键字.get / 2锁定您使用特定的数据结构。这意味着如果要更改结构类型,您现在需要更新所有呼叫站点。而不是这些函数,您应该更喜欢使用访问:

侧面实现的功能倾向于返回“结果”,如{:OK,术语()} | | {:错误,术语()}。如果您处理副作用函数,请不要将结果送入下一个功能。使用任一情况或与之直接处理结果总是更好。

#don' t do ... def main do数据|> call_service |> parse_response |> handle_result结束defp call_service(data)do#...结束defp parse_response({:确定,结果},do:jason。解码(结果)defp parse_response(错误,do:错误)defp handle_result({:确定,解码}) ,做:解码defp handle_result({:错误,错误}),do:提出错误#do ... def main与{:确定,响应}< - call_service(data),{:确定,解码}< - PARSE_RESPONSE(响应)进行解码结束结束

使用管道强制我们的功能来处理以前的函数的结果,在整个各种函数调用中传播错误处理。这里的核心问题是微妙的,但内化至关重要。这些功能中的每一个都必须了解有关如何调用的太多信息。良好的软件设计主要是关于构建可任意组成的可重用位。在管道示例中,该功能知道它们是如何使用的,它们是如何调用的,以及他们组成的顺序。

管道方法的另一个问题是它倾向于假设可以易于处理错误。这个假设通常不正确。

处理副作用时,唯一有足够信息的函数来决定与错误有关的是调用函数。在许多系统中,错误情况同样重要 - 如果不是更重要的 - 而不是比“快乐的路径”案例。错误情况是您必须必须执行后备或优雅的退化。

如果您在错误的情况下是函数控制流的重要组成部分,那么最好地使用案例语句保持调用函数中的所有错误处理。

#do ... def main(id)do case:保险丝。检查(:服务)DO:OK - > case call_service(id)do {:确定,结果} - > :好的=缓存。放(ID,结果){:确定,结果} {:错误,错误} - > :保险丝。熔化(:服务){:错误,错误}结束:吹 - >缓存=缓存。获取(id)如果缓存do {:确定,结果} else {:错误,错误}结束结束

这增加了调用函数的大小,但是您可以阅读整个函数并理解每个控制流程。

我曾经在围栏上围绕管道进入案例陈述,但我已经看到这种模式滥用太多次数。严重的是,放下管道操作员并显示有点克制。如果您发现自己的管道进入案例,它几乎总是最好分配到变量的中间步骤。

#don' t do ... build_post(attrs)|> store_post()|>案例do {:好的,发布} - > #... {:错误,_} - > #...结束#do ... chankinget = build_post(attrs)case store_post(chanceset)do {:好的,post} - > #... {:错误,_} - > # ... 结尾

高阶函数很棒,所以尽量不要将它们隐藏起来。如果您正在使用集合,则应更喜欢编写在单个实体而不是集合本身上运行的功能。然后,您可以直接在管道中使用高阶函数。

#don' t do ... def main do collection |> Parse_Items |> add_items结尾parse_items(list)do枚举。地图(列表,& String。to_integer / 1)结束def add_items(list)do枚举。减少(列表,0,&& 1 +& 2)结束#do ... def main do collection |>枚举。地图(& parse_item / 1)|>枚举。减少(0,& add_item / 2)结束defp parse_item(项目),do:string。 to_integer(项目)defp add_item(num,acc),do:num + acc

通过此更改,我们的PARSE_ITEM和ADD_ITEM功能在更广泛的上下文集中可重复使用。现在可以在单个项目上使用这些功能,或者可以升到流,枚举,任务或任意数量的其他用途的上下文中。隐藏此逻辑远离呼叫者是一种更糟糕的设计,因为它将函数耦合到其呼叫站点,并使它不太可重复使用。理想情况下,我们的API在广泛的背景下可重复使用。

这种变化的另一个好处是更好的解决方案可能会揭示自己。在这种情况下,我们可以决定我们不需要命名功能,可以使用匿名功能。我们意识到我们不需要减少和可以使用总和。

最后一步可能并不总是正确的选择。这取决于您的功能在做多少工作。但是,作为一般规则,您应该努力消除只有单个呼叫站点的功能。即使这些函数没有专用名称,最终版本也不少于启动时的“可读”。 Elixir程序员仍然可以查看这一系列步骤,并了解目标是将字符串集合转换为整数,然后将这些整数进行总计。并且,他们可以在不需要沿途读取任何其他功能的情况下实现这一点。

如果您需要执行返回所有错误值的操作,则可以有帮助。您不应该用别人来处理所有潜在的错误(甚至是大量错误)。

#don' t do ...使用{:确定,响应}< - call_service(data),{:确定,解码}< - jason。解码(响应),{:确定,结果}< - store_in_db(解码)do:确定{:错误,%jason。错误{} =错误} - > #用json错误做点什么{:错误,%serviceError {} =错误} - > #使用服务错误{:错误,%dberror {}} - > #用db错误结束做点什么

出于同样的原因,在任何情况下,您都应该用名称注释您的函数调用,以便您可以区分它们。

使用{:service,{:确定,resp}}< - {:service,call_service(data)},{:解码,{:确定,解码}}< - {:decode,jason。解码(RESP)},{:db,{:确定,结果}}< - {:db,store_in_db(解码)} do:确定{:service,{:错误,错误}} - > #用服务错误做点什么{:解码,{:错误,错误}} - > #用JSON错误做点什么{:db,{:错误,错误}} - > #用db错误结束做点什么

如果您发现自己这样做,这意味着错误条件很重要。这意味着你根本不想要。你想要案例。

在不担心特定错误或相反模式的情况下,您可以在任何时候都可以掉落。创建更统一的方式来处理错误的好方法是构建一个常见的错误类型:

defmodule myapp。错误de defexception [:代码,:meta:meta] def新建(代码,消息,meta)在is_binary(msg)do%__module__ {code:code,msg:msg,meta:map。 New(Meta)}}结束def not_found(msg,meta \\%{})做新的(:not_found,msg,meta)结束def内部dem(msg,meta \\%{})do new(:msg,msg,meta )结束结束DEF主要与{:OK,响应}< - call_service(数据),{:确定,解码}< - 解码(响应),{:确定,结果}< - store_in_db(解码)do:好的结束#我们将jason.decode的结果包装在我们自己的自定义错误类型defp解码(RESP)与{:错误,e}< - jason。解码(resp)do {:错误,错误。内部("无法解码:#{inspect resp}")}结束结束

此错误结构提供了统一的方式来浏览应用程序中的所有错误。结构可以在Phoenix控制器中呈现错误,也可以从RPC处理程序返回。由于Surruct您的使用是一个例外,所以来电也可以选择提出错误,并且您将获得格式化的错误消息。

您应该有意地了解您的功能的要求。不要打扰检查一个值不是nil如果你期望它是一个字符串:

#don' t do ... def call_service(%{req:req}),当不是is_nil(req)do#...结束#do ... def call_service(%{req:req})ins_binary( req)do#...结束

对于案例陈述以及IF语句也是如此。更明确地了解您的期望。如果您收到违反您期望的论据,您更愿意提高或崩溃。

您应该只强制您的用户处理他们可以做点什么的错误。如果您的API可能会出现错误,并且呼叫者无法对此进行任何事情,然后提出异常或抛出。当他们无能为力时,不要打扰你的呼叫者处理结果元组。

#don' t do ... def get(table \\ __module__,ID)do#,如果表没有,则存在ETS会抛出错误。捕获并返回#错误元组尝试执行以下操作:ETS。查找(表,ID)捕获_,_ - > {:错误,"表格不可用" }结束#do ... def get(表\\ __module__,ID)do#如果表没有存在,那里的'没有呼叫者可以做#关于它,所以刚刚扔。 :ETES。查找(表,ID)结束

如果返回值或数据违反了您的期望,您不应该害怕仅提高异常。如果您调用了应该始终返回JSON的下游服务,请使用Jason.decode!并避免编写额外的错误处理逻辑。

#don' t do ... def main do {:好的,resp} = call_service(id)case jason。解码(RESP)DO {:OK,解码} - >解码{:错误,e} - > #现在是什么?...结束#do ... def main do {:好的,resp} = call_service(id)解码= jason。解码! (resh)结束

这允许我们崩溃该过程(这是好的),并从功能中删除无用的错误处理逻辑。

#don' t do ...断言枚举。全部? (帖子,fn后>%post {} ==帖子结束)#do ...对于post< posts,do:senert%post {} == post