Ruboty のコードリーディングで Ruboty の仕組みを理解すると共に Ruby の設計・実装の定石を学ぶ #ruboty

# Ruboty のコードリーディングで Ruboty の仕組みを理解すると共に Ruby の設計・実装の定石を学ぶ #ruboty
## <i class="fa fa-cubes" style="font-size:1em;"></i> 概要
Ruboty のコードリーディングで Ruboty の仕組みを理解すると共に Ruby の設計・実装の定石を学びます。
確認対象は、コマンドラインで `ruboty` を実行して Ruboty が起動するまでの部分です。
## <i class="fa fa-cube" style="font-size:1em;"></i> はじめに
Ruboty の構成を理解するために、この解説を作ろうと思ったのですが、

* Plugin による拡張などを含めた高レイヤの設計技法
* 適切な命名により、単一責務で分割されたメソッド群
* メモ化、 alias method chain , ..etc など様々な Ruby のイディオム

など、 Ruby の良い作法を **高レイヤの設計技法** から **低レイヤのイディオム** まで幅広く学ぶ素材として
非常に魅力的なソフトウェアであることに気づきました。

そのため、 Ruboty 自体には興味がない方も是非 Ruboty のコードリーディングをしてみることをおすすめします。
他の Rubyist にも読んでもらいたい！ と思ったら拡散をお願いします。

