お題
前回、Javaで書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のScala版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ」からの改善版。
試行Index
- 第1回:簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ
- 第2回:【改善編】簡単なツール作成を通してRubyを学ぶ
- 第3回:【改善編】簡単なツール作成を通してPython3を学ぶ
- 第4回:【改善編】簡単なツール作成を通してGolangを学ぶ
- 第5回:【改善編】簡単なツール作成を通してJavaを学ぶ
- 第6回:【改善編】簡単なツール作成を通してScalaを学ぶ
- 第7回:簡単なツール作成を通してRustを学ぶ
- 第8回:【改善編】簡単なツール作成を通してRustを学ぶ
実装・動作確認端末
# 言語バージョン
$ scala -version
Scala code runner version 2.11.12 -- Copyright 2002-2017, LAMP/EPFL
# IDE - IntelliJ IDEA
IntelliJ IDEA 2019.2 (Ultimate Edition)
Build #IU-192.5728.98, built on July 23, 2019
実践
要件
アプリを起動すると、キーバリュー形式でテキスト情報をJSONファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば第1回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件
ソース全量
解説
全ソースファイル
scalaソース | 説明 |
---|---|
Main.scala | アプリ起動エントリーポイント |
StoreInfo.scala | キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。 現状は「ファイル名」だけ保持 |
Commands.scala | キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。 コマンドの増減に関する影響は、このソースに閉じる。 |
Command.scala | 各コマンドに共通のインタフェース |
SaveCommand.scala | キーバリュー情報の保存を担う。 |
GetCommand.scala | 指定キーに対するバリューの取得を担う。 |
ListCommand.scala | 全キーバリュー情報の取得を担う。 |
RemoveCommand.scala | 指定キーに対するバリューの削除を担う。 |
ClearCommand.scala | 全キーバリュー情報の削除を担う。 |
HelpCommand.scala | ヘルプ情報の表示を担う。 |
EndCommand.scala | アプリの終了を担う。 |
[Main.scala]アプリ起動エントリーポイント
[Main.scala]
object Main extends App {
val commands = Commands(StoreInfo("store.json"))
println("Start!")
while (true) {
commands.exec(io.StdIn.readLine().split(" "))
}
}
[StoreInfo.scala]ストア情報の管理
[StoreInfo.scala]
// privateにすることで new による生成を抑止
class StoreInfo private(val storeName: String = "store.json")
// コンパニオンオブジェクト(StoreInfoクラスのコンストラクタを隠蔽するファクトリ)
object StoreInfo {
def apply(storeName: String): StoreInfo = new StoreInfo(storeName)
}
[Commands.scala]各コマンドの管理
[Commands.scala]
import java.nio.file.{Files, Paths}
// privateにすることで new による生成を抑止
class Commands private(commands: Map[String, Command]) {
def exec(cmds: Array[String]): Unit = {
if (commands.contains(cmds.head)) {
commands(cmds.head).exec(cmds.tail: _*) // 可変長引数の定義に対して Array<String> を渡す際は _* を使う
} else {
commands("help").exec(null)
}
}
}
// コンパニオンオブジェクト(Commandsクラスのコンストラクタを隠蔽するファクトリ)
object Commands {
def apply(storeInfo: StoreInfo): Commands = {
if (Files.notExists(Paths.get(storeInfo.storeName))) {
ClearCommand(storeInfo).exec(null)
}
new Commands(
Map(
"end" -> EndCommand(),
"help" -> HelpCommand(),
"clear" -> ClearCommand(storeInfo),
"save" -> SaveCommand(storeInfo),
"get" -> GetCommand(storeInfo),
"remove" -> RemoveCommand(storeInfo),
"list" -> ListCommand(storeInfo)
)
)
}
}
[Command.scala]各コマンドの親クラス
[Command.scala]
trait Command {
def exec(args: String*): Unit
}
[Helper.scala]他の言語での事例では作っていなかったヘルパークラス
JSONファイル読み書きのところがあまりにも冗長だったので trait として切り出した。
[Helper.scala]
import java.nio.file.{Files, Paths}
import com.lambdaworks.jacks.JacksMapper
import scala.util.Try
trait Helper {
def getJson(storeName: String): Try[Map[String, String]] = Try(JacksMapper.readValue[Map[String, String]](Files.readAllLines(Paths.get(storeName)).get(0)))
def writeJson(storeName: String, jsonMap: Map[String, String]): Try[Unit] = Try(Files.write(Paths.get(storeName), JacksMapper.writeValueAsString(jsonMap).getBytes()))
}
各コマンドクラス
各コマンドについては、ストア情報がオンメモリのハッシュからJSONに変わった点以外はやることは一緒。(なので説明省く。)
■保存
[SaveCommand.scala]
import scala.util.{Failure, Success}
// privateにすることで new による生成を抑止
class SaveCommand private(storeInfo: StoreInfo) extends Command with Helper {
override def exec(args: String*): Unit = {
if (args.size > 1) {
getJson(storeInfo.storeName) match {
// 既存の json に、 + (key -> value) でコンソール入力されたキーバリューが追加される
case Success(json) => writeJson(storeInfo.storeName, json + (args(0) -> args(1))) match {
case Success(_) => // 何もしないが、case 句は必要
case Failure(e) => println(e.getMessage)
}
case Failure(e) => println(e.getMessage)
}
}
}
}
// コンパニオンオブジェクト(SaveCommandクラスのコンストラクタを隠蔽するファクトリ)
object SaveCommand {
def apply(storeInfo: StoreInfo): Command = new SaveCommand(storeInfo)
}
■1件取得
[GetCommand.scala]
import scala.util.{Failure, Success}
// privateにすることで new による生成を抑止
class GetCommand private(storeInfo: StoreInfo) extends Command with Helper {
override def exec(args: String*): Unit = {
if (args.size > 0) {
getJson(storeInfo.storeName) match {
case Success(json) => {
if (json.contains(args(0))) {
println(json(args(0)))
}
}
case Failure(e) => println(e.getMessage)
}
}
}
}
// コンパニオンオブジェクト(GetCommandクラスのコンストラクタを隠蔽するファクトリ)
object GetCommand {
def apply(storeInfo: StoreInfo): Command = new GetCommand(storeInfo)
}
■全件取得
[ListCommand.scala]
import scala.util.{Failure, Success}
// privateにすることで new による生成を抑止
class ListCommand private(storeInfo: StoreInfo) extends Command with Helper {
override def exec(args: String*): Unit = {
getJson(storeInfo.storeName) match {
case Success(json) =>
println("\"key\",\"value\"")
json.foreach { case (k, v) => println("\"%s\",\"%s\"".format(k, v)) }
case Failure(e) => println(e.getMessage)
}
}
}
// コンパニオンオブジェクト(ListCommandクラスのコンストラクタを隠蔽するファクトリ)
object ListCommand {
def apply(storeInfo: StoreInfo): Command = new ListCommand(storeInfo)
}
■1件削除
[RemoveCommand.scala]
import scala.util.{Failure, Success}
// privateにすることで new による生成を抑止
class RemoveCommand private(storeInfo: StoreInfo) extends Command with Helper {
override def exec(args: String*): Unit = {
if (args.size > 0) {
getJson(storeInfo.storeName) match {
// 既存の json に、 - key でコンソール入力されたキーが削除される
case Success(json) => writeJson(storeInfo.storeName, json - args(0)) match {
case Success(_) => // 何もしないが、case 句は必要
case Failure(e) => println(e.getMessage)
}
case Failure(e) => println(e.getMessage)
}
}
}
}
// コンパニオンオブジェクト(RemoveCommandクラスのコンストラクタを隠蔽するファクトリ)
object RemoveCommand {
def apply(storeInfo: StoreInfo): Command = new RemoveCommand(storeInfo)
}
■全件削除
[ClearCommand.scala]
import scala.util.{Failure, Success}
// privateにすることで new による生成を抑止
class ClearCommand private(storeInfo: StoreInfo) extends Command with Helper {
override def exec(args: String*): Unit = {
// Helperトレイトに持たせたJSONファイルへの書き込みロジックを使用
writeJson(storeInfo.storeName, Map[String, String]()) match {
case Success(_) => // 何もしないが、case 句は必要
case Failure(e) => println(e.getMessage)
}
}
}
// コンパニオンオブジェクト(ClearCommandクラスのコンストラクタを隠蔽するファクトリ)
object ClearCommand {
def apply(storeInfo: StoreInfo): Command = new ClearCommand(storeInfo)
}
■ヘルプ
[HelpCommand.scala]
// privateにすることで new による生成を抑止
class HelpCommand private extends Command {
override def exec(args: String*): Unit = println(
"""
|[usage]
|キーバリュー形式で文字列情報を管理するコマンドです。
|以下のサブコマンドが利用可能です。
|
|save ... keyとvalueを渡して保存します。
|get ... keyを渡してvalueを表示します。
|remove ... keyを渡してvalueを削除します。
|list ... 保存済みの内容を一覧表示します。
|clear ... 保存済みの内容を初期化します。
|help ... ヘルプ情報(当内容と同じ)を表示します。
|end ... アプリを終了します。
|
|""".stripMargin)
}
// コンパニオンオブジェクト(HelpCommandクラスのコンストラクタを隠蔽するファクトリ)
object HelpCommand {
def apply(): Command = new HelpCommand()
}
■アプリ終了
[EndCommand.scala]
// privateにすることで new による生成を抑止
class EndCommand private() extends Command {
override def exec(args: String*): Unit = {
println("End!")
sys.exit(-1)
}
}
// コンパニオンオブジェクト(EndCommandクラスのコンストラクタを隠蔽するファクトリ)
object EndCommand {
def apply(): Command = new EndCommand()
}
まとめ
Javaライブラリを使っている箇所以外で if 文が出てくる時点でScalaらしい書き方になっていないらしい。。。
やはり、Scalaは難しい・・・。