Rails 使用 Rails Generator 提高公司的开发效率

xiaoronglv for Workstream · 2020年08月22日 · 最后由 Qwaz1314 回复于 2020年11月07日 · 6240 次阅读
本帖已被管理员设置为精华贴

Rails 有非常多的约定,比如当我用这行命令创建一个资源,框架会帮我做很多决策,也会帮我创建相应的代码文件。

rails generate resource post title:string description:text

  • 数据库的表的命名为复数 posts
  • 主键是 id
  • model 放在 app/models/post.rb
  • controller 放在 app/controllers/posts_controller.rb

公共的约定降低了初学者的门槛,一个工程师并不需要懂太多计算机知识,也不需要花费太多时间去配置框架的方方面面,就可以搭建出一个像样的网站。

问题

在现实世界中,很多公司还会在 Rails 框架之上再抽象出一层业务框架。新人要开展工作,不但要精通技术,还要把一堆模型的逻辑关系理清楚,即使是有经验的工程师也会怀疑人生:“我是来写代码的,还是来读历史的?”

拿我公司的一个子产品入职流程来举例,这个产品帮助 HR 管理入职流程。HR 可以像搭积木一样搭建各种入职流程,“产品经理入职流程”,“程序员入职流程”,“设计师入职流程”等等。

  • 收集员工的基本信息。姓名,性别,紧急联系人,家庭住址等个人信息。
  • W4 模块。美国的税务文档,员工填写税务相关的信息给雇主,方便雇主帮自己缴税。
  • ADP 集成模块。我们把员工的个人信息,推送给雇主的其他人力资源软件。
  • WOTC 集成模块。我们把员工的个人信息,推送给政府,判断是否可以减税。
  • 工资单模块。把员工信息推送给雇主正在使用的工资单应用,比如 Paychex。(在美国,各个行业非常细分,甚至有单独的企业专门做工资单。)

当有新员工入职时,HR 选择一个合适的流程让他们入职,员工一步步完成任务,就可以顺利的入职了。

我的主要的工作就是开发一个个的模块(积木)。我自认为整个业务代码的设计还是比较符合 Open–closed 的原则。添加一个模块几乎不需要修改老代码,主要就是创建子类 (Concrete class),增加和某个模块相关的特殊 API,测试,locale 翻译文件,配置文件等等。

表面上不复杂,但因为涉及的业务模型太多,其他工程师没有一两个月根本插不上手,主要有这几个原因:

  • 我自认为设计非常直白,其实晦涩难懂。只不过因为是我写的,所以我知道上下文。对于别人则是天书。
  • 文档质量不好,或很快过期。
  • 大家撸起袖子就干,很少读文档,都是遇到问题再去翻文档。

如果有些工作是重复的,有套路的,有什么办法让降低业务开发的门槛,提高开发效率?

很多人首先想到的办法是写文档,事无巨细都写到文档里。让后人照着做就可以了。

最人性的办法是安排一个 mentor,手把手告诉新人:”Hi,虽然有 30 个表,但是你需要加的只有这三个文件。这个,这个,还有那个。。。“

最不近人情的办法,写非常完备的测试。如果别人不按规矩提交代码,就让集成测试 (CI) 直接挂掉。这加剧了别人的痛苦,但是没提高别人的工作效率。

今天我想介绍的是另外一种办法 Rails generator。

什么是 Rails Generator?

我们每天的日常工作都会用到它。

# 创建一个 rails 项目
rails new blog

# 创建一个 db migration 文件
rails generate migration AddNameToUser name:string

# 创建一个 model
rails generate model post title:string description:text

# 创建一个 resource 对应的全套文件,model,view,路由。
rails generate resource product slug:string name:string

简单来讲,一个 generator 内部定义了模版文件,当我们运行时,它会基于模版文件生成代码文件,然后复制到目标目录中。

下面我给大家介绍制作一个简单 generator 的过程。这个 generator 的用途是,当工程师执行 rails generate onboarding_module paychex 时:

  • 自动创建好几个子类
  • 自动创建这几个子类的 rspec 测试文件
  • 自动创建 FactoryBot 的文件
  • 创建对应的 mapper,做数据转换
  • 渲染一段配置,添加到某个 yml 的尾部
  • 在某个文件的某一行代码里,加一个常数。

方法

第一步,创建一个 Generator

Rails 有一个专门的 generator 来创建 generator。运行这个命令来创建我自己的 onbaording_module 生成器。

bin/rails generate generator onboarding_module

它会帮我创建以下目录和文件

# 生成器的主要逻辑放在这个文件里
create  lib/generators/onboarding_module/onboarding_module_generator.rb
# 帮助文档
create  lib/generators/onboarding_module/USAGE
# 模版文件
create  lib/generators/onboarding_module/templates

第二步,在 template 目录下,创建模版文件。

└── onboarding_module
    ├── USAGE
    ├── onboarding_module_generator.rb
    └── templates 👈👈👈 看这里,这些是我创建的模版。
        ├── factories
        │   ├── template_module.rb.erb
        │   └── employee_module.rb.erb
        ├── mapper.rb.erb
        ├── models
        │   ├── template_module.rb.erb
        │   └── employee_module.rb.erb
        ├── module.yml.erb
        └── spec
            ├── template_module_spec.rb.erb
            └── employee_module_spec.rb.erb

