哈希值是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 ,你明确表示值必须在那里,并且明确表示不希望出现无效结构。
我认为坚持总是深层获取的惯例可以提供很多好处,而且复杂度开销最小。实施起来就像在你的项目中加入十几行代码一样简单,所以我非常鼓励你去尝试一下。