Ruby小妙招--对嵌套哈希值使用深度取值法的操作方法

129 阅读4分钟

哈希值是Ruby和Rails应用程序中最常见的数据结构。在本教程中,我将介绍一个简单的技巧,使处理哈希值时不容易出错。它还能提高代码的可读性,并为处理数据结构问题提供统一的方法。

对于一个快速的提示来说,这是一个很大的承诺,所以让我们开始吧!

如何不使用Ruby哈希值...

深度嵌套的哈希值在Rails应用程序中是一流的公民,而且写这样的代码是一种常见的做法。

if params[:user][:comment][:body].present?
  # some logic
else
  # other logic
end

这种方法的一个缺点是,它隐含地假设了一个哈希结构。在这个特殊的例子中,我们正在使用params ,所以是一个外部数据源。通过编写这样的代码,你允许你的用户崩溃应用程序,因为你乐观地假设收到的数据结构将总是正确的。无效的输入可能引起不同的错误,这取决于有效载荷。

在Ruby 2.3中引入的一个dig 方法可以提供一个轻微的改进。

begin
  if params.dig(:user, :comment, :body).present?
    # some logic
  else
    # other logic
  end
rescue TypeError
  # handle invalid structure
end

但是dig API使得它无法区分缺失的Hash键和包含nil 值的现有键。而在实践中,往往需要分别处理这两种情况。

哈希fetch 来拯救

一个内置的哈希fetch,为所述问题提供了另一种解决方案。让我们看看它的作用。

begin
  if params.fetch(:user).fetch(:comment).fetch(:body).present?
    # some logic
  else
    # other logic
  end
rescue KeyError
  # handle missing key
end

对于一个稍显冗长的实现的代价,我们现在可以很容易地处理params ,其中有丢失的键。但是我们仍然在做一个隐含的假设,即收到的数据将包含访问键中的嵌套哈希值。用户仍然可以通过发送下面的params ,使我们的应用程序崩溃。

{ user: { comment: nil } }

而且这种链式结构看起来有点难看。所以让我们看看如何用一个简单的Hash扩展来做得更好。

介绍deep_fetch

所以这里是我们使用自定义deep_fetch Hash方法的最终实现。

begin
  if params.deep_fetch(:user, :comment, :body) { raise ParamsError, "Invalid input" }.present?
    # some logic
  else
    # other logic
  end
rescue ParamsError
  # handle invalid params
end

deep_fetch 其工作原理类似于 和 的组合。当没有找到一个键时,它不会返回 ,而是引发一个 ,或者返回一个所提供的块的运行结果。下面是猴子补丁的实现。fetch dig nil KeyError

config/initializers/deep_fetch.rb

module DeepFetch
  def deep_fetch(*keys, &block)
    keys.reduce(self) do |hash_object, key|
      hash_object.fetch(key)
    end
  rescue NoMethodError, KeyError, ActionController::ParameterMissing => e
    if block_given?
      block.call
    else
      raise KeyError, e.message, e.backtrace
    end
  end
end

class Hash
  include DeepFetch
end

class ActionController::Parameters
  include DeepFetch
end

令人惊讶的是,ActionController::Parameters 并没有继承自Hash ,所以它必须被单独扩展。

如果你不喜欢Monkey-patching,你可以用refinements代替。

module HashHelpers
  refine Hash do
    include DeepFetch
  end

  refine ActionController::Parameters do
    include DeepFetch
  end
end

并在每个你想使用扩展的类中包含HashHelpers

class UsersController
  using HashHelpers
end

通过使用deep_fetch ,我们可以处理所有描述的情况。如果结构无效,我们会收到一个易于救援的错误,所以用户不能再通过发送无效的输入来破坏我们的应用程序。即使收到的值是nil ,我们也可以确定它是从结构正确的参数中提取出来的。

有意见的摘要

deep_fetch 可以作为重量级库的一个可行的替代品,用于验证任何Ruby Hash的结构。我甚至建议去假设Hash括号符号是一个明确的标志,即缺失的键是预期的,应该被相应的处理。这意味着下面的代码不应该通过代码审查。

value = params[:comment][:body]

它隐含地假设comment 包含一个Hash,所以它是生产中随机NoMethodError bug的一个极有可能的来源。对于内部哈希值,即那些不是从用户那里收到的哈希值,采用深度获取的方法也是有意义的。我曾经在代码审查中为这个问题与诸如以下的评论争吵过。

"对于我确信存在的值,使用deep_fetch 有什么意义?"。

而这正是问题的关键所在!通过使用deep_fetch ,你明确表示值必须在那里,并且明确表示不希望出现无效结构。

我认为坚持总是深层获取的惯例可以提供很多好处,而且复杂度开销最小。实施起来就像在你的项目中加入十几行代码一样简单,所以我非常鼓励你去尝试一下。