【改善編】簡単なツール作成を通してRubyを学ぶ

# お題
「アプリ実行中だけ機能する簡易Key/Valueストアを各プログラミング言語で実装してみる。」というお題で、前回はRuby、Golang、Java、Scala、Kotlin、Pythonで簡単なコンソールアプリを作ってみた。

[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)

前回はKey/Value情報をオンメモリで保持していたので、アプリを終了すると情報が消えるという実用に耐えない仕様だった。
今回は上記言語のうち、まずRubyをピックアップして、Key/Value情報をJSONファイルとしてローカルに保存することにした。
かつ、前回は`main.rb`１ファイルに全ロジックをまとめていたけど、今回は多少拡張性を考慮して「コマンドパターン」（というと各方面からマサカリが飛んできそうだけど）を採用してソースファイルを分割することにした。

[2020/09/18 update]
コメントもらったのでJSONファイルへの読み書き部分をmodule化。
ついでに、Rubyのバージョンも2.7.1にupdate

# 試行Index
- 第１回：[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)
- 第２回：[【改善編】簡単なツール作成を通してRubyを学ぶ](https://qiita.com/sky0621/items/5868b19934187571d0cd)
- 第３回：[【改善編】簡単なツール作成を通してPython3を学ぶ](https://qiita.com/sky0621/items/27c610e08391c7cc83ea)
- 第４回：[【改善編】簡単なツール作成を通してGolangを学ぶ](https://qiita.com/sky0621/items/cb14aa67ef327922c0a1)
- 第５回：[【改善編】簡単なツール作成を通してJavaを学ぶ](https://qiita.com/sky0621/items/f19f5191754f61a4d4c3)
- 第６回：[【改善編】簡単なツール作成を通してScalaを学ぶ](https://qiita.com/sky0621/items/a68ae8b64d4856d1a195)
- 第７回：[簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/86d9159bc1930618162c)
- 第８回：[【改善編】簡単なツール作成を通してRustを学ぶ](https://qiita.com/sky0621/items/a70cc7a9453795e0c9e9)

# 実装・動作確認端末
### # OS - Linux(Ubuntu)
```
$ cat /etc/os-release 
NAME="Ubuntu"
VERSION="18.04.5 LTS (Bionic Beaver)"
```

### # 言語バージョン
```
$ ruby -v
ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux]
```

### # IDE - RubyMine
```
RubyMine 2020.2.2
Build #RM-202.7319.53, built on September 15, 2020
```

# 実践

## 要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば前回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件

### ソース全量
https://github.com/sky0621/book_rb/tree/v0.4.0

## 解説

### 全ソースファイル

|Rubyソース|説明|
|---|---|
|main.rb|アプリ起動エントリーポイント|
|store.rb|キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。|
|commands.rb|キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。<br>コマンドの増減に関する影響は、このソースに閉じる。|
|command.rb|各コマンドに共通のインタフェース(と言いたいがRubyにはインタフェースが無いので普通のclass)|
|save.rb|キーバリュー情報の保存を担う。|
|get.rb|指定キーに対するバリューの取得を担う。|
|list.rb|全キーバリュー情報の取得を担う。|
|remove.rb|指定キーに対するバリューの削除を担う。|
|clear.rb|全キーバリュー情報の削除を担う。|
|help.rb|ヘルプ情報の表示を担う。|
|end.rb|アプリの終了を担う。|

### [main.rb]アプリ起動エントリーポイント

```ruby:[main.rb]
# 同階層にあるファイルを読む時は「./」で始める。
require './store'
require './commands'

puts "Start!"

commands = Commands.new(StoreInfo.new('store.json'))

loop do
  # 改行コードが含まれるので削る。
  cmd, *args = gets.chomp.split /\s+/
  commands.exec(cmd.to_sym, *args)
end
```

・コンソールでユーザ入力を待ち受けるくだりは[前回](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#ruby-3)と同じ。

```ruby
loop do
　〜〜〜　ここの処理が（明示的に終了しない限り）無限ループ　〜〜〜
end
```

・同じディレクトリ階層にあるRubyソースを読み込む時は「 `./` 」が必須だったのはプチハマりした。

```ruby
require './store'
```

・`Commands`クラスにて各コマンドオブジェクトの生成と実行は行う。そのために必要なストア情報も`Commands`に渡しておく。

```ruby
commands = Commands.new(StoreInfo.new('store.json'))
```

・各コマンドは`Commands`クラスでハッシュ管理しておりシンボルをキーとしているので、ユーザ入力のコマンド文字列はシンボルに変換して渡す。

```ruby
cmd.to_sym
```

### [store.rb]ストア情報の管理

```ruby:[store.rb]
# アルファベット大文字で始まると定数扱い
DEFAULT_STORE_NAME = 'store.json'

class StoreInfo
  # オブジェクト生成時に自動で呼ばれるメソッド（コンストラクタ扱い）
  # デフォルト引数が指定できる
  # インスタンス変数は外部から直接参照することはできない
  def initialize(storeName = DEFAULT_STORE_NAME)
    # 「@xxxx」がインスタンス変数
    @storeName = storeName
  end

  def getName
    @storeName
  end
end

module Store
  def load
    return open(@storeInfo.getName, 'r') do |io|
      JSON.load(io)
    end
  end

  def dump(json_data)
    open(@storeInfo.getName, 'w') do |io|
      JSON.dump(json_data, io)
    end
  end
end
```

・アルファベット大文字で始まるとRubyでは定数扱いになるようだ。

```ruby
DEFAULT_STORE_NAME = 'store.json'
```

・クラス内に`initialize`という名前のメソッドを用意すると、オブジェクト生成時に自動で呼ばれるらしい。

```ruby
def initialize(storeName = DEFAULT_STORE_NAME)
```

・クラス内で明示的にフィールドを設けておかなくとも `@xxxx` として変数に値を格納するコードを書くとインスタンス変数になるらしい。

```ruby
@storeName = storeName
```

### [commands.rb]各コマンドの管理

```ruby:[commands.rb]
# 同階層にあるファイルを読む時は「./」で始める。
require './store'
require './end'
require './help'
require './clear'
require './save'
require './get'
require './remove'
require './list'

class Commands
  def initialize(storeInfo)
    @commands = {
        end: End.new,
        help: Help.new,
        clear: Clear.new(storeInfo),
        save: Save.new(storeInfo),
        get: Get.new(storeInfo),
        remove: Remove.new(storeInfo),
        list: List.new(storeInfo),
    }
    # コマンド群の初期化時に実行するよう修正
    if !File.exists?(storeInfo.getName)
      @commands[:clear].exec
    end
  end

  def exec(cmd, *args)
    if @commands[cmd].nil?
      puts "no target"
      return
    end
    @commands[cmd].exec(*args)
  end
end
```

・事前にシンボルをキーとしてコマンド群を引き当てるハッシュを作っておく。

```ruby
    @commands = {
        end: End.new,
　　　　　　〜〜　省略　〜〜
        list: List.new(storeInfo),
    }
```

・デフォルトではストアファイルが存在しないのでアプリ起動時に作成しておく。

```ruby
    if !File.exists?(storeInfo.getName)
      @commands[:clear].exec      <--- これにより空ファイルが作成される。
    end
```

### [command.rb]各コマンドの親クラス

```ruby:[command.rb]
class Command
  def exec(*args)
    raise NotImplementedError.new
  end
end
```

Rubyの場合は型が同じでなくてもハッシュに詰められる、かつ、同じ名前でメソッド定義しておけばコールできるんだっけ？
とするなら、このクラスは不要だね・・・。

### 各コマンドクラス

各コマンドについては、ストア情報がオンメモリのハッシュからJSONに変わった点以外はやることは一緒。（なので説明省く。）

#### ■保存

```ruby:[save.rb]
require './command'
require './store'

class Save < Command
  include Store

  def initialize(storeInfo)
    @storeInfo = storeInfo
  end

  def exec(*args)
    if args.size != 2
      puts "not valid"
      return
    end

    json_data = load
    json_data[args[0]] = args[1]
    dump(json_data)
  end
end
```

#### ■１件取得

```ruby:[get.rb]
require './command'
require './store'

class Get < Command
  include Store

  def initialize(storeInfo)
    @storeInfo = storeInfo
  end

  def exec(*args)
    if args.size != 1
      puts "not valid"
      return
    end

    puts load[args[0]]
  end
end
```

#### ■全件取得

```ruby:[list.rb]
require './command'
require './store'

class List < Command
  include Store
  
  def initialize(storeInfo)
    @storeInfo = storeInfo
  end

  def exec(*args)
    puts '"key","value"'
    load.each {|k, v| puts %("#{k}","#{v}")}
  end
end
```

#### ■１件削除

```ruby:[remove.rb]
require './command'
require './store'

class Remove < Command
  include Store
  
  def initialize(storeInfo)
    @storeInfo = storeInfo
  end

  def exec(*args)
    if args.size != 1
      puts "not valid"
      return
    end

    json_data = load
    json_data.delete(args[0])
    dump(json_data)
  end
end
```

#### ■全件削除

```ruby:[clear.rb]
require 'json'
require './command'
require './store'

class Clear < Command
  include Store
  
  def initialize(storeInfo)
    @storeInfo = storeInfo
  end

  def exec(*args)
    dump(Hash.new)
  end
end
```

#### ■ヘルプ

あ、`clear`コマンドについて追記するの忘れた。

```ruby:[help.rb]
require './command'

class Help < Command
  def exec(*args)
    puts <<~EOB

    [usage]
    キーバリュー形式で文字列情報を管理するコマンドです。
    以下のサブコマンドが利用可能です。

    list   ... 保存済みの内容を一覧表示します。
    save   ... keyとvalueを渡して保存します。
    get    ... keyを渡してvalueを表示します。
    remove ... keyを渡してvalueを削除します。
    help   ... ヘルプ情報（当内容と同じ）を表示します。

    EOB
  end
end
```

#### ■アプリ終了

```ruby:[end.rb]
require './command'

class End < Command
  def exec(*args)
    puts "End!"
    exit
  end
end
```

# まとめ
エラー処理などの考慮は漏れ漏れだけど、ひとまずこれで動いてる。
[前回](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#ソース全量20190803コメントにならって修正)と違って実行パスに「`store.json`」が吐かれるので、保存した情報がアプリ終了時に消えることはない。
しかし、拡張はしやすくなったはずだけど、まあ、冗長になったね・・・。
次は、他の言語でも同じような構造で改善してみよう。