JQ Blog

メソッド

動的メソッド

メソッドを呼び出す方法

  • ドットを使う
  • send()を使う
    • send()メソッドの第一引数は呼び出すメソッド名 => 文字列またはシンボル
    • その他の引数はそのままメソッドに渡される

send()を使えばコードの実行時に呼び出すメソッドを直前に決められる。なので動的な処理が可能になる。これを動的ディスバッチと呼ぶ。

プライバシー問題

send()メソッドの問題はprivateメソッドも呼ばれるということだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C
  private

  def private_method
    puts "called private method"
  end
end

C.new.private_method
=> NoMethodError: private method 'private_method' called for #<C:0x007f8149e5c8e8>

C.new.send(:private_method)
"called private method"
=> nil

プライバシーの尊重のため、public_send()を使う方法もある。

  • 例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class C
  def public_method
    puts "called public method"
  end

  private

  def private_method
    puts "called private method"
  end
end

C.new.public_send(:public_method)
"called public method"
=> nil

C.new.public_send(:private_method)
=> NoMethodError: private method 'private_method' called for #<C:0x007f8149e5c8e8>

メソッドを動的に定義

  • define_method()

Module#define_method()を使えば、メソッドをその場で定義できる。

1
2
3
4
5
6
7
8
9
class C
  define_method :my_method do |my_arg|
    my_arg * 3
  end
end

obj = C.new
obj.my_method(2)
=> 6

define_methodはClassの中で実行され、my_method()Cクラスのインスタンスメソッドとして定義される。実行時にメソッドを定義するこの技術を動的メソッドと呼ぶ。

method_missing()

method_missing()のオーバーライド

method_missing()メソッドはBasicObjectのインスタンスメソッドだ。つまり、BasicObjectを継承しているObjectを継承するオブジェクトはmethod_missing()を持っているということになり、もちろんオーバーライドもでき、存在しないメソッドに対するハンドリングもできるということだ。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class C
  def method_missing(method, *args)
    puts "#{method}(#{args.join(',')})を呼びました"
    puts "(ブロックを渡した)" if block_given?
  end
end

C.new.hello('a', 'b')
hello(a,b)を呼びました
=> nil

C.new.hello('a', 'b') do
  # block
end
hello(a,b)を呼びました
(ブロックを渡した)
=> nil

ゴーストメソッド

同じようなメソッドをたくさん定義しなければならないとき、いちいち定義するより、method_missing()を呼び出す方がもっと楽かもしれない。method_missing()での処理は呼び出し側からは通常の呼び出しのように見える。しかし、レシーバに対応するメソッドは見当たらない。これはゴーストメソッドと呼ぶ。上記の例ではhello()がゴーストメソッドなるのだ。

もっとmethod_missing()

メソッド名が衝突したら

1
2
3
4
5
6
7
8
9
10
11
class C
  def method_missing(method, *args)
    puts "#{method}(#{args.join(',')})を呼びました"
    puts "(ブロックを渡した)" if block_given?
  end
end

C.new.display('a', 'b') do
  # block
end
ArgumentError: wrong number of arguments (given 2, expected 0..1)

上記のコードでは

1
2
display(a,b)を呼びました
(ブロックを渡した)

とプリントされると予想したけど、プリントされずArgumentErrorになってしまった。
これの問題はObjectから継承されたメソッドの中でdisplayというメソッドがあり、method_missing()が呼ばれなかったからだ。
これはは必要ないメソッドを全部削除することで対応できる。メソッドをすべて削除することをブランクスレートと呼ぶ。

  • 例2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class C
  instance_methods.each do |m|
    undef_method m unless m.to_s =~ /__|method_missing/
  end

  def method_missing(method, *args)
    puts "#{method}(#{args.join(',')})を呼びました"
    puts "(ブロックを渡した)" if block_given?
  end
end

C.new.display('a', 'b') do
  # block
end
display(a,b)を呼びました
(ブロックを渡した)
=> nil

