mcpField
R
rails-best-practices-core

rails-best-practices-core

github.com/marckohlbrugge开发73055 次安装20 周前更新
代码仓库

关于 rails-best-practices-core

为日常 Rails 开发提供基线实践的技能,内容涵盖建模、路由、授权、依赖选择与代码评审。它强调 Rails 内置约定、清晰代码和数据安全。

主要功能

  1. 01优先采用 Rails 约定与内置功能
  2. 02将领域行为放入模型
  3. 03以记录表达状态与行为
  4. 04通过数据库约束保障不变量
  5. 05涵盖 REST 路由与授权模式
  6. 06提供代码评审检查项

适用场景

  1. 01为 Rails 功能变更确定实现模式
  2. 02设计领域模型与资源路由
  3. 03检查数据约束和多租户边界
  4. 04评估新增依赖是否必要

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/marckohlbrugge/37signals-skills --skill rails-best-practices-core

安装后在 Claude Code 中输入/rails-best-practices-core确认已加载。

详细介绍

Rails 最佳实践核心

将此用作 Rails 开发的默认基线。内容提炼自 37signals 的代码库(Campfire、Fizzy)以及 DHH 的代码审查模式。

核心默认原则

  • 优先使用清晰、明确的代码,而非巧妙的抽象。抽象必须证明其价值;如果找不到至少 3 种需要它的变体,就直接内联。
  • 保持控制器精简,将领域行为放在模型中。
  • 添加 gem 之前,优先采用 Rails 约定和内置功能。
  • 使用领域概念来建模状态和行为,而非临时标志。
  • 通过所有权边界限定租户/用户数据的范围。
  • 对硬性不变量优先使用数据库约束;只有需要向用户显示错误消息时,才在 AR 中验证。
  • 保持接口精简;不要添加任何地方都未使用的公共方法。
  • 优先在写入时计算,而非在读取时进行高开销组合(计数缓存、委托类型、预计算汇总、不需要回调时使用 dependent: :delete_all)。
  • 在现代 Rails 中,使用 params.expect(...) 处理强参数。
  • 让错误抛出:使用 bang 方法(create!),在边界处处理异常。只有存在对应的非 bang 方法时,才使用 !。
  • 修复根本原因,而非表面症状(例如,针对竞态使用 enqueue_after_transaction_commit,而非重试逻辑)。
  • 与行为变更在同一个 PR 中提交测试。

建模模式

  • 将状态建模为记录,而非布尔值。 不要使用 closed: boolean,而是创建一个包含 creator 和时间戳的 Closure 记录。这样无需额外工作就能记录由谁在何时执行操作,而且限定范围也很简单:
has_one :closure, dependent: :destroy
scope :closed, -> { joins(:closure) }
scope :open, -> { where.missing(:closure) }
  • 将大型模型拆分为按能力命名的 concern(Closeable、Watchable、Assignable),每个 concern 都自成一体(包含关联、作用域和方法),约 50-150 行,且职责内聚。领域切片优先使用模型命名空间下的嵌套模块(在 app/models/card/closeable.rb 中定义 Card::Closeable);app/models/concerns/ 仅用于真正跨模型的行为。绝不要提取只包含私有方法的 concern。
  • PORO 放在 app/models/,而非 app/services/:展示对象(Event::Description)、复杂操作(SystemCommenter)、视图上下文集合(User::Filtering)。它们与模型相关,而非与控制器相关。
  • 通过 lambda 设置默认值: belongs_to :creator, class_name: "User", default: -> { Current.user };belongs_to :account, default: -> { board.account }。
  • 使用 Current 属性保存请求上下文(Current.user、Current.account),并设置级联赋值器(赋值 session 会解析出 identity,再由此解析出该账户的 user)。
  • 回调用于设置和清理,而非业务逻辑。 尽量减少回调数量。
  • 优先考虑的 Rails 简便功能: normalizes(验证前清理数据)、store_accessor(JSON 列)、delegated_type(异构集合)、generates_token_for(有过期时间的签名令牌)、通过 enum :status, %w[drafted published].index_by(&:itself) 定义字符串枚举、after_save_commit、用于缓存失效的 touch: true 链、delegate。
  • 使用关联扩展执行批量领域操作: 在 has_many 代理上定义 grant_to/revise;批量创建时使用 insert_all,并在无需回调的连接表上使用 dependent: :delete_all。
  • 易读的 URL: 用每个租户内的 number 重写 to_param,而非公开原始 ID/UUID。

