BigBing 技术博客

ActiveRecord Enum实战总结

基本使用方法

案例说明: 给已经存在的 Company 增加一个 size 属性, 属性包括 large, medium, small 三个选项

从 Rails4.1 开始,可以通过 ActiveRecord::Enum 来快速实现这样的功能

class Company < ActiveRecord::Base
  enum size: [:large, :medium, :small]
end

定义了 enum 后,Rails会自动产生下面这些方法

Company.sizes
# => {"large"=>0, "medium"=>1, "small"=>2}

# Scope methods
Company.large
Company.medium
Company.small

# Query methods
company.large?
company.medium?

# Action methods
company.large!
company.small!

Migration

enum 的实现是基于该字段是一个 integer 类型的。所以添加这个新的字段我们需要下面的 migration

class AddSizeToCompanies < ActiveRecord::Migration
  def change
    add_column :companies, :size, :integer, null: false, default: 0
    add_index  :companies, :size
  end
end

赋值操作

通过上面 Company.sizes 方法,我们看到 Rails 默认是从0开始对应的。所以上面的 migration 中默认值是0。当新建一个Company时,默认 company 的 size 是 large

company = Company.new
company.size # => "large"

# 多种赋值操作
company.size = :medium
company.medium? # => true

company.size = 2
company.small? # => true

company.large = 'large'
company.large? # => true

enum 会自动添加一些验证,如果给 size 属性赋错误的值,Rails会抛出异常

company.size = 5
# => ArgumentError: 5 is not a valid billing_category
company.size = :bala
# => ArgumentError: 'bala' is not a valid billing_category

Form下拉菜单的填充

当在前端的Form中填充到下拉列表中,可以这样做

f.input :size, collection: Company.sizes.keys.map {|s| [s.titleize, s]}, prompt: "Select a size"

需要注意的地方:

由于存到数据库的仍然是 number,所以如果有别的应用也使用同样的数据库,那么该应用需要知道对应关系。

Rails4.1 及之后的版本中,由于 enum 会自动产生一些方法,所以要特别注意选项的命名问题,尽量用明确的命名。 另外如果你还需要不同的属性,拥有相同的选项,那么你可以考虑这个 gem activerecord-enum-without-methods

同时在 Rails5 中,就可以使用_prefix_postfix选项来避免相应的问题。·

更好的Migration方法

上面的migration方法对于产品环境已经有数据的情况下,可能会产生问题。

对于Mysql 数据库,新加字段的时候对于已有的记录,mysql 会自动设置 NOT NULL 的已有记录为 default 但是对于 postgreSQL 就会出现下面的错误:

PG::NotNullViolation: ERROR: column “size” contains null values

这时可以先add_column添加字段,不要加 NOT NULL 的限制,然后更新已有数据,然后再通过change_column_null来添加 NOT NULL 限制。

用CSS隐藏元素的五种方法

1. opacity: 0

.hide {
  opacity: 0;
}      

相当于设置元素为透明 屏幕阅读器可读, 可以继续交互

2. visibility: hidden

.hide {
  visibility: hidden;
}      

屏幕阅读器不可读.

重新成visible后就可以重新进行交互了 后续的子节点可以设置为visible而变得可见

3. display: none

.hide {
  display: none;
}

该元素不会被渲染, 所有后续的子节点也都是不会显示的 只可以通过dom api进行操作 屏幕阅读器不可读.

4. move out the viewport

.hide {
   position: absolute;
   top: -9999px;
   left: -9999px;
}

该方法的目的是想保留交互的功能,同时不要影响布局. 屏幕阅读器可读.

5. clip-path

.hide {
  clip-path: polygon(0px 0px,0px 0px,0px 0px,0px 0px);
}

通过裁剪的方法来隐藏.之前大家会用clip属性,现在clip属性已经废弃了,可以使用新的clip-path属性 想深入了解的可以看: 介绍clip-path属性

不过现在IE对clip-path还不支持

总结

用一张表来总结各种方法的区别:

方法 Occupy its position User interaction Screen Reader Transition Animation IE Support
opacity: 0 yes yes yes yes yes
visibility: hidden yes no no no yes
display: none no no no no yes
move out the viewport no yes yes yes yes
clip-path yes no yes yes no

Make Parallel Test Fast

顺利通过Code Test的Checklist

最近在找新的工作,澳洲这里几乎所有招Ruby工程师的公司都会要求你先做一个Code Test。就是给你一个需求,用代码实现。 Code Test通过后,才会进入面对面的面试,这其中还会有Pair Programming,所以一定要自己好好实践。下面是我总结的一些经验:

需求

  • 要很好的理解需求,不要忽略一些细节
  • 不要忘记一些非功能性的需求(比如要提供github地址,或者发送zip包)

面向对象设计

  • 要符合一些基本的OOD的原则, 比如Single Responsibility Principle, Open Close Principle
  • 充分利用语言的特性,设计好类,接口以及抽象和继承的层次
  • 使用常用的设计模式:Factory PatternTemplate PatternObserver PatternCommand PatternNull Ojbect
  • 设想一两个需求变更的场景,验证自己的代码能够很容易的扩展

