第 22 章:最佳实践
第 22 章:最佳实践
“代码是写给人看的,偶尔让机器执行一下。” —— Harold Abelson
22.1 代码规范
22.1.1 命名约定
# 变量和方法:snake_case
user_name = "Alice"
def calculate_total; end
def valid?; end
def save!; end
# 类和模块:PascalCase
class UserAccount; end
module PaymentGateway; end
# 常量:SCREAMING_SNAKE_CASE
MAX_RETRIES = 3
DEFAULT_TIMEOUT = 30
API_BASE_URL = "https://api.example.com"
# 布尔方法:? 结尾
def active?; end
def valid?; end
def empty?; end
# 危险/修改方法:! 结尾
def save!; end
def reverse!; end
def gsub!; end
22.1.2 代码风格
# ✅ 使用单引号(无插值时)
name = 'Alice'
greeting = "Hello, #{name}!" # 有插值时用双引号
# ✅ 使用符号做标识符
status = :active
config = { host: 'localhost', port: 3000 }
# ✅ 省略不必要的括号
puts "hello"
puts("hello") # 也可以,但不必要
# ✅ 方法调用括号一致
user = User.new(name: 'Alice')
user.update(name: 'Bob')
# ✅ 使用 guard clause 减少嵌套
def process(data)
return if data.nil?
return [] if data.empty?
# 主逻辑
data.map(&:upcase)
end
# ❌ 不必要的嵌套
def process(data)
if data
if !data.empty?
data.map(&:upcase)
else
[]
end
end
end
22.1.3 方法设计
# ✅ 短小精悍的方法
class Order
def total
subtotal + tax - discount
end
private
def subtotal
items.sum { |item| item.price * item.quantity }
end
def tax
subtotal * tax_rate
end
def discount
eligible_for_discount? ? subtotal * 0.1 : 0
end
def tax_rate
0.08
end
def eligible_for_discount?
subtotal > 100
end
end
# ✅ 使用 Ruby 的表达能力
# 返回最后一个表达式的值
def classify(score)
case score
when 90..100 then :excellent
when 80...90 then :good
when 60...80 then :pass
else :fail
end
end
# ✅ 使用 Enumerable
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# 偶数的平方和
even_square_sum = numbers
.select(&:even?)
.map { |n| n ** 2 }
.sum
22.2 RuboCop
22.2.1 安装和配置
# Gemfile
group :development, :test do
gem 'rubocop', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'rubocop-performance', require: false
end
# .rubocop.yml
require:
- rubocop-rails
- rubocop-rspec
- rubocop-performance
AllCops:
TargetRubyVersion: 3.2
NewCops: enable
SuggestExtensions: false
Exclude:
- 'vendor/**/*'
- 'tmp/**/*'
- 'db/schema.rb'
- 'bin/**/*'
# 样式
Style/StringLiterals:
EnforcedStyle: single_quotes
Style/FrozenStringLiteralComment:
Enabled: true
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
# 布局
Layout/LineLength:
Max: 120
AllowedPatterns:
- '^\s*#' # 注释
Layout/MultilineMethodCallIndentation:
EnforcedStyle: indented
# 指标
Metrics/MethodLength:
Max: 20
CountAsOne:
- array
- hash
- heredoc
Metrics/AbcSize:
Max: 25
Metrics/ClassLength:
Max: 200
Metrics/BlockLength:
Exclude:
- 'spec/**/*'
- 'config/routes.rb'
# Rails
Rails/HasManyOrHasOneDependent:
Enabled: false
# Performance
Performance/DeletePrefix:
Enabled: true
Performance/StringInclude:
Enabled: true
22.2.2 常用命令
# 检查整个项目
rubocop
# 检查特定文件
rubocop app/models/user.rb
# 自动修复
rubocop -A # 自动修复所有(包括不安全的)
rubocop -a # 只修复安全的
# 生成配置文件
rubocop --init
# 生成 todo 文件(忽略现有问题)
rubocop --auto-gen-config
# 查看所有 cops
rubocop --show-cops
22.3 设计模式
22.3.1 策略模式
# 不同的折扣策略
module Pricing
class NoDiscount
def apply(total)
total
end
end
class PercentageDiscount
def initialize(percent)
@percent = percent
end
def apply(total)
total * (1 - @percent / 100.0)
end
end
class FlatDiscount
def initialize(amount)
@amount = amount
end
def apply(total)
[total - @amount, 0].max
end
end
end
class Order
attr_reader :items, :discount_strategy
def initialize(discount_strategy: Pricing::NoDiscount.new)
@items = []
@discount_strategy = discount_strategy
end
def total
raw_total = items.sum(&:price)
discount_strategy.apply(raw_total)
end
end
# 使用
order = Order.new(discount_strategy: Pricing::PercentageDiscount.new(10))
order = Order.new(discount_strategy: Pricing::FlatDiscount.new(50))
22.3.2 观察者模式
module Observable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def observers
@observers ||= []
end
def add_observer(observer)
observers << observer
end
def remove_observer(observer)
observers.delete(observer)
end
end
def notify_observers(event, data = {})
self.class.observers.each do |observer|
observer.update(event, data) if observer.respond_to?(:update)
end
end
end
class User
include Observable
def create
# 创建用户逻辑
notify_observers(:user_created, { user: self })
end
end
class EmailNotifier
def update(event, data)
puts "Sending email for #{event}" if event == :user_created
end
end
class ActivityLogger
def update(event, data)
puts "[LOG] #{event}: #{data}"
end
end
User.add_observer(EmailNotifier.new)
User.add_observer(ActivityLogger.new)
22.3.3 装饰器模式
class SimpleWriter
def initialize(path)
@file = File.open(path, 'w')
end
def write_line(line)
@file.puts(line)
end
def close
@file.close
end
end
module NumberingWriter
def write_line(line)
@line_number = (@line_number || 0) + 1
super("#{@line_number}: #{line}")
end
end
module TimestampingWriter
def write_line(line)
super("[#{Time.now}] #{line}")
end
end
# 使用(Ruby 的 prepend 实现装饰器)
class EnhancedWriter < SimpleWriter
prepend NumberingWriter
prepend TimestampingWriter
end
writer = EnhancedWriter.new('output.txt')
writer.write_line('Hello')
writer.write_line('World')
writer.close
# 文件内容:
# [2024-01-15 10:30:00] 1: Hello
# [2024-01-15 10:30:00] 2: World
22.3.4 服务对象模式
# 将复杂的业务逻辑封装到服务对象中
class CreateUserService
Result = Struct.new(:success?, :user, :errors, keyword_init: true)
def initialize(params)
@params = params
end
def call
validate_params
return failure(@errors) if @errors.any?
create_user
send_welcome_email
log_creation
success(@user)
rescue ActiveRecord::RecordInvalid => e
failure([e.message])
rescue => e
Rails.logger.error("CreateUserService failed: #{e.message}")
failure(['An unexpected error occurred'])
end
private
def validate_params
@errors = []
@errors << 'Name is required' if @params[:name].blank?
@errors << 'Email is required' if @params[:email].blank?
@errors << 'Invalid email format' unless valid_email?(@params[:email])
end
def create_user
@user = User.create!(@params)
end
def send_welcome_email
UserMailer.welcome(@user).deliver_later
end
def log_creation
Rails.logger.info("User created: #{@user.id}")
end
def valid_email?(email)
email.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
end
def success(user)
Result.new(success?: true, user: user, errors: [])
end
def failure(errors)
Result.new(success?: false, user: nil, errors: errors)
end
end
# 使用
result = CreateUserService.new(params).call
if result.success?
redirect_to result.user
else
flash[:error] = result.errors.join(', ')
render :new
end
22.3.5 查询对象模式
class UserQuery
def initialize(relation = User.all)
@relation = relation
end
def search(term)
return self if term.blank?
@relation = @relation.where(
'name LIKE :term OR email LIKE :term',
term: "%#{term}%"
)
self
end
def active
@relation = @relation.where(active: true)
self
end
def adults
@relation = @relation.where('age >= ?', 18)
self
end
def created_after(date)
@relation = @relation.where('created_at >= ?', date)
self
end
def order_by(field, direction = :asc)
@relation = @relation.order(field => direction)
self
end
def page(number, per_page = 25)
@relation = @relation.offset((number - 1) * per_page).limit(per_page)
self
end
def results
@relation
end
def count
@relation.count
end
end
# 使用
users = UserQuery.new
.search(params[:q])
.active
.adults
.created_after(30.days.ago)
.order_by(:created_at, :desc)
.page(params[:page])
.results
22.4 常见陷阱
22.4.1 浮点数精度
# ❌ 浮点数比较
0.1 + 0.2 == 0.3 # => false!
# ✅ 使用近似比较
(0.1 + 0.2 - 0.3).abs < Float::EPSILON # => true
# ✅ 或使用有理数
(1r/10 + 2r/10) == 3r/10 # => true
# ✅ 或使用 BigDecimal
require 'bigdecimal'
BigDecimal('0.1') + BigDecimal('0.2') == BigDecimal('0.3') # => true
22.4.2 可变默认值
# ❌ 危险:共享可变默认值
def bad_method(arr = [])
arr << 1
arr
end
bad_method # => [1]
bad_method # => [1, 1] ← 副作用!
# ✅ 正确:在方法内创建新数组
def good_method(arr = nil)
arr = arr.dup || []
arr << 1
arr
end
# ✅ 或使用冻结默认值
def safe_method(arr = [].freeze)
arr = arr.dup
arr << 1
arr
end
22.4.3 哈希键类型不一致
# ❌ 符号和字符串键混用
data = { 'name' => 'Alice', :age => 25 }
data['name'] # => 'Alice'
data[:name] # => nil
data[:age] # => 25
data['age'] # => nil
# ✅ 统一使用符号
data = { name: 'Alice', age: 25 }
# ✅ 或统一转换
data = { 'name' => 'Alice', 'age' => 25 }
data = data.transform_keys(&:to_sym)
22.4.4 空值处理
# ❌ 不安全的链式调用
user.address.city # => NoMethodError if address is nil
# ✅ 安全导航操作符(Ruby 2.3+)
user&.address&.city # => nil
# ✅ 或使用 dig
data.dig(:user, :address, :city) # => nil(安全)
# ❌ 不要用 == nil
if value == nil; end
# ✅ 使用 nil?
if value.nil?; end
# ❌ 不要用 !! 强制转布尔
def active?
!!@active
end
# ✅ 直接返回(Ruby 中任何非 nil/false 都是真值)
def active?
@active
end
22.4.5 循环引用和内存泄漏
# ❌ 注意闭包捕获
def create_closures
arr = []
1000.times do |i|
large_data = 'x' * 1_000_000 # 大字符串
arr << -> { "#{i}: #{large_data.length}" }
end
arr # large_data 被闭包捕获,不会被 GC
end
# ✅ 只捕获需要的数据
def create_closures
arr = []
1000.times do |i|
large_data = 'x' * 1_000_000
length = large_data.length # 只保存需要的值
arr << -> { "#{i}: #{length}" }
end
arr
end
22.4.6 线程安全
# ❌ 共享可变状态
class Counter
def initialize
@count = 0
end
def increment
@count += 1 # 非原子操作!
end
end
# ✅ 使用 Mutex
class ThreadSafeCounter
def initialize
@count = 0
@mutex = Mutex.new
end
def increment
@mutex.synchronize { @count += 1 }
end
def count
@count
end
end
# ✅ 或使用 Concurrent::AtomicFixnum
require 'concurrent'
class AtomicCounter
def initialize
@count = Concurrent::AtomicFixnum.new(0)
end
def increment
@count.increment
end
def count
@count.value
end
end
22.5 项目组织
22.5.1 目录结构最佳实践
my_project/
├── app/
│ ├── commands/ # 服务对象/命令
│ ├── contracts/ # 验证契约
│ ├── decorators/ # 装饰器
│ ├── forms/ # 表单对象
│ ├── policies/ # 授权策略
│ ├── presenters/ # 展示器
│ ├── queries/ # 查询对象
│ ├── services/ # 服务层
│ └── validators/ # 验证器
├── lib/
│ ├── middleware/ # Rack 中间件
│ └── tasks/ # Rake 任务
└── spec/
├── commands/
├── contracts/
├── factories/ # Factory Bot
├── fixtures/
├── models/
├── queries/
├── services/
└── support/ # 共享示例和配置
22.5.2 Gemfile 最佳实践
source 'https://rubygems.org'
ruby '3.3.0'
# 核心框架
gem 'rails', '~> 7.1'
# 数据库
gem 'pg', '~> 1.5'
# Web 服务器
gem 'puma', '~> 6.0'
# 认证
gem 'devise', '~> 4.9'
# 后台任务
gem 'sidekiq', '~> 7.0'
# 前端
gem 'turbo-rails', '~> 1.5'
gem 'stimulus-rails', '~> 1.3'
# 工具
gem 'jbuilder', '~> 2.11'
gem 'bootsnap', require: false
group :development, :test do
gem 'rspec-rails', '~> 6.1'
gem 'factory_bot_rails', '~> 6.4'
gem 'faker', '~> 3.2'
gem 'rubocop', require: false
gem 'rubocop-rails', require: false
gem 'rubocop-rspec', require: false
gem 'pry-byebug'
end
group :development do
gem 'web-console'
gem 'letter_opener'
end
group :test do
gem 'capybara', '~> 3.39'
gem 'simplecov', require: false
gem 'shoulda-matchers', '~> 6.0'
gem 'database_cleaner-active_record', '~> 2.1'
end
group :production do
gem 'lograge'
end
22.6 动手练习
- 配置 RuboCop
# 为你的项目配置 RuboCop
# 并修复所有警告
rubocop -A
- 重构代码
# 重构以下代码,应用本章学到的模式
class Order
def process
if valid?
total = 0
items.each do |item|
total += item.price * item.quantity
end
if user.vip?
total *= 0.9
end
tax = total * 0.08
total += tax
# ... 更多逻辑
end
end
end
- 代码审查清单
创建一个代码审查清单,包含本章提到的所有最佳实践。
22.7 学习资源汇总
| 资源 | 说明 |
|---|---|
| 《Practical Object-Oriented Design in Ruby》 | Sandi Metz 的经典之作 |
| 《Eloquent Ruby》 | 优雅 Ruby 代码的指南 |
| 《Effective Ruby》 | Ruby 最佳实践 |
| 《Metaprogramming Ruby 2》 | 元编程深入 |
| Ruby Style Guide | 社区代码风格指南 |
| Rails Style Guide | Rails 代码风格指南 |
| Awesome Ruby | Ruby 精选资源列表 |
22.8 本章小结
| 要点 | 说明 |
|---|---|
| 命名规范 | snake_case 方法变量,PascalCase 类模块 |
| RuboCop | 自动化代码风格检查工具 |
| 设计模式 | 策略、观察者、装饰器、服务对象、查询对象 |
| 常见陷阱 | 浮点精度、可变默认值、线程安全 |
| 项目组织 | 清晰的目录结构和依赖管理 |
结语
恭喜你完成了 Ruby 入门指南的全部 22 章内容!
回顾你的学习旅程:
起步篇 → 核心语法 → 面向对象 → 高级特性 → 工程实践 → 进阶与生产
01-04 05-08 09-10 11-14 15-18 19-22
接下来的建议:
- 实践项目:用 Ruby 构建一个完整的项目
- 阅读源码:学习 Rails、Sinatra 等框架的源码
- 参与社区:加入 Ruby China,参与开源项目
- 持续学习:关注 Ruby 版本更新和新特性
“Ruby 不仅是一门语言,更是一种编程哲学。享受编程的乐趣吧!” —— Matz
上一章:← 第 21 章:Docker 部署 返回目录:Ruby 入门指南