このコードは予想通りmethod_missing()を呼び出すことができる。

まとめ

  • メソッドはsend()でも呼び出せる
  • send()privateメソッドも呼び出せるため、public_send()を使うこともできる
  • define_method()でメソッドを動的に定義できる
  • method_missing()を利用すると存在しないメソッド、ゴーストメソッドに対するハンドリングや処理ができる

オブジェクトモデル

オープンクラス

下記のメソッドは文字列からアルファベットとスペース以外を取り除くメソッド

1
2
3
def to_alphanumeric(s)
  s.gsub /[^\w\s]/, ''
end

オープンクラスを使えば、

1
2
3
4
5
class String
  def to_alphanumeric
    gsub /[^\w\s]/, ''
  end
end

こう書くこともできる。
標準クラスのStringを書き換える。すると、

1
2
"!a #b+c".to_alphanumeric
=> "a bc"

こういうふうに適用される。

クラス定義

1
2
3
4
5
6
7
8
9
10
11
class C
  def my_method
    puts "my_method"
  end
end

class C
  def my_method2
    puts "my_method2"
  end
end

class Cを定義してまたclass Cをさらに定義するとどうなるだろう。
class Cを上書きすると思ったが実際叩いてみたら

1
2
3
4
5
6
C.new.my_method
my_method
=> nil
C.new.my_method2
my_method2
=> nil

上書きじゃなく、class Cに追加する形になる。

定数

定数はディレクトリとファイルのようにパスで呼び出すことができる。

1
2
3
4
5
6
7
8
9
10
11
12
13
module M
  MyConstant = "ABCDE"

  class C
    MyConstant = "12345"
  end
end

M::MyConstant
=> "ABCDE"

M::C::MyConstant
=> "12345"

クラスオブジェクト

クラスオブジェクトをみてみるため、"hello"からたどり着いてみる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
"hello".class
=> String

String.class
=> Class

Class.instance_methods(false)
=> [:allocate, :new, :superclass]

String.superclass
=> Object

Object.superclass
=> BasicObject

BasicObject.superclass
=> nil

Class.superclass
=> Module

Module.superclass
=> Object

そうなんだ!と思いながら1つ気になる点がある。

1
2
Object.class
=> Class

ObjectclassClass
でも、Classsuperclassmoduleであり、modulesuperclassObject
ではClassの上位classのObjectClass? ちゃんと理解ができていない。。これは今度深く調べてみよう。

self

class定義の時のself

1
2
3
4
5
6
7
8
9
10
11
12
13
class C
  self
end
=> C

class C
  def my_method
    self
  end
end

C.new.my_method
=> #<CC:0x007ffbe8a67e08>

method内でのselfはオブジェクトを指す。method外でのselfはクラスを指す。

privateキーワードのself

privateメソッドのルール:「明示的なレシーバーをつけて呼び出せない」 下記は明示的なselfをつけているのでエラーとなる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C
  def public_method
    self.private_method
  end

  private

  def private_method
    puts "ABCDE"
  end
end

