Ruby 標準ライブラリ Singleton Observable SingleForwardable PStore

以前Forwardableモジュールについて書いたとき、Rubyに標準ライブラリをもっと知っていた方がいいなというところを感じていました。

いくつか確認したので、メモ。

参考

試す

Singleton

SingletonSingleton パターンを提供するモジュール。
クラスが単一のインスタンスしか持たなくなります。

test-singleton.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
require 'singleton'

class SingletonClass
include Singleton
attr_reader :val

def initialize
@val = 1
end

def set_val(val)
@val = val
end

def val
@val
end
end

sc1 = SingletonClass.instance
sc2 = SingletonClass.instance
# sc3 = SingletonClass.new <= new が、private メソッドになったので、.new はできない
# test-singleton.rb:19:in `<main>': private method `new' called for SingletonClass:Class (NoMethodError)

puts sc1
# => <SingletonClass:0x000055697269b780>

puts sc2
# => <SingletonClass:0x000055697269b780>
# => 同一のインスタンスである。

puts sc2.val
# => 1

sc1.set_val(3)
puts sc2.val
# => 3
# <= sc1 を変更して sc2 に反映されている

こういうことをすると、クラスメソッドとクラス変数でやってしまいそうなところですが、newでインスタンス化できないのがポイントになりそうでした。

Rubyによるデザインパターンまとめ
という記事に注意が書いてあります。
この記事が「Rubyによるデザインパターン」という書籍のまとめ記事なのですが、書籍自体は絶版らしくアマゾンでは高額。
どうやら都内の図書館で借りられる様です。(都内で 9 館)

Rubyによるデザインパターン

この記事で触れられていることを正とすると、「状態」を持つことを避けるべきだそうです。
なので上記のような使い方はしていけないということです。

だとしたらほとんどmoduleでモジュール関数でいいんじゃないか?という感じ。

Observable

Observer パターンを提供するモジュールObservable
注意なのは、Observableを読み込むのは、Subjectであること。

test-observable-1.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
require "observer"

# 変更が発生して監視される Subject
# User クラス
class User
include Observable
attr_reader :name

def initialize(name)
@name = name
end

def update_name(new_name)
unless @name == new_name
@name = new_name
changed
# 登録された Observer に通知する
notify_observers(self)
end
end
end

# 変更を監視する Observer
# UserObserver クラス
class UserMailObserver
def update(user)
puts "Mail: #{user.name}に変更されました"
end
end

class UserChatObserver
def update(user)
puts "Chat: #{user.name}に変更されました"
end
end

u = User.new("user1")

# Observerを割り当て
u.add_observer(UserMailObserver.new)
u.add_observer(UserChatObserver.new)

puts u.name
# => user1

u.update_name("user1-1")
# => Mail: user1-1に変更されました
# Chat: user1-1に変更されました

Userクラスのインスタンスの変更を、UserMailObserverUserChatObserver が受け取り、それぞれのupdateメソッドを実行する。
変更を検知し実行する内容が増えても、add_observerでオブザーバーを追加することで対応できる。

もし、オブザーバーの処理が例外を出したらどうなるか確認する。

test-observable-2.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
require "observer"

# 変更が発生して監視される Subject
# User クラス
class User
include Observable
attr_reader :name

def initialize(name)
@name = name
end

def update_name(new_name)
unless @name == new_name
@name = new_name
changed
# 登録された Observer に通知する
notify_observers(self)
end
end
end

# 変更を監視する Observer
# UserObserver クラス
class UserMailObserver
def update(user)
puts "Mail: #{user.name}に変更されました"
end
end

class UserChatObserver
def update(user)
puts "Chat: #{user.name}に変更されました"
end
end

# わざと例外を出す Observer
class UserRaiseObserver
def update(user)
raise
end
end

u2 = User.new("user2")

# 例外を出す UserRaiseObserver を 間に挿んで登録する
u2.add_observer(UserMailObserver.new)
u2.add_observer(UserRaiseObserver.new)
u2.add_observer(UserChatObserver.new)

u2.update_name("user2-2")

# => Mail: user2-2に変更されました
# test-observable.rb:53:in `update': Error : UserRaiseObserver#update (RuntimeError)
# from /usr/local/lib/ruby/3.0.0/observer.rb:222:in `block in notify_observers'
# from /usr/local/lib/ruby/3.0.0/observer.rb:221:in `each'
# from /usr/local/lib/ruby/3.0.0/observer.rb:221:in `notify_observers'
# from test-observable.rb:18:in `update_name'
# from test-observable.rb:63:in `<main>'
#

結論として、例外を出した後の UserChatObserver は呼び出されなかった。
Observer 側で例外が閉じていることが望ましいのでしょうが、敢えて例外の可能性を踏まえて Subject で対応してみます。

test-observable-3.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
require "observer"

# 変更が発生して監視される Subject
# UserPossibilityRaise クラス
# notify_observers は例外を考慮
class UserPossibilityRaise
include Observable
attr_reader :name

def initialize(name)
@name = name
end

def update_name(new_name)
unless @name == new_name
@name = new_name
changed
# 登録された Observer に通知する
begin
notify_observers(self)
rescue => e
puts e
# 異常終了してしまったのでフラグを自分で落としておく
changed(false)
end
end
end
end

# 変更を監視する Observer
# UserObserver クラス
class UserMailObserver
def update(user)
puts "Mail: #{user.name}に変更されました"
end
end