系统和架构的设计

  • 尽量采用MVC分层架构模式,有Data ModelBusiness ModelControllerDisplay/Runner Class

异常的处理

  • 自定义异常,然后做好异常的处理
  • 提供友好的错误的提示

Test:

  • 尽可能采用TDD,有清楚的commits,体现代码的更新过程
  • 每个测试尽量保证一个assertion
  • 保证足够高的测试覆盖率,100%是可以有的,可以使用simplecov来帮助
  • 要有集成测试,有正常的流程测试和非正常的流程测试
  • 一定要测试好各种edge cases
  • 提供易用的rake tasks,方便运行相关的测试,比如单元测试,集成测试等

良好的接口设计

  • 非常容易安装和执行,减少需要的依赖(比如尽量少的gems,Ruby版本)
  • 有很好的命令行接口和帮助说明
  • 要有一份README,说明如何安装和使用

注意代码质量和规范:

  • 按照产品级别的代码要求自己
  • 采用Rubocop进行质量检查
  • 不要遗留任何的TODO
  • 方法尽可能短小,不要超过5行

最后祝所有找工作的朋友好运!

通过Spree开源代码学Ruby和Rails知识

最近在做Spree的定制开发,翻看其中的代码,发现其中有下面这么一段代码信息量是非常的大。所以就想通过这一小段代码来好好学习一下相关的Ruby知识。

# spree/api/lib/spree/api/engine.rb

def self.activate
  Dir.glob(File.join(File.dirname(__FILE__), "../../../app/**/*_decorator*.rb")) do |c|
    Rails.configuration.cache_classes ? require(c) : load(c)
  end
end

config.to_prepare &method(:activate).to_proc

就是这样的一段代码却包含了大量的Ruby和Rails的知识。其中最后一行config.to_prepare &method(:activate).to_proc 是本文重点解释的对象,接下来我们就一个一个来学习。

self.active

self.activate方法其实不难理解,这是一个类方法,会在引入Spree的应用中查找app目录下面文件名中带有_decorator的文件,这些文件都是用来定制扩展Spree功能的文件。在这里,找到后会判断是否设置了Rails.configuration.cache_classes这个选项,一般在开发环境中,该选项是false,这样当你修改代码后Rails会自动reload相关的文件。而在产品环境中,该选项为true,因为我们不会在产品环境修改代码让Rails自动reload。

后面requireload的最重要的区别就是,require如果发现该文件已经加载过后就不会重新载入,而load不管之前是否已经载入都会加载该文件。

重点来了,让我们看看下面这行信息量巨大的代码吧。一眼就看明白的同学,请自行绕过!

config.to_prepare &method(:activate).to_proc

method(:activate)

method方法来自于Object,会在当前的实例(上面的例子就是self)上查找通过参数指定的方法。找到后返回一个Method的实例,这个Method的对象就是一个封装了方法所属的对象以及该对象的实例变量的闭包。

method(:activate).to_proc

接下来就是调用Method对象的to_proc方法,产生一个对应的Proc对象。

&method(:activate).to_proc

Proc对象前面加上&符号,作为参数传递,相当于给方法传递了一个block,然后方法内部通过yield调用block,例如:

def to_prepare
  yield if block_given?
  puts 'Done prepare!'
end

相反,如果在接收的block参数前面加上&符号,那就相当于给方法传递了一个Proc的对象,然后方法内部通过proc.call来调用

def config(&block)
  block.call
  puts 'done config!'
end

config do
  puts 'I am block!'
end

config.to_prepare

这是来自Rails配置里面功能, 该配置是全局性的,会在所有的initializers运行之后运行。很重要的一点是,该配置中的代码,在production和test环境中默认只会执行一次,而在开发环境dev中,会在每次修改文件,重新发出请求时,Rails完成reload,在实际进行请求处理之前执行这段代码。(好绕的流程)

所以,你会看到上面的代码就是来重新加载一些定制Spree功能的代码。

在Rails中你还可以手动调用ActionDispatch::Reloader.to_prepare来实现同样地功能。

另外还有一个与之对应的ActionDispatch::Reloader.to_cleanup,区别是该callback会在请求处理完成后执行。

上面提到开发环境的不同,主要是受到config.cache_classes配置的控制。开发环境dev下,该属性为false,所以Rails会发现有文件修改后,自动realod。

实例

介绍完这些基本的知识点后,你可能会觉得还是有点糊涂,为什么Spree中要这么做呢?

这种做法其实在Ruby的世界是一种常见的用法,主要就是利用闭包的特点,把相关的逻辑更好的组织和封装。关于这一点上面Spree的例子不是很完美。

下面有给出一段利用闭包中封装的实例变量的例子。

class MethodTest
  class << self
    attr_accessor :root_path

    def activate
      puts @root_path
      puts helper
    end

    def helper
      "I am helper method. #{@root_path}"
    end
  end
end


def to_prepare
  yield if block_given?
  puts 'Done prepare!'
end

MethodTest.root_path = '/root'
to_prepare &MethodTest.method(:activate).to_proc