Menu
CoddyTech

单例模式

本课属于 Coddy Ruby 学习旅程的“面向对象编程”单元。第 42 课,共 57 课。

设计模式是解决常见设计问题的经过验证的方案。单例模式确保一个类恰好只有一个实例,并且可以从任何地方访问。Ruby 的标准库内置了此模式:require 'singleton' 和 include Singleton 会添加一个类方法 instance:

require 'singleton'

class Settings
  include Singleton
  attr_accessor :theme

  def initialize
    @theme = "light"
  end
end

Settings.instance.theme = "dark"
puts Settings.instance.theme
puts Settings.instance.equal?(Settings.instance)

输出:

dark
true

include Singleton 还会将 new 设为私有,因此无法创建第二个实例。程序中每个请求 instance 的部分都会获得同一个对象:

require 'singleton'

class Counter
  include Singleton
  attr_reader :hits

  def initialize
    @hits = 0
  end

  def hit
    @hits += 1
  end
end

def page_a
  Counter.instance.hit
end

def page_b
  2.times { Counter.instance.hit }
end

page_a
page_b
puts Counter.instance.hits
begin
  Counter.new
rescue NoMethodError => e
  puts e.message
end

输出:

3
private method `new' called for class Counter

手动实现相同思路,就是编写一个类方法,在首次使用时创建实例,并将 new 设为私有。这展示了模块为你做了什么:

class Config
  def self.instance
    @instance ||= new
  end
  private_class_method :new

  attr_accessor :level

  def initialize
    @level = 1
  end
end

Config.instance.level = 3
puts Config.instance.level

输出:

3

谨慎使用单例。单例是全局状态:任何代码都可以更改它,而且测试无法从干净的副本开始。单例适用于程序中确实只存在一个实例的事物,例如日志记录器或配置;将对象作为参数传入通常更清晰:

# 更容易测试:logger 是传入的。
def process(order, log)
  log.write("processing #{order.id}")
end
challenge icon

挑战

简单

使用 include Singleton 完成单例 AppLogger。它会保存一个条目数组,并包含 log(message, level),用于存储 [INFO] message 或 [ERROR] message(级别使用大写字母)、entries 和 error_count。functions.rb 中的辅助函数 handle_info(message) 和 handle_error(message) 都通过 AppLogger.instance 记录日志,因此它们会访问同一个对象。

提供的代码会读取 info,message 或 error,message 格式的行,将它们传给辅助函数,打印每个条目和 errors: 1,并检查 AppLogger.new 是否会被拒绝。

你的代码应写在 app_logger.rb 和 functions.rb 中。main.rb 包含提供的输入/输出代码,不能编辑。

亲手试试

require_relative 'app_logger'
require_relative 'functions'

# 提供的输入输出代码:保持原样
input = []
while (line = gets)
  input << line.chomp
end
input.each do |line|
  level, message = line.split(",", 2)
  level == "error" ? handle_error(message) : handle_info(message)
end
AppLogger.instance.entries.each { |e| puts e }
puts "errors: #{AppLogger.instance.error_count}"
begin
  AppLogger.new
  puts "new is allowed"
rescue NoMethodError
  puts "new is private"
end
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Ruby 在线编译器