Ruby中的安全枚举(附实例)

78 阅读6分钟

对于那些从Java、C#和类似的OOP语言进入Ruby的人来说,可能会对Ruby没有开箱即用的枚举实现感到非常惊讶。有人可能会争辩说,Ruby中有Symbol 类型,用这个来代替就足够了。好吧,对于一个非常小的项目来说,这可能是真的,但是如果你有一个大的项目,事情就会变得更加困难。

这到底是怎么回事?

首先,让我们试着理解什么是枚举,它们解决了哪些问题,以及它们被发明出来的原因。我将引用维基百科的内容。

......如果程序员希望一个变量,例如myColor ,其值为red ,那么将声明变量red ,并分配一些任意的值,通常是一个整数常数。然后,变量red 将被分配给myColor...

这些任意值有时被称为神奇的数字,因为通常没有解释这些数字是如何得到的,或者它们的实际值是否有意义。这些神奇的数字会让其他人更难理解和维护源代码。

另一方面,枚举类型使代码更具有自我记录性。根据语言的不同,编译器可以自动为枚举器分配默认值,从而向程序员隐藏不必要的细节。这些值甚至可能对程序员不可见。枚举类型还可以防止程序员编写不合逻辑的代码,例如对枚举器的值进行数学运算。如果要打印一个被分配了枚举器的变量的值,一些编程语言也可以打印枚举器的名称而不是它的基本数值。另一个优点是,枚举类型可以让编译器强制执行语义正确性。例如:myColor = TRIANGLE可以被禁止,而myColor = RED可以被接受,即使TRIANGLERED 内部都表示为1

从概念上讲,一个枚举类型类似于一个名词表(数字代码),因为该类型的每个可能值都被分配了一个独特的自然数。因此,一个给定的枚举类型是这个概念的具体实现。当顺序是有意义的和/或用于比较时,那么一个枚举类型就变成了一个序数类型。

简而言之,目前我们在许多编程语言中都有枚举的实现(注意,只有来自脚本家族的Python实现了它们,而且有趣的是,它只从最新版本之一--3.4开始)。早期人们使用整数来枚举数值,这种方法有缺点。在编程中,枚举类型基本上解决了以下问题:

  • 不可预测的行为。
  • 维护。
  • 代码的可读性。

而这种类型有以下特点:

  • 它定义了一组标识符(一组命名的值,称为该类型的枚举器或元素)。
  • 这些元素的行为是常量。
  • 一个被声明为具有枚举类型的变量可以被分配任何元素。

值得注意的是,现有的Symbols和其他标准的Ruby类型都没有涵盖所有的枚举定义。

解决方案

在我们的项目(Mezuka)中,我们有很多实体应该表现得像枚举。实际上,我们使用了一个已经实现的Gem(说实话,我以前在我的项目中使用了许多Ruby中枚举的实现)。但它并不适合我们,而且有多余的功能。在另一次重构中,我们引入了我们自己的库,解决了这些问题,并且完全适合我们。最后我们发布了这个名为safe-enumgem

那么,它能解决什么问题呢?

  • 序列化到数据库中--值应该像读和写一样在Ruby中表示,没有任何隐患。
  • 安全性 - 如果没有定义枚举值,我应该有运行时错误(最好是复杂的错误,但是,记住,在Ruby中我们没有编译器)。
  • 易于定义--简单的DSL。
  • 它应该是一个序数类型--我们应该能够比较枚举值,并在某些时候对它们进行排序。
  • 它应该在纯Ruby类中使用(不仅仅是与ActiveRecord或其他ORM一起使用)。
  • 方便枚举值的国际化(I18n)和它们的键的维护。

这些都是架构师对gem的决定,它允许按照我们的意愿来实现它:

  • 由于枚举值应该被写入数据库,由于整数的缺点和我们的数据库(PostgreSQL)不支持符号(或原子)的事实,所有的枚举值在gem内部只被表示为字符串。
  • 枚举被保存到Set ,以便快速查明值是否有效和定义。
  • 值定义的顺序被保存,它们有其整数表示,以便进行比较(为简单起见,只保存其定义索引)。
  • 当试图使用枚举值时,如果没有定义的枚举值,就会引发一个异常
  • 对于枚举值和I18n键的定义,易于使用DSL。

我在这里贴上了一些 gem 使用的例子:

require 'enum'
require 'i18n'
require 'yaml'


# The enum values definition feature demo:
class Side < Enum::Base
  values :left, :right
end


# The safe enums retrieving feature demo:
Side.enum(:left) # => "left"
Side.enum('left') # => "left"
Side.enum(:invalid) # => Enum::TokenNotFoundError: token 'invalid'' not found in the enum Side
Side.enum('invalid') # => Enum::TokenNotFoundError: token 'invalid'' not found in the enum Side
Side.all # => ['left', 'rigth', 'whole']
Side.enums(:left, :right) # => ['left', 'right']


# The I18n feature demo:
I18n.enforce_available_locales = false

# This is the content of yaml file that holds the enum values translations:
translations = <<-YAML
enum:
  Side:
    left: 'Left'
    right: 'Right'
YAML

I18n.backend.store_translations(I18n.locale, YAML.load(translations))

Side.name(:left) # => "Left"
Side.name('left') # => "Left"
Side.name(:right) # => "Right"
Side.name('right') # => "Right"
Side.name(:invalid) # => Enum::TokenNotFoundError: token 'invalid'' not found in the enum Side

# The enum values comparison feature demo:
class WeekDay < Enum::Base
  values :sunday, :monday, :tuesday, :wednesday, :thusday, :friday, :saturday
end
WeekDay.index(:sunday) == Date.new(2015, 9, 13).wday # => true
WeekDay.index(:monday) # => 1
WeekDay.indexes # => [0, 1, 2, 3, 4, 5, 6]

注意:I18n功能是可选的。因此,如果你的项目中没有安装I18n,在调用名称方法时将会产生NameError异常。

作为奖励,我们引入了安全设置器和方便的谓词,以便在Ruby对象中操作枚举值:

class Table
  extend Enum::Predicates

  attr_accessor :side

  enumerize :side, Side
end

@table = Table.new
@table.side_is?(:left) # => false
@table.side_is?(nil) # => false

@table.side = Side.enum(:left)
@table.side_is?(:left) # => true
@table.side_is?(:right) # => false
@table.side_is?(nil) # => false
@table.side_is?(:invalid) # => Enum::TokenNotFoundError: token 'invalid'' not found in the enum Side

@table.side = 'invalid'
@table.side_is?(nil) # => false
@table.side_is?(:left) # => Enum::TokenNotFoundError: token 'invalid'' not found in the enum Side
@table.side_any?(:left, :right) # => true
@table.side_any?(:right) # => false
@table.side_any?(:invalid, :left) # => Enum::TokenNotFoundError: token 'invalid'' not found in the enum Side

如果你传递给谓词nil 或在字段中有nil 的值,结果将总是false 。如果你想检查这个字段是否是nil ,只需使用Ruby的标准方法nil?

替代方法

有许多其他的宝石试图实现Ruby枚举,但正如我已经说过的,它们中没有一个适合我们。这有很多原因。我不会在这篇文章中单独回顾它们,但是,用两句话来说,我可以在这里说,它们都违反了规则--枚举应该像常量一样表现。有些实现还违反了枚举定义中的其他规则,即存在冗余。

总结

在我们的宝石中,我们试图遵循枚举定义的规则,并使解决方案易于使用和维护。我认为我们成功地做到了这一点。如果你有任何反馈,请发表评论,在github上发布问题,我们永远欢迎你的拉动请求。