## <i class="fa fa-cube" style="font-size:1em;"></i> 前提
* Ruboty の全体的な構成の概要についてはこちらを参照。
    * [Ruboty | 基本構成図解](http://qiita.com/tbpgr/items/7f08569dd8828bae6527)
* コマンドラインからの Ruboty 起動をエントリポイントとして、そこを起点に説明をします
* `ruboty --generatr` 側の処理については詳細について触れません

## <i class="fa fa-tree" style="font-size:1em;"></i> 構成
ざっと眺めて次に進みましょう。

~~~bash
$ tree
.
┣ bin
┃ ┗ ruboty
┣ CHANGELOG.md
┣ Gemfile
┣ lib
┃ ┣ ruboty
┃ ┃ ┣ action.rb
┃ ┃ ┣ actions
┃ ┃ ┃ ┣ base.rb
┃ ┃ ┃ ┣ help.rb
┃ ┃ ┃ ┣ ping.rb
┃ ┃ ┃ ┗ whoami.rb
┃ ┃ ┣ adapter_builder.rb
┃ ┃ ┣ adapters
┃ ┃ ┃ ┣ base.rb
┃ ┃ ┃ ┗ shell.rb
┃ ┃ ┣ brains
┃ ┃ ┃ ┣ base.rb
┃ ┃ ┃ ┗ memory.rb
┃ ┃ ┣ command_builder.rb
┃ ┃ ┣ commands
┃ ┃ ┃ ┣ base.rb
┃ ┃ ┃ ┣ generate.rb
┃ ┃ ┃ ┗ run.rb
┃ ┃ ┣ env
┃ ┃ ┃ ┣ missing_required_key_error.rb
┃ ┃ ┃ ┣ validatable.rb
┃ ┃ ┃ ┗ validation_error.rb
┃ ┃ ┣ env.rb
┃ ┃ ┣ handlers
┃ ┃ ┃ ┣ base.rb
┃ ┃ ┃ ┣ help.rb
┃ ┃ ┃ ┣ ping.rb
┃ ┃ ┃ ┗ whoami.rb
┃ ┃ ┣ logger.rb
┃ ┃ ┣ message.rb
┃ ┃ ┣ robot.rb
┃ ┃ ┗ version.rb
┃ ┗ ruboty.rb
┣ LICENSE.txt
┣ Rakefile
┣ README.md
┣ ruboty.gemspec
┣ spec
┃ ┣ ruboty
┃ ┃ ┣ adapter_builder_spec.rb
┃ ┃ ┣ adapters
┃ ┃ ┃ ┗ shell_spec.rb
┃ ┃ ┣ commands
┃ ┃ ┃ ┣ generate_spec.rb
┃ ┃ ┃ ┗ run_spec.rb
┃ ┃ ┣ env
┃ ┃ ┃ ┗ validatable_spec.rb
┃ ┃ ┣ handlers
┃ ┃ ┃ ┣ base_spec.rb
┃ ┃ ┃ ┣ help_spec.rb
┃ ┃ ┃ ┣ ping_spec.rb
┃ ┃ ┃ ┗ whoami_spec.rb
┃ ┃ ┗ robot_spec.rb
┃ ┗ spec_helper.rb
┗ templates
    ┗ Gemfile
~~~

## <i class="fa fa-files-o" style="font-size:1em;"></i> bin/ruboty
### <i class="fa fa-code" style="font-size:1em;"></i> bin/ruboty のソースコード
<i class="fa fa-github-square" style="font-size:1em;"></i> [ruboty | ./bin/ruboty](https://github.com/r7kamura/ruboty/blob/master/bin/ruboty)
### <i class="fa fa-pencil" style="font-size:1em;"></i> bin/ruboty について
ruboty コマンドを実行した時のエントリポイントになります
`Ruboty::CommandBuilder.new(ARGV).build.call` でコマンドラインからの呼び出しに対応しています。

例えば、コマンドラインで

~~~bash
$ ruboty --generate
~~~

を呼び出せば、 `ARGV` に `--generate` が引き渡され、以降の処理で
<i class="fa fa-github-square" style="font-size:1em;"></i> [Commands::Generate.call](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/commands/generate.rb#L6-L8) が呼び出され、 ruboty のディレクトリや Gemfile などのひな形生成が実行されることになります。

## <i class="fa fa-files-o" style="font-size:1em;"></i> lib/command_builder.rb
### <i class="fa fa-code" style="font-size:1em;"></i> lib/command_builder.rb のソースコード
<i class="fa fa-github-square" style="font-size:1em;"></i> [ruboty | ./lib/ruboty/command_builder.rb](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/command_builder.rb)
### <i class="fa fa-pencil" style="font-size:1em;"></i> lib/command_builder.rb について
`command_builder` はコマンドラインの操作を受け持ちます。
### <i class="fa fa-gavel" style="font-size:1em;"></i> インスタンスの Build

~~~ruby
def build
  command_class.new(options)
end
~~~

`command_builder` の名前の通り、オプション引数の内容に応じて 
<i class="fa fa-github-square" style="font-size:1em;"></i> [Commands::Generate](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/commands/generate.rb) と <i class="fa fa-github-square" style="font-size:1em;"></i> [Commands::Run](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/commands/run.rb) のインスタンスの生成 = build を行っています。
両 Commands はダックタイピング可能なように call メソッドでインターフェースを揃えてあります。

### <i class="fa fa-file-text" style="font-size:1em;"></i> メモ化
Ruboty ではいたるところで mem gem を利用した メモ化を行っています。

~~~ruby
module Ruboty
  class Robot
    DEFAULT_ENV = "development"
    DEFAULT_ROBOT_NAME = "ruboty"

    include Mem
# 以下略
~~~

Mem module を MixIn しています。
Mem module はメモ化のための gem で、以降のコードでも頻繁に利用されています。
Mem module は <i class="fa fa-github-square" style="font-size:1em;"></i> [mem gem](https://github.com/r7kamura/mem) に含まれる Module であり、こちらも Ruboty と同様 @r7kamura さん製です。
この gem を活用することで、同様の処理が何度も呼ばれる場合の性能を向上させています。

Mem module を MixIn するとインスタンスメソッドとして

* has_memoized?(key)
* memoize(key, value)
* memoized(key)
* memoized_table
* unmemoize(key)

を利用可能になります。
命名が適切なので、内容は想像がつきますね。

memoize クラスマクロに保存するメソッドをシンボルで指定すると、

* `"#{method_name}_with_memoize"` 
* `"unmemoize_#{method_name}"` 

を動的に定義します。
前者は Hash へのメモ化。
後者は Hash に保存した情報の解放（削除）を行います。

`"#{method_name}_with_memoize"` はメモリに結果があれば、メモ化された結果を利用、
なければ処理を実行して、結果をメモ化しています。

そして、直後に ***alias_method_chain*** のイディオムで既存メソッドと、メモ化機能付きの新規メソッドに別名を与えています。
***alias_method_chain*** についてはこちらが簡潔で分かりやすいです。
<i class="fa fa-external-link-square" style="font-size:1em;"></i> [(ﾟ∀ﾟ)o彡 sasata299's blog | rails の alias_method_chain が素晴らしすぎる](http://blog.livedoor.jp/sasata299/archives/51166404.html)

これにより、既存メソッドと動的に追加したメモ化機能付きのメソッドの両方を呼び分けられるようになりました。
`memoize :some_method_name` を設定したことで、既存のメソッドに対して呼び出しを行うとメモ化機能を付加されて、
処理が実行されるようになりました。

@r7kamura さんによる Mem の簡単な説明は以下のページの中程にあります。
<i class="fa fa-external-link-square" style="font-size:1em;"></i> [Railsアプリつくった](http://r7kamura.hatenablog.com/entry/2014/04/06/023344)

### <i class="fa fa-cube" style="font-size:1em;"></i> Option の Parse
<i class="fa fa-github-square" style="font-size:1em;"></i> [Slop gem](https://github.com/leejarvis/slop) を利用しています。
詳細は Slop gem の README をご確認ください。

~~~ruby
def options
  Slop.parse!(arguments, help: true) do
    on("dotenv", "Load .env before running.")
    on("g", "generate", "Generate a new chatterbot with ./ruboty/ directory if specified.")
    on("l", "load=", "Load a ruby file before running.")
  end.to_hash
end
~~~

## <i class="fa fa-files-o" style="font-size:1em;"></i> lib/commands/run.rb
### <i class="fa fa-code" style="font-size:1em;"></i> lib/commands/run.rb のソースコード
<i class="fa fa-github-square" style="font-size:1em;"></i> [ruboty | ./lib/commands/run.rb](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/commands/run.rb)
### <i class="fa fa-pencil" style="font-size:1em;"></i> lib/commands/run.rb について
Robot の `run` メソッドを呼び出しているだけです。

~~~ruby
def call
  Robot.new(options).run
end
~~~

## <i class="fa fa-files-o" style="font-size:1em;"></i> lib/ruboty/robot.rb
### <i class="fa fa-code" style="font-size:1em;"></i> lib/ruboty/robot.rb のソースコード
<i class="fa fa-github-square" style="font-size:1em;"></i> [ruboty | ./lib/ruboty/robot.rb](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/robot.rb)
### <i class="fa fa-pencil" style="font-size:1em;"></i> lib/ruboty/robot.rb について
Ruboty の起動本処理です。
### <i class="fa fa-gear" style="font-size:1em;"></i> デプロイ種別 ( production / development 等 )
Ruboty のデプロイモードの切り替えは ENV['RUBOTY_ENV'] で行われています。
Ruboty::DEFAULT_ENV に、デフォルトのデプロイモードが `development` で設定されています。

### <i class="fa fa-reddit-square" style="font-size:1em;"></i> ロボット名
Ruboty のロボット名は以下の順で決定されます。

1. `ENV["RUBOTY_NAME"]`
1. `ENV["ROBOT_NAME"]`
1. `Ruboty::DEFAULT_ROBOT_NAME`

`Ruboty::DEFAULT_ROBOT_NAME` は、 `ruboty` です。
コメントに `ROBOT_NAME is deprecated.` と書いてあるので, 環境変数 `RUBOTY_NAME` に
好きなロボット名をつけるのが正しい作法ということでしょう。

* or を利用したデフォルト値設定のイディオム
上記の設定の実コードが以下になります。

~~~ruby
def name
  ENV["RUBOTY_NAME"] || ENV["ROBOT_NAME"] || DEFAULT_ROBOT_NAME
end
~~~

値がなかった場合のみ、デフォルト値を指定する場合に

~~~ruby
return specific_value || default_value
~~~

というイディオムがよく利用されます。

### <i class="fa fa-cube" style="font-size:1em;"></i> run
ここから、 Ruboty 起動のメイン処理の始まりです。
`run` メソッドの中は、 抽象度を合わせたメソッド群の集まりになっていて、
低レイヤーの処理はありません。
これだけでも、起動処理中にどんな順で何を行っているか想像が付きます。
[Composed Method Pattern](http://qiita.com/tbpgr/items/06266a4161019b445756) ですね。

~~~ruby
def run
  dotenv
  bundle
  setup
  remember
  handle
  adapt
end
~~~

### <i class="fa fa-cube" style="font-size:1em;"></i> run : dotenv
ruboty コマンド呼び出し時に `--dotenv` オプションを指定していた場合に、
.env ファイルから環境変数をロードする処理です。
dotenv gem については、 <i class="fa fa-github-square" style="font-size:1em;"></i> [こちら](https://github.com/bkeepers/dotenv)を参照

Ruby でよく利用する、後置の `if` によるコンパクトな記法です。

~~~ruby
def dotenv
  Dotenv.load if options[:dotenv]
end
~~~

### <i class="fa fa-cube" style="font-size:1em;"></i> run : bundle
Gemfile に指定した 各種 Plugin などの gem を development や production などの環境に
応じて require しています。

~~~ruby
def bundle
  Bundler.require(:default, env)
rescue Bundler::GemfileNotFound
end
~~~

`rescue Bundler::GemfileNotFound` の部分は、 Gemfile がなかった場合でも
エラーが発生せずに動作が続くようにしています。

ちなみに通常、 Ruby の例外処理については

~~~ruby
begin
  0/0
rescue => e
  puts e.message # => divided by 0
end
~~~

のように、 `begin-rescue-end` の形式で紹介されることが多いですが、
メソッド内の処理のトップレベルで利用する場合は、`begin-end` を省略することで
コード全体のブロックを1階層下げることができます。
これにより、コードの見通しが良くなり、可読性があがります。

書籍 「 <i class="fa fa-book" style="font-size:1em;"></i> [Confident Ruby](https://pragprog.com/book/agcr/confident-ruby)」の中でも <i class="fa fa-external-link-square" style="font-size:1em;"></i> [Prefer top-level rescue clause](http://d.hatena.ne.jp/tbpg/20140514/1400072585) という名前のテクニックとして紹介されています。

### <i class="fa fa-cube" style="font-size:1em;"></i> run : setup
ruboty コマンド呼び出し時に `--load` オプションを指定していた場合に、
対象ファイルをロードする処理です。

~~~ruby
def setup
  load(options[:load]) if options[:load]
end
~~~

### <i class="fa fa-cube" style="font-size:1em;"></i> run : remember
`remember` メソッドは `brain` メソッドを呼び出しているのみです。

~~~ruby
def remember
  brain
end
~~~

brain メソッドは以下の様な処理になっています。

~~~ruby
def brain
  Brains::Base.find_class.new
end
memoize :brain
~~~

<i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::Brains::Base](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/brains/base.rb) クラスの `find_class` メソッドを呼び出し、結果をメモリにメモ化します。
<i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::Brains::Base](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/brains/base.rb) クラスは内部に `@brain_classes` というインスタンス変数を配列形式で保持しています。
この配列は、 <i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::Brains::Base を継承する度に継承したクラスが追加されています。](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/brains/base.rb#L7-L9)
<i class="fa fa-github-square" style="font-size:1em;"></i> [`find_class` は最後に継承されたクラスを返却します。](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/brains/base.rb#L12)
特に何も拡張しなければ、 Ruboty はメモリに Hash として値を保持する <i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::Brains::Memory](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/brains/memory.rb) クラスのインスタンスを生成し、データを記憶します。
Gemfile で <i class="fa fa-github-square" style="font-size:1em;"></i> [ruboty-redis gem](https://github.com/r7kamura/ruboty-redis) のような `Ruboty::Brains::Base` を継承した gem を指定していた場合は、
<i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::Brains::Redis](https://github.com/r7kamura/ruboty-redis/blob/master/lib/ruboty/brains/redis.rb) のインスタンスを生成し、データを記憶します。

### <i class="fa fa-cube" style="font-size:1em;"></i> run : handle
Handler の初期化処理を行います。
Handler は ruboty を利用する際に、メッセージに反応して何かしらの処理を行う個別の機能です。
例えば ping に対して pong を返却する `lib/ruboty/handlers/ping.rb` など。

`handle` メソッドは `handlers` メソッドを呼び出しているのみです。

~~~ruby
def handle
  handlers
end
~~~

以下は `handlers` メソッドの処理です。

~~~ruby
def handlers
  Ruboty.handlers.map {|handler_class| handler_class.new(self) }
end
memoize :handlers
~~~

Ruboty クラスの handlers は以下の様な処理になっています。

* [lib/ruboty.rb の 22-25行目](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty.rb#L22-L25)

~~~ruby
    def handlers
      []
    end
    memoize :handlers
~~~

これだけ見ると、空配列に見えますが既に説明済みの `run : bundle` の実行時に `Bundler.require(:default, env)` で読み込まれた
Handler Plugin にからくりがあります。

Ruboty の Handler は <i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::Handlers::Base](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/handlers/base.rb) を継承して作成されます。
Ruboty::Handlers::Base は継承時に <i class="fa fa-github-square" style="font-size:1em;"></i> [継承先のクラスを Ruboty の handlers 配列に追加しています。](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/handlers/base.rb#L8)

つまり、

~~~ruby
Ruboty.handlers.map {|handler_class| handler_class.new(self) }
~~~

という処理は、 組み込みの Handler Plugin (ping, whoami, help) と `Bundler.require(:default, env)` で読み込まれた
Gemfile に指定した全ての Hanlder Plugin のインスタンスを生成して、 Ruboty::Brain を継承したクラスのメモリに記憶し、
`Ruboty::Robot#handlers` で取得可能にしています。

### <i class="fa fa-cube" style="font-size:1em;"></i> run : adapt
adapte は Ruboty が動作するチャットツールのアダプタをロードする処理です。
adapter の層があることで、様々んチャットクライアントに対応することができています。

`adapt` メソッドは `adapter` メソッドで Gemfile に指定した Adapter のインスタンスを取得し、
`run` メソッドを呼び出しているのみです。

~~~ruby
def adapt
  adapter.run
end
~~~

`adapter` メソッドの内容は以下です。

~~~ruby
def adapter
  AdapterBuilder.new(self).build
end
memoize :adapter
~~~

<i class="fa fa-github-square" style="font-size:1em;"></i> [Ruboty::AdapterBuilder](https://github.com/r7kamura/ruboty/blob/master/lib/ruboty/adapter_builder.rb) からインスタンスを取得してメモリに記憶しています。
インスタンス取得の処理は以下です。

~~~ruby
def build
  adapter_class.new(robot)
end
~~~

`adapter_class` メソッドの本体は以下です。

~~~
def adapter_class
  self.class.adapter_classes.last
end
~~~

仕組みは Handlers と同じなので説明を割愛します。

## 総括
以上で、 Ruboty の起動の仕組みをソースコードから読み取ることが出来ました。
また、下記のような点を学び取ることができました。

### 設計手法
* gem を利用した Handler / Adapter / Brain の Plugin 拡張のための仕組み
* mem gem を利用したメモ化
* slop を利用した option の parse
* dotenv を利用した環境変数による設定

### イディオム
* alias_method_chain
* 後置の if
* Bundle.require を用いた gem の一括 require
* Prefer top-level rescue clause を利用した例外処理
* or を利用したデフォルト値設定
* compose method pattern を利用し、抽象度を揃えつつ、単一機能で細かく別れたメソッド群

### その他
* 設計に一貫性があるため、類似コードを見かけると処理を類推しやすく、すぐに理解しやすい
* 命名が適切なため、コメントが非常に少ないが問題なく読むことができる

## <i class="fa fa-arrow-circle-right" style="font-size:1em;"></i> 参照
* [Ruboty | 基本構成図解](http://qiita.com/tbpgr/items/7f08569dd8828bae6527)
* <i class="fa fa-github-square" style="font-size:1em;"></i> [octokit.rb](https://github.com/octokit/octokit.rb)
* <i class="fa fa-github-square" style="font-size:1em;"></i> [mem gem](https://github.com/r7kamura/mem)
* <i class="fa fa-external-link-square" style="font-size:1em;"></i> [(ﾟ∀ﾟ)o彡 sasata299's blog | rails の alias_method_chain が素晴らしすぎる](http://blog.livedoor.jp/sasata299/archives/51166404.html)