命名

  • 花时间斟酌名称——命名就是设计。Closure 胜过 CardClose;Mention 胜过 UserReference。
  • 使用正向名称:用 active,而非 not_deleted;用 visible,而非 not_hidden。
  • 关联名称应体现语义角色:使用 belongs_to :creator, class_name: "User",而非 belongs_to :user。
  • 优先使用领域术语,而非技术术语:用 quota.depleted?,而非 quota.over_limit?。
  • 作用域名称应面向业务:使用 :active、:unassigned、:golden,而非类似 SQL 的 :without_pop。
  • 保持领域术语一致:不要用 source/resource/container 指代同一个概念。

REST 与路由

  • 一切皆为 CRUD:将动词转为名词。关闭 → resource :closure(POST 关闭,DELETE 重新打开);发布 → resource :publication。不要自定义成员操作。
  • 对每个父级仅有一个的状态使用单数 resource;使用 scope module: 对嵌套控制器分组(Cards::ClosuresController);对于深层级结构使用浅层嵌套。
  • 在嵌套控制器之间共享资源作用域控制器关注点(CardScoped 通过 Current.user.accessible_cards.find_by!(...) 设置 @card),包括共享的 Turbo 渲染辅助方法。
  • 使用 resolve "Comment",将多态 URL 生成为指向父级并带锚点的 URL。
  • 通过 respond_to 让同一控制器处理 HTML/Turbo/JSON,而不是单独设置 API 命名空间。

授权

  • 不使用 Pundit/CanCanCan:在模型上使用简单的谓词方法(card.editable_by?(user)、user.can_administer_board?(board))。
  • 控制器进行检查(head :forbidden unless ...),模型定义权限的含义。
  • 使用声明式控制器宏设置授权策略:allow_unauthenticated_access、ensure_can_administer。

依赖项

添加 gem 之前先问:原生 Rails 能做到吗?在项目内写 50-150 行代码,是否比添加依赖更简单?通常不使用:Devise、Pundit、ViewComponent、RSpec、FactoryBot、Redis(Solid Queue/Cache/Cable 使用数据库)、服务对象、表单对象、装饰器、GraphQL、SPA 框架、Tailwind。

审查优先级

  1. 正确性与数据安全。
  2. 多租户/安全边界。
  3. 可维护性与可读性。
  4. 性能热点。
  5. 风格与润色。

务必指出

  • 面向租户的流程中未限定作用域的记录查询(Comment.find(params[:id]))。
  • 缺乏充分理由的新依赖项。
  • 应在 SQL 中进行、却在内存中完成的筛选/排序(以及明明可以使用 .pluck(:name) 却使用 .map(&:name) 的情况)。
  • 用服务对象替代简单直接的模型方法。
  • 资源建模更清晰时,却使用了非 RESTful 的自定义操作。
  • 可以通过记录捕获操作人/时间,却使用了布尔状态列。
  • 使用 HTTP 缓存(fresh_when/etag)的表单页面——过期的 CSRF 令牌会导致 422 错误。
  • 有谓词式 API 可用(StringInquirer / 字符串枚举)时,却使用字符串状态检查(status == "x")。
  • 使用 validates :x, uniqueness: true,却没有对应的唯一索引。
  • 辅助方法依赖隐式实例变量,而不是显式参数。
  • 将未转义的插值内容放入 html_safe 字符串——先转义:"<b>#{h(input)}</b>".html_safe。
  • 只针对 2-3 种情况使用元编程——直接编写方法即可。
  • 仅在私有范围内使用的关注点——将其内联。

审查输出

  • 从严重程度最高的问题开始。
  • 对每个问题说明:问题、影响、具体修复方式,并附上文件:行号引用。
  • 直接务实;“这是过度设计。”本身就是完整的句子。
  • 结尾使用 Ship it 或简短的优先级修复清单。

评论

支持 Markdown