YAML 与单例对象

在思考我们如何处理这个问题之前,让我们先简单地看看 YAML 将如何应对这种情况。程序 singleton_y.rb 尝试使用 YAML.dump 保存上面显示的单例对象,并且与 Marshal.dump 不同,它成功了 - 嗯,可以说是的…

singleton_y.rb
  1. ob.xxx( "hello world" )
  2. File.open( 'test.yml', 'w' ){ |f|
  3. YAML.dump( ob, f )
  4. }
  5. ob.xxx( "new string" )
  6. File.open( 'test.yml' ){ |f|
  7. ob = YAML.load(f)
  8. }

如果你看一下保存的 YAML 文件 ‘test.yml’,你会发现它定义了一个普通泛类型(vanilla)对象的实例,它附加了一个名为 x 的变量,它有一个字符串值 “hello world”。这一切都很好。除了通过加载保存的数据重建对象时,新的 ob 将是恰好包含一个额外的实例变量 @x 的 Object 的标准实例。然而,它不再是原来的单例对象,所以新的 ob 会无法访问该单例中定义的任何方法(此处为 xxx 方法)。因此,虽然 YAML 序列化更容易保存和加载在单例中创建的数据项,但在重新加载被保存的数据时,它不会自动重新创建单例本身。

现在让我们回到这个程序的 Marshal 版本。我需要做的第一件事是找到一种至少使它可以保存和加载数据项的方法。一旦我做完了,我将试着弄清楚如何在重新加载时重建单例对象。

为了保存特定的数据项,我可以定义 marshal_dumpmarshal_load 方法,如前所述(参见 limit_m.rb)。这些通常应该在单例的派生类中定义 - 而不是单例本身。

这是因为,如已经说明的那样,当保存数据时,它将被存储为单例的派生类的表示。这意味着,虽然你确实可以将 marshal_dump 添加到从类 X 派生的单例中,但在重构对象时,你将加载泛型类型 X 的对象的数据,而不是特定单例实例的对象。

此代码创建类 X 的单例 ob,保存其数据,然后重新创建类 X 的通用对象:

singleton_m2.rb
  1. class X
  2. def marshal_dump
  3. [@x]
  4. end
  5. def marshal_load(data)
  6. @x = data[0]
  7. end
  8. end
  9. ob = X.new
  10. class << ob
  11. def xxx( aStr )
  12. @x = aStr
  13. end
  14. end
  15. ob.xxx( "hello" )
  16. File.open( 'test2.sav', 'w' ){ |f|
  17. Marshal.dump( ob, f )
  18. }
  19. File.open( 'test2.sav' ){ |f|
  20. ob = Marshal.load(f)
  21. }

就其包含的数据而言,保存的对象和重新加载的对象是相同的。但是,重新加载的对象对单例类没有任何了解,并且单例类包含的方法 xxx 不构成重构对象的一部分。然后,以下将失败:

  1. ob.xxx( "this fails" )

因此,该 Marshal 版本的代码等同于之前给出的 YAML 版本。它可以正确保存和恢复数据,但不会重建单例。

那么,如何从保存的数据中重建单例呢?毫无疑问,有许多聪明而巧妙的方式可以实现这一目标。但是,我会选择一种非常简单的方式:

singleton_m3.rb
  1. FILENAME = 'test2.sav'
  2. class X
  3. def marshal_dump
  4. [@x]
  5. end
  6. def marshal_load(data)
  7. @x = data[0]
  8. end
  9. end
  10. ob = X.new
  11. if File.exists?(FILENAME) then
  12. File.open(FILENAME){ |f|
  13. ob = Marshal.load(f)
  14. }
  15. else
  16. puts( "Saved data can't be found" )
  17. end
  18. # singleton class
  19. class << ob
  20. def xxx=( aStr )
  21. @x = aStr
  22. end
  23. def xxx
  24. return @x
  25. end
  26. end

此代码首先检查是否可以找到包含已保存数据的文件(此示例有意保持简单 - 在实际的应用程序中,你当然需要编写一些异常处理代码来处理可能读取无效数据的问题)。如果找到该文件,则将数据加载到通用 X 类型的对象中:

  1. ob = X.new
  2. if File.exists?(FILENAME) then
  3. File.open(FILENAME){ |f|
  4. ob = Marshal.load(f)
  5. }

只有在完成此操作后,此对象才会“转换”为单例对象。完成此操作后,代码可以在重构单例上使用单例方法 xxx。然后,我们可以将新数据保存回磁盘并在稍后重新加载并重新创建修改后的单例:

  1. if ob.xxx == "hello" then
  2. ob.xxx = "goodbye"
  3. else
  4. ob.xxx = "hello"
  5. end
  6. File.open( FILENAME, 'w' ){ |f|
  7. Marshal.dump( ob, f )
  8. }

如果你希望在实际的应用程序中保存和加载单例,单独的“重建”代码自然可以给出自己的方法:

singleton_m4.rb
  1. def makeIntoSingleton( someOb )
  2. class << someOb
  3. def xxx=( aStr )
  4. @x = aStr
  5. end
  6. def xxx
  7. return @x
  8. end
  9. end
  10. return someOb
  11. end