simplifying-ruby-code

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Simplifying Ruby Code

简化 Ruby 代码

Prefer plain data (Hash, Array, Struct, Data) and pure functions over classes that add no state or behavior. Separate decisions from effects: a decision takes data and returns data; an effect does I/O (database, network, files, time). Keep them in separate methods or modules so decisions test without mocks.
When the user asks for a review, report findings and edit nothing unless they ask. Cite patterns by number ("Pattern 1") or by anti-pattern name.
优先使用普通数据(Hash、Array、Struct、Data)和纯函数,而不是那些未增加任何状态或行为的类。将决策与副作用分离:决策接收数据并返回数据;副作用执行 I/O(数据库、网络、文件、时间)。将二者放在不同的方法或 module 中,使决策无需 mocks 即可测试。
当用户要求审查时,只报告发现的问题,除非用户明确要求,否则不要编辑代码。通过编号(“Pattern 1”)或反模式名称引用相关模式。

Patterns

模式

  1. Command objects to module functions. A class whose only public method is
    call
    ,
    perform
    ,
    run
    , or
    execute
    , holding nothing beyond its constructor arguments, becomes a direct call or a module function (
    UserCreator.new(params).call
    becomes
    User.create(params)
    ). Keep the object when it holds state across calls, runs a multi-step algorithm worth naming, or must be serialized, as a background job is.
  2. Value objects to Data, Struct, or Hash. A class that only stores attributes, with hand-written
    initialize
    , readers, or
    ==
    and no behavior or validation:
    UseWhen
    HashTransient data, varying keys, JSON in or out
    StructFixed attributes, mutation acceptable
    DataFixed attributes, immutable (check
    ruby -v
    >= 3.2)
    Custom classValidation, invariants, or real domain behavior
  3. Class-method-only classes to modules. A class never instantiated, defining only
    self.
    methods, becomes a module with
    module_function
    .
  4. Deep inheritance to composition. More than 2 levels counting only the project's own classes (framework and stdlib bases such as
    ApplicationRecord
    or
    StandardError
    do not count), or an abstract base with a single subclass.
  5. Missing Ruby protocols. Callers unpack a collection-like or value-like class by hand instead of it implementing
    each
    with
    Enumerable
    ,
    to_h
    ,
    <=>
    with
    Comparable
    , or
    hash
    and
    eql?
    .
  6. Mixed decisions and effects. Tests need heavy mocks, or a method computes and writes in one body. Extract the computation into a pure function and leave a thin method for the I/O.
  1. Command object 转换为 module function。 如果一个类唯一的公开方法是
    call
    、
    perform
    、
    run
    或
    execute
    ,且除构造参数外不保存任何内容,则应改为直接调用或 module function(
    UserCreator.new(params).call
    变为
    User.create(params)
    )。当对象需要在多次调用之间保存状态、运行值得命名且包含多个步骤的算法,或必须像 background job 一样支持序列化时,应保留该对象。
  2. Value object 转换为 Data、Struct 或 Hash。 对于只存储属性、没有行为或验证,并手写
    initialize
    、readers 或
    ==
    的类:
    使用方式适用场景
    Hash临时数据、键可变、输入或输出 JSON
    Struct固定属性,允许变更
    Data固定属性,不可变(检查
    ruby -v
    >= 3.2)
    Custom class验证、不变量或真正的领域行为
  3. 仅含类方法的类转换为 module。 从不实例化且只定义
    self.
    方法的类,改为使用带有
    module_function
    的 module。
  4. 深层继承转换为组合。 继承层级超过 2 层时,仅计算项目自身的类;framework 和 stdlib 基类(如
    ApplicationRecord
    或
    StandardError
    )不计入。抽象基类仅有一个子类时也应进行转换。
  5. 缺少 Ruby protocol。 调用方手动解包 collection-like 或 value-like class,而该类没有通过实现
    each
    并包含
    Enumerable
    、实现
    to_h
    、通过
    <=>
    并包含
    Comparable
    ,或实现
    hash
    和
    eql?
    来支持相应 protocol。
  6. 决策与副作用混杂。 测试需要大量 mocks,或一个方法在同一函数体中同时执行计算和写入时,应提取计算逻辑并放入纯函数,同时保留一个执行 I/O 的精简方法。

Rails anti-patterns

