お題
「アプリ実行中だけ機能する簡易Key/Valueストアを各プログラミング言語で実装してみる。」というお題で、前回はRuby、Golang、Java、Scala、Kotlin、Pythonで簡単なコンソールアプリを作ってみた。
簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ
前回はKey/Value情報をオンメモリで保持していたので、アプリを終了すると情報が消えるという実用に耐えない仕様だった。
今回は上記言語のうち、まずRubyをピックアップして、Key/Value情報をJSONファイルとしてローカルに保存することにした。
かつ、前回はmain.rb
1ファイルに全ロジックをまとめていたけど、今回は多少拡張性を考慮して「コマンドパターン」(というと各方面からマサカリが飛んできそうだけど)を採用してソースファイルを分割することにした。
[2020/09/18 update]
コメントもらったのでJSONファイルへの読み書き部分をmodule化。
ついでに、Rubyのバージョンも2.7.1にupdate
試行Index
- 第1回:簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ
- 第2回:【改善編】簡単なツール作成を通してRubyを学ぶ
- 第3回:【改善編】簡単なツール作成を通してPython3を学ぶ
- 第4回:【改善編】簡単なツール作成を通してGolangを学ぶ
- 第5回:【改善編】簡単なツール作成を通してJavaを学ぶ
- 第6回:【改善編】簡単なツール作成を通してScalaを学ぶ
- 第7回:簡単なツール作成を通してRustを学ぶ
- 第8回:【改善編】簡単なツール作成を通してRustを学ぶ
実装・動作確認端末
# 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]アプリ起動エントリーポイント
# 同階層にあるファイルを読む時は「./」で始める。
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]ストア情報の管理
# アルファベット大文字で始まると定数扱い
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]各コマンドの管理
# 同階層にあるファイルを読む時は「./」で始める。
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]各コマンドの親クラス
class Command
def exec(*args)
raise NotImplementedError.new
end
end
Rubyの場合は型が同じでなくてもハッシュに詰められる、かつ、同じ名前でメソッド定義しておけばコールできるんだっけ?
とするなら、このクラスは不要だね・・・。
各コマンドクラス
各コマンドについては、ストア情報がオンメモリのハッシュからJSONに変わった点以外はやることは一緒。(なので説明省く。)
■保存
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件取得
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
■全件取得
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件削除
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
■全件削除
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
コマンドについて追記するの忘れた。
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
■アプリ終了
require './command'
class End < Command
def exec(*args)
puts "End!"
exit
end
end
まとめ
エラー処理などの考慮は漏れ漏れだけど、ひとまずこれで動いてる。
前回と違って実行パスに「store.json
」が吐かれるので、保存した情報がアプリ終了時に消えることはない。
しかし、拡張はしやすくなったはずだけど、まあ、冗長になったね・・・。
次は、他の言語でも同じような構造で改善してみよう。