强曰为道

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

第 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 GuideRails 代码风格指南
Awesome RubyRuby 精选资源列表

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 入门指南