这些模版都是 erb 文件,内部嵌有实例变量,if/else,我可以动态的生成各种代码文件。

# templates/factories/template_module.rb.erb
FactoryBot.define do
  factory :company_<%= @module_name -%>_module, class: Onboarding::<%= @module_name.camelize -%>Module do
    company_onboarding_process { nil }
    deleted_at { nil }
    module_type { '<%= @module_name %>' }
    name { "TODO: Write this module's name" }
    sequence(:sequence) { |n| n - 1 }
    config { }

    trait :with_dependencies do
      after(:create) do |mod|
        <%- @actions.each do |action| -%>
        create(:company_onboarding_action, :<%= action -%>, company_onboarding_module: mod)
        <%- end -%>
        mod.reload
      end
    end # end of trait

  end # end of factory
end # end of top

第三步,生成代码。

Rails generator 提供了很多接口来操作文件,我们可以通过调用不同接口来加工文件。

一, template 方法。它可以渲染 erb 模版,生成对应的代码文件,并放到目标位置。我用这种方式生成 model, factories, rspec 测试。

template(
  'models/company_module.rb.erb', 
  'app/models/#{file_name}_module.rb'
  )
end

二, inject_into_file ,在文件的某个位置插入一段代码。

在目标文件 foo.rb 做作一个标记

ModuleList = [
  # 我的标记
  'w4_module',
  'paychex_module',
]

在这个标记的后面插入目标文件

inject_into_file 'app/models/foo.rb', after: "# 我的标记" do
  'new_module',
end

执行后的效果

ModuleList = [
  # 我的标记
  'new_module'  # 👈 看这里,这一行是新添加的
  'w4_module',
  'paychex_module',
]

Rails generator 提供的接口非常多,可以添加 gem, 替换文件内容,在这里就不一一涵盖了。你可以在 官方文档 查看。

效果

一个不熟悉业务的工程师开展工作,只需要运行 rails generate onboarding_module paychex 就可以搭好业务架子,然后填上自己的业务代码就搞定了。

这是效果图

这就是 Rails 对新手也很友好的一个表现了。

jasl 将本帖设为了精华贴。 08月24日 00:29

楼主这种可能是对业务级别代码抽象之后,比较方便

其实公司流程指定的好的话,这种挺方便的

其实这个也是编程界的传统手艺了,在很多年前 ORM 不是很成熟的时候,

大家上三层架构,很多语言都会有所谓的 代码生成器的存在

另外其实 rails 周边也有一些对常见流程的代码的整合,

比如 gorails 小胖子 弄的 https://www.railsbytes.com 我就特别喜欢

自从用了 JHipster 之后,觉得他的 jdl 描述方式方便对整个系统做整体设计,不用一个个模型去生成,生成出来的模式自带关联关系,可以通过使用 jhipster-core 来实现 Rails 版本的代码生成。

zhenyuanliu 回复

zhenyuanliu:最后开个玩笑,“一个工程师并不需要懂太多计算机知识”,你们公司会招这样的工程师吗?

我们刚好在招聘,这是我们公司的岗位要求,有兴趣可以看看哈。

https://ruby-china.org/topics/40332

😏 楼主发帖的目的性很强啊

lyb124553153 回复

天地良心,我的老板前天晚上(周一)大半夜才告诉我要招聘的。而这篇文章写于上周六。此外,我的帖子里一开始压根就没有放招聘链接,只不过刚好有人问,我就顺便回复一下。

前一段时间写的都是乱七八糟的东西,和技术没关系所以不好意思贴到 Ruby-China。这是我的文章流水:https://mednoter.com/

xiaoronglv 回复

添加一个模块几乎不需要修改老代码,主要就是创建子类 (Concrete class) 对这块实现比较好奇?具体的思路求分享。

mingyuan0715 回复

类似这个开源项目的设计

  1. 先定义一个父类
  2. 子类使用了单表继承(STI)
  3. 每次做集成第三方系统,就加一个子类。

https://github.com/huginn/huginn/tree/master/app/models/agents

xiaoronglv 回复

单表继承,我认为还不够体现 ruby 的灵活性:

  • 在 build model 的时候,新手如果对其机制理解的不够,容易出现误用;
  • 子类在方法继承等层面不灵活,因此 ruby 采纳了 module 方案,这是 ruby 的亮点;

所以我在 engine 中,全面拥抱了 module 方案:

class AcmeAccount < ApplicationRecord
  include RailsCom::AcmeAccount
end unless defined? AcmeAccount

特点:

  1. 更容易 override,engine 默认定义,如项目中存在同名定义 (override),engine 中则失效;
  2. 业务代码组织更灵活,以 user 模型为例:
class User < ApplicationRecord
  include RailsAuth::User
  include RailsOrg::User
  include RailsNotice::User
  include RailsTrade::Buyer
end

https://github.com/work-design/rails_com

xiaoronglv 回复

开个玩笑 放轻松

我们之前也是这么做的,好处是不用手敲,缺点是人慢慢的不愿意动脑子了,只停留在 works 共性的层面,对一些特例和优化的地方考虑的不够,慢慢的会累计很多无效代码,除非内部有很强的 SOP 或 review 机制,否则不建议作为团队共有工具,到是可以用来丰富个人工具箱。

前陣子才剛好想要熟練這一塊,開始在練習而已,這文章就出來了 .. XD

需要 登录 后方可回复, 如果你还没有账号请 注册新账号