C.new.public_method
NoMethodError: private method `private_method'

selfを削除すると動作するようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class C
  def public_method
    private_method
  end

  private

  def private_method
    puts "ABCDE"
  end
end

C.new.public_method

ABCDE
=> nil

参照

本「メタプログラミングRuby」第1章

Rubyのblock, Proc, Lambda

ブロック

ブロックとは

  • do~endまたは{~}で囲われた引数となるためのもの
  • それ単体では存在できず、メソッドの引数にしかなれない
  • 引数として渡されたブロックは、yieldによって実行される
1
2
3
4
5
6
7
8
def my_method
  yield
end

my_method do
  p "Hello, block!"
end
=> "Hello, block!"

説明

  • 引数を明示
1
2
3
4
5
6
7
8
def my_method(&block)
  block.call
end

my_method do
  p "Hello, block!"
end
=> "Hello, block!"
  • 「&」とは
    &を付けることで、実際に引数にブロックが渡ってきた際、
    Procオブジェクトに変換している。

  • Procオブジェクトとは

    • ブロックをオブジェクト化したものがProc
      • ブロックをオブジェクトに変換することで、引き渡されたメソッド内で扱えるようにする
    • Procオブジェクトは、callで呼び出すことが出来る
  • 注意点
    引数として渡せるブロックは一つだけ。

  • yieldが使える
    引数として渡されるブロックが1つに限られているならば、呼び出し箇所をblock.callと明示せずに、yieldで統一する。

1
2
3
4
5
6
7
8
9
#メソッド定義
def my_method(&block)
  yield
end

my_method do
  p "Hello, block!"
end
=> "Hello, block!"

さらに、
yieldを使うなら引数の(&block)はいらないので

1
2
3
4
5
6
7
8
def my_method
  yield
end

my_method do
  p "Hello, block!"
end
=> "Hello, block!"

こういうふうに省略できる。

Proc

  • ブロックをオブジェクト化したものがProc
  • Procオブジェクトはcallで呼び出すことが出来る
  • Procに引数を持つこともできる
  • Proc.newとlambdaはほぼ同義

Procオブジェクトの定義と呼び出し

1
2
3
4
5
6
proc = Proc.new do
  p "hoge"
end

proc.call
=> "hoge"

Procに引数が渡された場合

1
2
3
4
5
6
proc = Proc.new do |s|
  p "Hello, #{s}!"
end

proc.call("Proc")
=> "Hello, Proc!"

Proc.newとlambdaはほぼ同義

1
2
3
4
5
6
7
lambda1 = lambda { p "hoge" }
lambda1.call
=> "hoge"

lambda2 = lambda { |s| p "Hello, #{s}!" }
lambda2.call("Proc")
=> "Hello, Proc!"

Procとlambdaの違い

  • lambdaはブロックの引数の数が違うとエラーを起こしてくれる。
1
2
3
4
5
6
7
8
9
proc = Proc.new { |a,b,c| p "#{a},#{b},#{c}" }
proc.call(2, 4)

=> 2,4,nil

lambda1 = lambda { |a,b,c| p "#{a},#{b},#{c}" }
lambda1.call(2, 4)

=> wrong number of arguments (2 for 3) (ArgumentError)
  • 明示的にreturnやbreakを行った場合の挙動が違います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def method_proc
  proc = Proc.new { return p "proc"}
  proc.call
  p "method_proc"
end

method_proc
=>"proc"

def method_lambda
  lambda = lambda{ return p "lambda"}
  lambda.call
  p "method_lambda"
end

method_lambda
=>"lambda"
  "method_lambda"

ブロックやProcのメリット

柔軟に拡張できる

下のコードのようにメソッドを汎用的に使うことができる

1
2
3
def my_method(input, someProc)
  someProc.call(5, input)
end
1
2
3
4
5
6
sum_proc = Proc.new do |x, y|
  p x + y
end

my_method(3, sum_proc)
=> 8
1
2
3
4
5
6
string_proc = Proc.new do |x, string|
  p string * x
end

my_method('hello_proc! ', string_proc)
=> "hello_proc! hello_proc! hello_proc! hello_proc! hello_proc! "

クロージャとしての機能が得られる

サンプル①

1
2
3
4
5
6
7
8
9
def bar
  str = 'bye'
  yield('Jo')
end

str = 'hello'
bar { |name| "#{str}, #{name}" }

=> "hello, Jo"

サンプル②

1
2
3
4
5
6
7
8
9
10
11
12
13
14
n = 1

def foo(n)
  n = n + 1
end

foo(n)
=> 2
foo(n)
=> 2
foo(n)
=> 2
foo(n)
=> 2
1
2
3
4
5
6
7
8
9
10
11
n = 1
proc = Proc.new do
  n = n + 1
end

proc.call
=> 2
proc.call
=> 3
proc.call
=> 4

block_given?

block_given?とは

引数としてブロックが与えられたかどうかを判別するメソッド。

使い方

1
2
3
4
5
6
7
def greet_method
  if block_given?
    yield('hello!')
  else
    p "ブロックがありません。"
  end
end
1
2
3
4
greet_method do |str|
  p str
end
=> "hello!"
1
2
greet_method
=> "ブロックがありません。"

参考

https://qiita.com/kidach1/items/15cfee9ec66804c3afd2
https://qiita.com/ryo-ma/items/24c46592b45775e8644d

Active Storage

Rails5.2の新しい機能の中にActive Storageというものが気になったので使ってみた。

Active Storageとは

AWS S3やGoogle Cloud Storageなどのクラウドファイルストレージサービスへのアップロードをシンプルにやってくれる仕組みとのこと。
もちろん物理的なディスクシステムへ保存することもできるけど、主眼はあくまでクラウドサービスに置いているらしい。

Modelの仕組

Active Storageをインストールするとactive_storage_blobs,active_storage_attachmentsが生成される。

active_storage_blobsmetadata (filename, content-type, etc.)を保存するtableになっている。keyカラムはidentifier keyとしてクラウドファイルストレージのアップするファイルのfilenameになる。

active_storage_attachmentsは添付ファイルの親モデルとactive_storage_blobsをアソシエートしてくれる。親モデルはpolymorphicになる。

使い方

  • 環境
    • Rails 5.2.0.beta2
    • ruby 2.4.1p111

インストール

rails newを通してプロジェクトを作成するとき自動的にActive Storageがインストールされる。
rails new --skip-active-storageにするとインストールせずにプロジェクトを作成できる。

既存プロジェクトにActive Storageを入れたい場合は、まずRailsのバージョンをアップグレードしてから、

1
2
rails active_storage:install
bundle exec rake db:migrate

で入れることができる。

サンプルコード

  • gemfile
1
2
gem 'mini_magick', '~> 4.8' # 画像の加工のために
gem 'aws-sdk-s3' # S3へのアップロード
  • model
1
2
3
4
class User < ApplicationRecord
  has_one_attached :avatar # has_oneの場合
  has_many_attached :images # has_manyの場合
end
  • controller
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UsersController < ApplicationController
  def create
    @user = User.new(user_params)

    ActiveRecord::Base.transaction do
      render :new and return unless @user.save

      if (avatar = params[:user][:avatar]).present?
        @user.avatar.attach(avatar)
      end

      if (images = params[:user][:images]).present?
        @user.images.attach(images)
      end
    end

    redirect_to users_path
  end
end
  • view
1
2
3
4
5
6
7
8
9
10
11
12
13
14
= form_for @user do |f|
  = f.label :name
  = f.text_field :name
  br
  = f.label :email
  = f.email_field :email
  br
  = f.label :avatar
  = f.file_field :avatar
  br
  = f.label :images
  = f.file_field :images, multiple: true # has_manyの場合
  br
  = f.submit "作成"

クラウド設定

  • config/environments/development.rb
1
2
# Store uploaded files on the local file system (see config/storage.yml for options)
config.active_storage.service = :amazon
  • config/storage.yml
1
2
3
4
5
6
7
# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
amazon:
  service: S3
  access_key_id: <%= ENV['AWS_ACCESS_KEY'] %>
  secret_access_key: <%= ENV['AWS_SECRET_KEY'] %>
  region: <%= ENV['AWS_REGION'] %>
  bucket: <%= ENV['AWS_BUCKET'] %>

リサイズ

1
= image_tag @user.avatar.variant(resize: '200x200')

このようにvariantメソッドを使えばリサイズでビューに表示できる。
参照:https://github.com/rails/rails/blob/master/activestorage/app/models/active_storage/variant.rb

Direct upload

JSを使ってDirect uploadすることもできる。

  • include activestorage.js

    • asset pipelineの場合
    //= require activestorage
    
    • npm packageの場合
    import * as ActiveStorage from "activestorage"
    ActiveStorage.start()
    
  • file_fieldに設定

    • ビューの file_field に direct_upload: true を入れる
    = f.file_field :avatar, direct_upload: true
    

サンプル

https://active-storage-jo.herokuapp.com

参考

https://qiita.com/nashirox/items/e63fd28d974ecf12f0e7
https://github.com/rails/rails/tree/master/activestorage

RubyにおけるObserverパターン

Observerパターン

サンプル①

コード

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name}のために小切手を切ります!"
    puts "彼の給料はいま#{changed_employee.salary}です!"
  end
end

class Employee
  attr_reader :name
  attr_accessor :salary

  def initialize(name, salary, payroll)
    @name = name
    @salary = salary
    @payroll = payroll
  end

  def salary=(new_salary)
    @salary = new_salary
    @payroll.update(self)
  end
end

結果

1
2
3
4
5
6
7
payroll = Payroll.new
jo = Employee.new('Jo', 30000, payroll)
jo.salary = 35000

# =>
Joのために小切手を切ります!
彼の給料はいま35000です!

これはPayrollupdateだけ指定されていて汎用的ではない。

サンプル②

コード

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
class Payroll
  def update(changed_employee)
    puts "#{changed_employee.name}のために小切手を切ります!"
    puts "彼の給料はいま#{changed_employee.salary}です!"
  end
end

class Employee
  attr_reader :name
  attr_accessor :salary

  def initialize(name, salary)
    @name = name
    @salary = salary
    @observers = []
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end
end

結果

1
2
3
4
5
6
7
8
9
10
jo = Employee.new('Jo', 30000)

payroll = Payroll.new
jo.add_observer(payroll)

jo.salary=35000

# =>
Joのために小切手を切ります!
彼の給料はいま35000です!

このコードに新しいクラスを出してそのクラスも適用してみると、

1
2
3
4
5
class TaxMan
  def update(changed_employee)
    puts "#{changed_employee.name}に新しい税金の請求書を送ります!"
  end
end
1
2
3
4
5
6
7
8
9
tax_man = TaxMan.new
jo.add_observer(tax_man)

jo.salary = 90000

# =>
Joのために小切手を切ります!
彼の給料はいま90000です!
Joに新しい税金の請求書を送ります!

サンプル③(モジュール化)

コード

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
module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end

class Employee
  include Subject
  attr_reader :name, :salary
  def initialize(name, salary)
    super()
    @name = name
    @salary = salary
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end
end

結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jo = Employee.new('Jo', 30000)

payroll = Payroll.new
tax_man = TaxMan.new

jo.add_observer(payroll)
jo.add_observer(tax_man)

jo.salary = 90000

# =>
Joのために小切手を切ります!
彼の給料はいま90000です!
Joに新しい税金の請求書を送ります!

サンプル④(コードブロック)

RubyでObservableというライブラリを提供してくれるけど、Observableはコードブロックをサポートしてくれない。
次のコードはコードブロックに対する対応になる。

コード

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
module Subject
  def initialize
    @observers = []
  end

  def add_observer(&observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.call(self)
    end
  end
end

class Employee
  include Subject
  attr_reader :name, :salary
  def initialize(name, salary)
    super()
    @name = name
    @salary = salary
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end
end

結果

1
2
3
4
5
6
7
8
9
10
11
12
jo = Employee.new('Jo', 30000)

jo.add_observer do |changed_employee|
  puts "#{changed_employee.name}のために小切手を切ります!"
  puts "彼の給料はいま#{changed_employee.salary}です!"
end

jo.salary = 90000

# =>
Joのために小切手を切ります!
彼の給料はいま90000です!

RailsのActiveRecord::Observer

RailsではActiveRecord::Observerを提供している。
rails-observersというGemを通して使用できる。

インストール

1
gem 'rails-observers'
1
bundle install

or

1
$ gem install rails-observers

使い方

nameカラムを持っているUserというモデルがあるとしてこのUserモデルに対するObserverを作成してみる。

1
$ rails g observer user

すると、app/models/user_observer.rbというルートで生成される。
そのファイルにクラスを定義する。

1
2
3
4
5
class UserObserver < ActiveRecord::Observer
  def after_save(user)
    puts "#{user.name}が保存されました。"
  end
end

あと、application.rbに設定する。

1
config.active_record.observers = :user_observer

observerが複数の場合、

1
config.active_record.observers = [:user_observer, :team_observer]

のようにもできる。

結果

1
2
3
4
5
user = User.new(name: 'Jo')
user.save

# =>
Joが保存されました。

その他

1
2
3
4
5
6
7
class NotificationsObserver < ActiveRecord::Observer
  observe :comment, :like

  def after_create(record)
    # notifiy users of new comment or like
  end
end

observeを指定すると指定されたモデルのみobserveすることもできる。

終わり

RubyのObserverパターンとActiveRecord::Observerを調べながら
Railsで提供しているライブラリActiveRecord::Callbacksと少し似ていると思った。

参考

本 - Rubyによるデザインパターン
https://github.com/rails/rails-observers

PubSubとObserver

PubSub

PubSubとは

出版-購読型モデル(しゅっぱん-こうどくがたモデル、英: Publish/subscribe)は、非同期メッセージングパラダイムの一種であり、メッセージの送信者(出版側)が特定の受信者(購読側)を想定せずにメッセージを送るようプログラムされたものである。出版されたメッセージにはクラス分けされ、購読者に関する知識を持たない。購読側は興味のあるクラスを指定しておき、そのクラスに属するメッセージだけを受け取り、出版者についての知識を持たない。出版側と購読側の結合度が低いため、スケーラビリティがよく、動的なネットワーク構成に対応可能である。

参照:出版-購読型モデル(ウィキペディア)

利点

出版側と購読側は疎結合されており、相手の存在を知る必要もない。トピックに関して通信さえできれば、両者はシステムのネットワーク構成も知る必要がない。また、相手の状態がどうであろうと個々のシステムは正常に稼動し続ける。一般的な密結合のクライアントサーバモデルでは、クライアントはサーバ上でサーバプロセスが動作していないときはメッセージを送ることが出来ないし、サーバはクライアントが動作していないときはメッセージを受け取れない。出版-購読型モデルでは、出版側と購読側を位置的に分離すると同時に、時間的にも分離する。出版-購読型システムの戦略として、出版側がダウンしていても、バックログを使って購読側が動作し続けるようにできる(帯域幅調整の一種)。

欠点

ブローカー(サーバ)を使った出版-購読型システムでは、購読側がブローカーに対してメッセージ送信を要求するのは帯域内で行われ、セキュリティ問題が発生する可能性がある。ブローカーを騙して間違ったクライアントにメッセージを送らせたり、クライアントがサービスを受けられないようにすることが考えられる。購読の正当性を検証するようにした場合、ブローカーが過負荷に陥る可能性もある。ブローカーを使わないシステムでも、購読側が認証されていないメッセージを受信する可能性がある。認証されていない出版側は不正で損害を与えるようなメッセージをシステム内に送り込む可能性がある。これはシステムがブロードキャストやマルチキャストを使っているために発生する。このような不正アクセスに対する防御策としては、今のところ暗号化(SSL/TLSなど)しかない。

RailsのPubSub

Railsでは前記事を書いてたActionCableがPubSubモデルを利用して使われている。
RailsGuide(日本語版)にはこう書いてある。

Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。Action Cable が導入されたことで、Rails アプリケーションの効率の良さとスケーラビリティを損なわずに、通常のRailsアプリケーションと同じスタイル・方法でリアルタイム機能をRubyで記述できます。クライアント側のJavaScriptフレームワークとサーバー側のRubyフレームワークを同時に提供する、フルスタックのフレームワークです。Active RecordなどのORMで書かれたすべてのドメインモデルにアクセスできます。

Pub/Subは出版-購読型モデルとも呼ばれる、メッセージキューのパラダイムです。出版側(Publisher)が、購読側(Subscriber)の抽象クラスに情報を送信します。 このとき、個別の受信者を指定しません。Action Cableでは、このアプローチを採用してサーバーと多数のクライアント間で通信を行います。

参照:RailsGuide(日本語版)

Observer

Observerパターンとは

ウィキペディアにはObserverパターンに対してこう書いてある。

Observer パターン(オブザーバ・パターン)とは、プログラム内のオブジェクトの状態を観察(英: observe)するようなプログラムで使われるデザインパターンの一種。出版-購読型モデルとも呼ばれる。暗黙的呼び出しの原則と関係が深い。

分散イベント処理システムの実装に主に使われる。言語によっては、このパターンで扱われる問題は言語が持つイベント処理構文で処理される。リアルタイムのアプリケーション配置の手段として興味深い機能である。

参考:Observer パターン(ウィキペディア)

つまり、出版者(Subject)に対して購読者(Observer)は監視するし、出版者に何かの変更があればそれに対する処理を行うということになる。

RailsのObserverパターン

RubyにはobservableというModuleがあって監視できるようになっている。

サンプルコード

  • Employee(サブジェクト):従業員を表す
  • Payroll(オブザーバ1):給与の小切手の発行を行う
  • TaxMan(オブザーバ2):税金の請求書の発行を行う

まずは従業員を表すEmployeeクラスは、name, title, salaryといったデータと、salaryの変更を受け付けるメソッドを持っている。 さらに、Employeeクラスにobservableincludeする。 observableで用いるメソッドは次のとなる。

  • add_observerメソッドで通知する先のオブジェクトを追加
  • changedメソッドとnotify_observersメソッドでオブジェクトに通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'observer'
class Employee
  include Observable
  attr_reader :name, :title, :salary
  def initialize(name, title, salary)
    @name   = name
    @title  = title
    @salary = salary
    payroll = Payroll.new
    taxman  = TaxMan.new
    add_observer(payroll)
    add_observer(taxman)
  end

  def salary=(new_salary)
    @salary = new_salary
    changed
    notify_observers(self)
  end
end

次に給与の小切手の発行を行うPayrollクラスと、税金の請求書の発行を行うTaxManクラスを作成する。

1
2
3
4
5
6
7
8
9
10
11
class Payroll
  def update(changed_employee)
    puts "彼の給料は#{changed_employee.salary}になりました!#{changed_employee.name}のために新しい小切手を切ります。"
  end
end

class TaxMan
  def update(changed_employee)
    puts "#{changed_employee.name}に新しい税金の請求書を送ります"
  end
end

コードはここまでになるので、コンソールで確認してみる。

1
2
3
4
5
6
7
jo = Employee.new('Jo', 'System Team member', 5000)
jo.salary = 6000
#=> 彼の給料は6000になりました!Joのために新しい小切手を切ります。
#=> Joに新しい税金の請求書を送ります
jo.salary = 7000
#=> 彼の給料は7000になりました!Joのために新しい小切手を切ります。
#=> Joに新しい税金の請求書を送ります

こういうふうにRubyでObserverの機能を使える。

参照

出版-購読型モデル(ウィキペディア)
Observer パターン(ウィキペディア)
RailsGuide(日本語版)

PubSubHubbub

PubSubHubbubとは

PubSubHubbub (パブサブハブバブ) とは、更新情報を Google にリアルタイムで通知することができるプロトコルです。略して、PuSH (プッシュ) とも呼ばれます。Pub は Publisher (配信)、Sub は Subscriber (購読) を意味しており、その間に Hub というサービス (中間サーバ) があるため、このような名前になっています。配信側がコンテンツを更新すると、Hub が購読側に通知を行います。購読側は更新情報をすぐに知ることができるため、リアルタイムの情報配信が実現できる仕組みになっています。

参照 - https://murashun.jp/blog/20150915-01.html

PubSubHubbubにリクエストを送る方法

PubSubHubbubにリクエストを送る場合は、下記のURLにPOSTでリクエストを送る。

http://pubsubhubbub.appspot.com/

リクエストを送る場合、dataとしてhub.modepublishを設定し、hub.urlフィードのURLを設定する。また、Content-Typeにはapplication/x-www-form-urlencodedを設定する。

PubSubHubbub対応に使えるGems

とりあえずgithubのstarsがigrigorik/PubSubHubbubの方が多かったのでこれの使い方を少しみてみる。
- Simple client example

1
2
3
4
5
6
EventMachine.run {
  pub = EventMachine::PubSubHubbub.new('http://pubsubhubbub.appspot.com/publish').publish "http://www.test.com/"

  pub.callback { puts "Successfully notified hub." }
  pub.errback  { puts "Uh oh, something broke: #{pub.response}" }
}
  • Posting multiple URL’s
1
2
3
4
5
6
7
EventMachine.run {
  feeds = ["http://www.test.com", "http://www.test.com/2"]
  pub = EventMachine::PubSubHubbub.new('http://pubsubhubbub.appspot.com/publish').publish feeds

  pub.callback { puts "Successfully notified hub." }
  pub.errback  { puts "Uh oh, something broke: #{pub.response}" }
}

参照

https://qiita.com/tackeyy/items/0d2079d901d495f50c1e
https://murashun.jp/blog/20150915-01.html
https://github.com/igrigorik/PubSubHubbub

Mastodon把握 - ③(FollowServiceの処理)

callメソッド

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def call(source_account, uri)
  target_account = ResolveRemoteAccountService.new.call(uri)

  raise ActiveRecord::RecordNotFound if target_account.nil? || target_account.id == source_account.id || target_account.suspended?
  raise Mastodon::NotPermittedError  if target_account.blocking?(source_account) || source_account.blocking?(target_account)

  return if source_account.following?(target_account)

  if target_account.locked?
    request_follow(source_account, target_account)
  else
    direct_follow(source_account, target_account)
  end
end

基本的にfollowはターゲットユーザーが非公開アカウントかどうかによって処理が変わる。非公開アカウントの設定はAccountモデルのlockedというカラムに保存するようになっている。
まずターゲットユーザーが非公開アカウントじゃない場合から見てみると、
direct_followというメソッドが呼ばれる。
direct_followメソッドにはターゲットユーザーがlocalかどうかによって処理が変わる。ここのlocalっていうのはAccountモデルのdomainカラムがnilかどうかによって判断する。
ターゲットユーザーがlocalの場合はただメール通知が行われる。localじゃない場合はPubsubhubbub::SubscribeWorkerNotificationWorkerAfterRemoteFollowWorkerクラスが呼ばれる。

Pubsubhubbub::SubscribeWorker

  • PubSubHubbubとは
  • 処理
    • SubscribeServiceを呼ぶ → ターゲットユーザーのAccountモデルに保存されているhub_urlを元にしてhubサーバーにrequestし、購読しているユーザーにみえるようにする。

NotificationWorker

  • Salmon Protocolとは
    • また来週!

AfterRemoteFollowWorker

  • Atom Protocolとは
    • また来週!
  • 処理
    • 新しくアップデートされたターゲットユーザーのlockedtrueになっている場合はfollowをキャンセルして再開FollowServiceを呼び出す。

Mastodon把握 - ②

フォロー

  • Flow
    色んな細かい処理を行なっているが大きい処理を見てみると下記のようになる。

    1. フォロー情報の保存
    2. プッシュ通知
    3. 購読の処理


  • 処理Flow

Alt text

Mastodon把握 - ①

投稿

  • 処理Flow

Alt text

  • 簡単な説明
    • Frontend側
      • JSは細かくコンポーネント化になっていてそれのConnectのためにreact-reduxというライブラリを利用
    • Backend側
      • アーキテクチャとしてServiceクラスを主に使っている
      • それ以外にはappの中にlibフォルダーを置き、処理のためのクラスを管理している