class UserChatObserver
def update(user)
puts "Chat: #{user.name}に変更されました"
end
end

# わざと例外を出す Observer
class UserRaiseObserver
def update(user)
raise "Error : UserRaiseObserver#update"
end
end

u3 = UserPossibilityRaise.new("user3")

u3.add_observer(UserMailObserver.new)
u3.add_observer(UserRaiseObserver.new)
u3.add_observer(UserChatObserver.new)

u3.update_name("user3-3")
# => Mail: user3-3に変更されました
# Error : UserRaiseObserver#update
#
# 例外は安全に処理できるが、後続のオブザーバーの処理は実行できない

例外は処理できますが、後続のものを再実行するであるとかはできないので、あくまでオブザーバー側で処理した方が良さそうです。
再実行や必須でまとまった処理だとを考慮する部分は、単一のオブザーバー側の中で閉じさせ処理を書き連ねるのがよさそうです。

SingleForwardable

Forwardable モジュールでのメソッド移譲Forwardableライブラリを使いました。
Forwardableライブラリは、Forwardableモジュールだけでなく、SingleForwardableを提供していました。

SingleForwardableの使い方を確認しました。
SingleForwardableは、オブジェクトへのメソッドの移譲機能を提供します。

test-singleforwardable.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
require 'forwardable'

# インスタンスメソッドの移譲には、Forwardableを使う
class Aa
extend Forwardable
attr_reader :arr

def_delegator :@arr, :each

def initialize(arr)
@arr = arr
end
end

a = Aa.new([1,2,3])
a.each{ |p| puts p}
# => 1
# 2
# 3

# オブジェクトのメソッドの移譲(クラスの場合はクラスメソッド)には、SingleForwardableを使う
class Bb
extend SingleForwardable
@@arr = [1,2,3]

def_delegator @@arr, :each
end

Bb.each{ |p| puts p}
# => 1
# 2
# 3

# オブジェクトのメソッド以上機能を提供するのでクラスだけでなく、モジュールでも使用できる
module BuiltInCc
def self.call
"BuiltInCc#call"
end
end

module Cc
extend SingleForwardable
def_delegator BuiltInCc, :call
end

puts Cc.call
# => "BuiltInCc#call"


# Forwardable と SingleForwardable を両方使う場合 def_instance_delegator と def_single_delegator を使う
module BuiltInDd
def self.call
"BuiltInDd#call"
end
end

class Dd
extend Forwardable
extend SingleForwardable

attr_reader :arr

def_single_delegator BuiltInDd, :call # <= def_delegator BuiltInDd, :call でも動作した
def_instance_delegator :@arr, :each
def_instance_delegator :@arr, :<<
# <= この二つは、 def_instance_delegators :@arr, :each, :<< と定義してもよい

def initialize(arr)
@arr = arr
end

end

puts Dd.call
# => "BuiltInDd#call"

d = Dd.new([1,2,3,4])
d.each{ |p| puts p}
# => 1
# 2
# 3
# 4

d << 999
d.each{ |p| puts p}
# => 1
# 2
# 3
# 4
# 999

うまく使わないと大混乱を生みそう。

PStore

Ruby のオブジェクトをファイルに書き出すことができる。

test-pstore.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
require 'pp'

require 'pstore'
require 'ostruct'

## オブジェクトを読み書きするファイルのパス
database = PStore.new("./obj")

## 書き込むオブジェクト定義
obj = OpenStruct.new({a:[1,2,3], b:"BBB"})
# => #<OpenStruct a=[1, 2, 3], b="BBB">

## オブジェクトを書き込み
database.transaction do
database["root"] = obj
end

## オブジェクトを読み込み1
# .transaction(read_only = false) なので読み込むだけの時は安全のため true にしておく
database.transaction(true) do
pp database
# => #<PStore:0x00005623d4081760
# @abort=false,
# @filename="./obj",
# @lock=#<Thread::Mutex:0x00005623d4081198>,
# @rdonly=false,
# @table={"root"=>#<OpenStruct a=[1, 2, 3], b="BBB">},
# @thread_safe=false,
# @ultra_safe=false>

p database.roots
# => ["root"]

pp database["root"].a
# => [1, 2, 3]
end

## オブジェクトを読み込み2
database.transaction(true) do |db|
pp db.fetch("root")
# => #<OpenStruct a=[1, 2, 3], b="BBB">

pp db.fetch("root", "defult")
# => #<OpenStruct a=[1, 2, 3], b="BBB">

pp db.fetch("root_1", "defult")
# => "defult"

# pp pstore.fetch("root_1")
# => PStore::Error エラー発生 対応するキーに値がなく、デフォルト値の設定がない

end

## オブジェクトを削除
database.transaction do |db|
pp db.delete("root")
# => #<OpenStruct a=[1, 2, 3], b="BBB">

pp db.fetch("root", "defult")
# => "defult"
end

保存されているのはオブジェクト単体ではなく、複数のオブジェクトがtableプロパティに含まれたものでした。
そういう意味でデータベースですね。

書き込みされたオブジェクトは、以下のように記述されていました。
読めることを期待していたけど、それはできないものでした。

./obj
1
{I"	root:ETU:OpenStruct{:a[iii:bI"BBB;

デザインパターンにかかわるモジュールも触ってみました。
うまく活用したいところです。

ではでは。