Ruby Object Design Expert by ag0os/rails-dev-plugin
npx skills add https://github.com/ag0os/rails-dev-plugin --skill 'Ruby Object Design Expert'基于 Ruby 是面向对象的,而非面向类 的原则,指导您选择合适的 Ruby 构造。打破将 class 关键字作为默认起点的思维定式。
Ruby 开发者应该从 对象和消息 的角度思考,而不是类和继承。class 关键字应保留给特定用例,而不是作为代码的默认容器。
你需要具有封装状态的多个实例吗?
├── 是:对象是否同时具有状态和行为?
│ ├── 是 → 使用类
│ └── 否(仅数据)→ 使用 Struct 或 Data
└── 否:这是一组相关函数吗?
├── 是 → 使用带有 `extend self` 的模块
└── 否:这是一个一次性转换吗?
└── 是 → 使用独立方法或 lambda
仅当你在创建一个对象工厂时才使用 class——一个专门设计用于生成多个对象的模板,这些对象封装了内部状态,并具有操作该状态的行为。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
# 良好:具有状态和行为的对象工厂
class Policy
def initialize(holder:, coverage:, premium:)
@holder = holder
@coverage = coverage
@premium = premium
end
def active?
@coverage.end_date > Date.today
end
def renew(new_end_date)
@coverage = @coverage.extend_to(new_end_date)
end
end
详细场景请参阅 class-vs-module.md。
| 场景 | 使用 | 原因 |
|---|---|---|
| 具有状态 + 行为的多个实例 | 类 | 真正的对象工厂 |
| 无状态的工具方法 | 带有 extend self 的模块 | 没有状态需要封装 |
| 简单的数据容器 | Struct 或 Data | 避免样板代码 |
| 不可变的值对象 | Data (Ruby 3.2+) 或冻结的 Struct | 内置不可变性 |
| 临时/临时数据分组 | Hash | 最简单的解决方案 |
| 以设计模式命名 | 重新考虑设计 | 模式在 Ruby 中通常不必要 |
在没有设置的情况下 .new 后无效 | 不是一个类 | 对象在创建时就应该有效 |
如果你的类没有实例变量、没有有意义的实例方法、也没有构造函数逻辑,那么它就是一个伪装成类的模块。
# 不好:没有状态的类
class StringUtils
def self.titleize(string)
string.split.map(&:capitalize).join(' ')
end
def self.truncate(string, length)
string[0...length]
end
end
# 良好:带有 extend self 的模块
module StringUtils
extend self
def titleize(string)
string.split.map(&:capitalize).join(' ')
end
def truncate(string, length)
string[0...length]
end
end
只有 call 或 perform 方法的类通常只是伪装成类的函数。
# 值得商榷:这真的是一个对象工厂吗?
class CalculateDiscount
def initialize(order)
@order = order
end
def call
@order.subtotal * discount_rate
end
private
def discount_rate
@order.customer.premium? ? 0.1 : 0.05
end
end
# 替代方案:模块函数
module Discounts
extend self
def calculate(order)
order.subtotal * discount_rate(order.customer)
end
private
def discount_rate(customer)
customer.premium? ? 0.1 : 0.05
end
end
如果你的类命名为 Factory、Builder、Decorator、Adapter 或 AbstractBase,请重新考虑。大多数 GoF 模式是为了解决 C++ 限制的变通方法,在 Ruby 中通常不必要或已内置。
# 不好:为模式而模式
class UserFactory
def self.create(type)
case type
when :admin then AdminUser.new
when :guest then GuestUser.new
end
end
end
# 良好:Ruby 已经处理了这种情况
User.new(role: :admin)
# 或者
AdminUser.new
如果一个对象在能够正常工作之前需要调用 setter 方法,那么它就不应该作为一个类存在。
# 不好:.new 后处于无效状态
class Report
def initialize
@data = nil
end
def set_data(data) # 在 generate 之前必须调用此方法!
@data = data
end
def generate
raise "No data!" unless @data
# ...
end
end
# 良好:创建即有效
class Report
def initialize(data)
@data = data
end
def generate
# @data 保证存在
end
end
对于仅保存数据的“普通 Ruby 对象”,应避免使用类的样板代码。
Struct、Data 和 Hash 模式请参阅 data-structures.md。
使用模块来分组相关方法并提供命名空间组织。
module Insurance
module PremiumCalculations
extend self
def calculate_base(policy)
policy.coverage_amount * rate_for(policy.type)
end
def apply_discounts(base_premium, discounts)
discounts.reduce(base_premium) { |premium, discount| premium * (1 - discount) }
end
private
def rate_for(type)
RATES.fetch(type, DEFAULT_RATE)
end
end
end
# 用法
Insurance::PremiumCalculations.calculate_base(policy)
将功能移出类可以实现 方法级多态,使代码更具通用性。
# 替代基于类的多态
def process(item)
case item
when Policy then PolicyProcessor.new(item).process
when Claim then ClaimProcessor.new(item).process
end
end
# 考虑基于函数的方法
module Processors
extend self
def process_policy(policy)
# ...
end
def process_claim(claim)
# ...
end
end
首先在不使用类或方法的情况下在单个文件中编写代码。只有当复杂性变得难以管理时,才将其重构为方法或模块。
# 从这里开始
data = fetch_data
processed = data.map { |d| transform(d) }
result = aggregate(processed)
# 仅在需要时提取
module DataPipeline
extend self
def run(source)
data = fetch_data(source)
processed = transform_all(data)
aggregate(processed)
end
# ... 提取出的方法
end
在应用这些原则之前,请检查现有代码库:
grep -r "class.*Service" app/| 模式 | 检测方法 | 冲突 | 适用性 |
|---|---|---|---|
| 模块优于类 | 检查无状态类 | 无 - 始终适用 | 立即使用 |
| Struct/Data | 检查 .ruby-version 是否为 3.2+ | 旧版 Ruby 版本 | 立即使用 / 未来使用 |
| 避免以模式命名的类 | 搜索 Factory、Decorator | 已建立的约定 | 未来方向 |
| 创建即有效的对象 | 检查必需的 setter | 遗留代码 | 立即使用 |
提供对象设计建议时:
识别正在使用的构造以及它可能不理想的原因
建议合适的 Ruby 构造并说明理由
提供显示改进前后的代码
如何安全地从当前状态过渡到推荐状态
注明任何团队约定或 Ruby 版本要求
| 问题 | 答案 |
|---|---|
| 它有实例状态吗? | 无状态 = 模块 |
| 它有作用于该状态的行为吗? | 无行为 = Struct/Data |
| 它以模式命名吗? | 重新考虑设计 |
它在 .new 后立即有效吗? | 否 = 不是一个类 |
它只是一个 call 方法吗? | 考虑模块函数 |
记住:Ruby 是面向对象的,而非面向类。 思考对象和消息,而不是类和继承。
每周安装
0
仓库
GitHub 星标
4
首次出现
1970年1月1日
安全审计
Guidance for choosing the right Ruby construct based on the principle that Ruby is object-oriented, not class-oriented. Break the reflex of using the class keyword as your default starting point.
Ruby developers should think in terms of objects and messages , not classes and inheritance. The class keyword should be reserved for specific use cases, not used as a default container for code.
Do you need multiple instances with encapsulated state?
├── YES: Does the object have both state AND behavior?
│ ├── YES → Use a Class
│ └── NO (just data) → Use Struct or Data
└── NO: Is this a collection of related functions?
├── YES → Use a Module with `extend self`
└── NO: Is this a one-off transformation?
└── YES → Use a standalone method or lambda
Only useclass if you are creating an object factory - a template specifically designed to generate multiple objects that encapsulate internal state with behaviors that operate on that state.
# Good: Object factory with state and behavior
class Policy
def initialize(holder:, coverage:, premium:)
@holder = holder
@coverage = coverage
@premium = premium
end
def active?
@coverage.end_date > Date.today
end
def renew(new_end_date)
@coverage = @coverage.extend_to(new_end_date)
end
end
See class-vs-module.md for detailed scenarios.
| Scenario | Use | Why |
|---|---|---|
| Multiple instances with state + behavior | Class | True object factory |
| Stateless utility methods | Module with extend self | No state to encapsulate |
| Simple data container | Struct or Data | Avoids boilerplate |
| Immutable value object | Data (Ruby 3.2+) or frozen Struct | Built-in immutability |
| Ad-hoc/temporary data grouping | Hash | Simplest solution |
| Named after design pattern | Rethink design | Patterns often unnecessary in Ruby |
Invalid after .new without setup | Not a class | Objects should be valid at birth |
If your class has no instance variables, no meaningful instance methods, and no constructor logic, it is a module pretending to be a class.
# Bad: Class with no state
class StringUtils
def self.titleize(string)
string.split.map(&:capitalize).join(' ')
end
def self.truncate(string, length)
string[0...length]
end
end
# Good: Module with extend self
module StringUtils
extend self
def titleize(string)
string.split.map(&:capitalize).join(' ')
end
def truncate(string, length)
string[0...length]
end
end
Classes with only a call or perform method are often just functions in disguise.
# Questionable: Is this really an object factory?
class CalculateDiscount
def initialize(order)
@order = order
end
def call
@order.subtotal * discount_rate
end
private
def discount_rate
@order.customer.premium? ? 0.1 : 0.05
end
end
# Alternative: Module function
module Discounts
extend self
def calculate(order)
order.subtotal * discount_rate(order.customer)
end
private
def discount_rate(customer)
customer.premium? ? 0.1 : 0.05
end
end
If your class is named Factory, Builder, Decorator, Adapter, or AbstractBase, reconsider. Most GoF patterns were workarounds for C++ limitations and are often unnecessary or built into Ruby.
# Bad: Pattern for pattern's sake
class UserFactory
def self.create(type)
case type
when :admin then AdminUser.new
when :guest then GuestUser.new
end
end
end
# Good: Ruby already handles this
User.new(role: :admin)
# or
AdminUser.new
If an object requires calling setter methods before it can function, it should not exist as a class.
# Bad: Invalid state after .new
class Report
def initialize
@data = nil
end
def set_data(data) # Must call this before generate!
@data = data
end
def generate
raise "No data!" unless @data
# ...
end
end
# Good: Valid at birth
class Report
def initialize(data)
@data = data
end
def generate
# @data is guaranteed to exist
end
end
For "Plain Old Ruby Objects" (POROs) that just hold data, avoid class boilerplate.
See data-structures.md for Struct, Data, and Hash patterns.
Use modules to group related methods and provide namespace organization.
module Insurance
module PremiumCalculations
extend self
def calculate_base(policy)
policy.coverage_amount * rate_for(policy.type)
end
def apply_discounts(base_premium, discounts)
discounts.reduce(base_premium) { |premium, discount| premium * (1 - discount) }
end
private
def rate_for(type)
RATES.fetch(type, DEFAULT_RATE)
end
end
end
# Usage
Insurance::PremiumCalculations.calculate_base(policy)
Moving functionality out of classes enables method-level polymorphism , making code more general-purpose.
# Instead of class-based polymorphism
def process(item)
case item
when Policy then PolicyProcessor.new(item).process
when Claim then ClaimProcessor.new(item).process
end
end
# Consider function-based approach
module Processors
extend self
def process_policy(policy)
# ...
end
def process_claim(claim)
# ...
end
end
Begin by writing code in a single file without classes or methods. Only refactor into methods or modules once the complexity becomes uncomfortable.
# Start here
data = fetch_data
processed = data.map { |d| transform(d) }
result = aggregate(processed)
# Only extract when needed
module DataPipeline
extend self
def run(source)
data = fetch_data(source)
processed = transform_all(data)
aggregate(processed)
end
# ... extracted methods
end
Before applying these principles, check the existing codebase:
grep -r "class.*Service" app/| Pattern | Detection | Conflicts | Applicability |
|---|---|---|---|
| Module over Class | Check for stateless classes | None - always applicable | Use Now |
| Struct/Data | Check .ruby-version for 3.2+ | Older Ruby versions | Use Now / Future |
| Avoid pattern-named classes | Grep for Factory, Decorator | Established conventions | Future Direction |
| Objects valid at birth | Check for required setters | Legacy code | Use Now |
When providing object design recommendations:
Identify what construct is being used and why it may be suboptimal
Suggest the appropriate Ruby construct with rationale
Provide before/after code showing the improvement
How to safely transition from current to recommended state
Note any team conventions or Ruby version requirements
| Question | Answer |
|---|---|
| Does it have instance state? | No state = Module |
| Does it have behavior on that state? | No behavior = Struct/Data |
| Is it named after a pattern? | Reconsider the design |
Is it valid immediately after .new? | No = Not a class |
Is it just a call method? | Consider a module function |
Remember: Ruby is object-oriented, not class-oriented. Think objects and messages, not classes and inheritance.
Weekly Installs
0
Repository
GitHub Stars
4
First Seen
Jan 1, 1970
Security Audits
冲刺回顾模板:敏捷团队回顾会议指南与模板(开始-停止-继续/愤怒-悲伤-高兴/4Ls)
10,400 周安装