需求

有这么一个需求,

一个教学网站,上架的课程需要分成不同的类别,对不同的会员进行权限设置。

  • 免费课
  • VIP 课
  • 员工课

这是一个非常常规的需求,一般做法是对课程的 Model 增加一个 :category 的字段,再判定就可以了。

但是,这个项目有一些「历史包袱」。「员工课」是新需求,本来网站的设计只考虑了「免费课」和「VIP课」,只用了一个 :vip_required 的布尔值字段作为判断。

对于一个已经运营的网站来说,为了尽可能少地修改原数据库,又能满足新需求,我采取的办法是新增一个 :staff 的布尔值字段来判定。

于是问题就来了。

在编辑或者新建课程 course 的时候,表单设计上非常别扭。因为 :vip_requied:staff 都是布尔值,如果使用 simple_form 的时候,会自动采用 check_box 的组件。于是表单就会变成

  • 是否VIP课
  • 是否员工课

这里有两个问题。

  1. 这个两个选项实际上用互斥的,而 check_box 在使用习惯上代表着多选;
  2. 选项不全,还有一个「免费课」的选项无法呈现。

最符合使用习惯的体验应该是这样:

FABDE176-468C-4770-B5CC-CCE145384F48

问题就变成了,如何用两个布尔值的字段来构建这么一个 radio_buttons 表单?

解决方法

跟同事商量之后,采取的办法是构建虚拟属性

新建一个虚拟属性

models/course.rb 里建一个 virtual_role 的虚拟属性,包括读、写方法。

def	virtual_role
end

def virtual_role=
end

这个虚拟属性不需要写入数据库,仅仅在新建和编辑的时候作为中间变量来使用,需要满足的功能是

  • 读取 :virtual_role 的时候,返回正确的 :vip_requied:staff 值;
  • 写入 :virtual_role 的时候,写入正确的 :vip_requied:staff 值;

可以这么来设计 :virtual_role

:virtual_role :vip_requied :staff 课程类别
1 false false 免费课
2 false true VIP 课
3 true false 员工课
4 true true 无此种情况

于是,对于读取方法,可以这么写

def virtual_role
  if !staff && !vip_required
    return 1
  elsif !staff && vip_required
    return 2
  elsif staff && !vip_required
    return 3
  elsif staff && vip_required
    return 4
  end
end

对于写入方法,可以这么写

def virtual_role=(_new_vbirual_role)
  _new_vbirual_role = _new_vbirual_role.to_i

  if _new_vbirual_role == 1
    self[:vip_required] = false
    self[:staff] = false
  elsif _new_vbirual_role == 2
    self[:vip_required] = true
    self[:staff] = false
  elsif _new_vbirual_role == 3
    self[:vip_required] = false
    self[:staff] = true
  elsif _new_vbirual_role == 4
    self[:vip_required] = true
    self[:staff] = true
  end
end

再建一个表单使用的选项

def self.virtual_roles
  [
    [3, '员工培训课'],
    [1, '免费公开课'],
    [2, '对外售卖课']
    ]
end
# 因为第4种情况不是我们想要的,表单不应该可选。

表单设计

这样一来,就可以在表单里直接使用 :virtual_role 来做 radio_bottons 了。根据 simple_form 的文档,可以这么来写。

<%= f.collection_radio_buttons :virtual_role, Course.virtual_roles, :first, :last %>

这样就得到想要的效果了。

强参数

还有一件事不能忘了,就是修改 Controller 里的 permit_params ,把 :vip_required:staff 删掉,把 :virtual_role 加上。

总结

基本思路是这样的

  • 构建虚拟属性作为选项
  • 穷举所有选项
  • 读取虚拟属性时,返回相应字段的值
  • 写入虚拟属性时,写入相应字段的值
  • 构建表单
  • 强参数检查