2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-04

お題

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

簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ

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

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

試行Index

実装・動作確認端末

# 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#要件

ソース全量

解説

全ソースファイル

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

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

[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

・コンソールでユーザ入力を待ち受けるくだりは前回と同じ。

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

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

require './store'

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

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

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

cmd.to_sym

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

[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では定数扱いになるようだ。

DEFAULT_STORE_NAME = 'store.json'

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

def initialize(storeName = DEFAULT_STORE_NAME)

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

@storeName = storeName

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

[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

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

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

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

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

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

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

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

各コマンドクラス

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

■保存

[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

■1件取得

[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

■全件取得

[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

■1件削除

[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

■全件削除

[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コマンドについて追記するの忘れた。

[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

■アプリ終了

[end.rb]
require './command'

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

まとめ

エラー処理などの考慮は漏れ漏れだけど、ひとまずこれで動いてる。
前回と違って実行パスに「store.json」が吐かれるので、保存した情報がアプリ終了時に消えることはない。
しかし、拡張はしやすくなったはずだけど、まあ、冗長になったね・・・。
次は、他の言語でも同じような構造で改善してみよう。

2
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?