强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Ruby 入门指南 / 第 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 动手练习

  1. 配置 RuboCop
# 为你的项目配置 RuboCop
# 并修复所有警告
rubocop -A
  1. 重构代码
# 重构以下代码,应用本章学到的模式
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
  1. 代码审查清单

创建一个代码审查清单,包含本章提到的所有最佳实践。


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

接下来的建议

  1. 实践项目:用 Ruby 构建一个完整的项目
  2. 阅读源码:学习 Rails、Sinatra 等框架的源码
  3. 参与社区:加入 Ruby China,参与开源项目
  4. 持续学习:关注 Ruby 版本更新和新特性

“Ruby 不仅是一门语言,更是一种编程哲学。享受编程的乐趣吧!” —— Matz


上一章← 第 21 章:Docker 部署 返回目录Ruby 入门指南