Rails 反模式

  • Service objects: a single-method, stateless service belongs in a model method or module function (Pattern 1). Keep complex orchestration as a service but separate decisions from effects (Pattern 6). Jobs are legitimate objects because they serialize.
  • Models: keep ActiveRecord models as classes; move business calculations into pure functions.
  • Concerns: a concern shares behavior across models; flag one used as a dumping ground for a single model.
  • Helpers: view formatting only; business rules go in a module.
  • Reinvented Rails: custom DSLs or base classes that duplicate scopes, validations, callbacks, enums, or
    ActiveModel
    .
  • Service objects: 单方法、无状态的 service 应放入 model method 或 module function(Pattern 1)。复杂的 orchestration 可以保留为 service,但应将决策与副作用分离(Pattern 6)。Job 是合理的对象,因为它们需要序列化。
  • Models: ActiveRecord model 应保留为类;将业务计算移入纯函数。
  • Concerns: Concern 用于在多个 model 之间共享行为;如果某个 concern 只是某个 model 的“垃圾场”,应将其标记出来。
  • Helpers: 仅用于视图格式化;业务规则应放入 module。
  • Reinvented Rails: 自定义 DSL 或基类重复实现了 scopes、validations、callbacks、enums 或
    ActiveModel
    的功能。

Plain Ruby anti-patterns

普通 Ruby 反模式

  • Template methods: a base class with hook methods where a block would do.
  • Builders: for objects that keyword arguments or
    Data.define
    build directly.
  • Wrappers: classes that wrap Array, Hash, Set, or another stdlib type and forward most calls.
  • Metaprogramming:
    method_missing
    ,
    define_method
    , or
    instance_eval
    DSLs where plain methods work.
  • Dependencies: a gem pulled in for a few lines of stdlib code.
In a gem, every public constant is API that other projects may call.
  • Template methods: 基类包含 hook methods,而使用 block 本可完成相同目的。
  • Builders: 为可直接通过 keyword arguments 或
    Data.define
    构建的对象创建 builder。
  • Wrappers: 类包装 Array、Hash、Set 或其他 stdlib 类型,并转发其中大多数调用。
  • Metaprogramming: 使用
    method_missing
    、
    define_method
    或
    instance_eval
    编写 DSL,而普通方法即可完成。
  • Dependencies: 仅为几行 stdlib 代码引入一个 gem。
在 gem 中,每个 public constant 都是其他项目可能调用的 API。

Before removing or inlining a class

删除或内联类之前

  • Run
    bash <skill-dir>/scripts/find-callers.sh <Constant> <project>
    for each class.
  • Exit 0: account for every
    reference
    and
    require
    hit, and decide for each
    dynamic
    hit whether the name it builds can be the class; that takes reading the code, so it stays with you.
  • Exit 1: no caller found. Exit 2: fix the argument. Exit 3: install the named tool. Exit 4: report the grep error.
  • A controller is also reached from
    config/routes.rb
    by its snake_case resource name, which the script does not search; read that file.
  • In a review, drop or downgrade a finding whose callers you cannot account for.
  • 对每个类运行
    bash <skill-dir>/scripts/find-callers.sh <Constant> <project>
    。
  • Exit 0:处理每一条
    reference
    和
    require
    命中;对于每一条
    dynamic
    命中,判断其构建的名称是否可能指向该类。这需要阅读代码,因此必须由你亲自判断。
  • Exit 1:未找到调用方。Exit 2:修正参数。Exit 3:安装指定工具。Exit 4:报告 grep 错误。
  • Controller 还可以通过
    config/routes.rb
    中的 snake_case resource name 访问,而脚本不会搜索该文件;请读取该文件。
  • 在审查中,如果无法确认某个发现的全部调用方,应删除或降低该发现的严重程度。

Review findings

审查发现

Read review findings when reporting a review: the grouping, the fields of each finding, and the closing order.
报告审查结果时,阅读审查发现:其中说明了分组方式、每条发现的字段以及收尾顺序。

Refactoring

重构

Get a green test baseline first. Apply one pattern at a time and run the suite after each; on failure, fix or revert that step before continuing. Update tests that mocked the removed layer to call the pure function or the direct API instead.
先建立测试全绿基线。每次只应用一个模式,并在之后运行测试套件;如果失败,应先修复或回退该步骤,再继续。更新那些 mock 已删除层的测试,使其改为调用纯函数或直接 API。