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

# お題
[前回](https://qiita.com/sky0621/items/f19f5191754f61a4d4c3)、Javaで書いた簡易ツール「ローカルJSONファイルに保存するキーバリューストア」のScala版。
シリーズ物としては、もともと複数言語で同じ内容のプログラムを書いて比較した「[簡単なツール作成を通して各プログラミング言語を比較しつつ学ぶ](https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff)」からの改善版。

# 試行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)

# 実装・動作確認端末
### # 言語バージョン
```
$ 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ファイルに保存する機能を持つコンソールアプリ。
オンメモリで保持していた点だけ除けば第１回と同じ仕様なので詳細は以下参照。
https://qiita.com/sky0621/items/32c87aed41cb1c3c67ff#要件

### ソース全量
https://github.com/sky0621/book_scala/tree/v0.2.0

## 解説

### 全ソースファイル

|scalaソース|説明|
|---|---|
|Main.scala|アプリ起動エントリーポイント|
|StoreInfo.scala|キーバリュー情報を保存するストア(JSONファイル)に関する情報を扱う。<br>現状は「ファイル名」だけ保持|
|Commands.scala|キーバリューストアからの情報取得や保存、削除といった各コマンドを管理。<br>コマンドの増減に関する影響は、このソースに閉じる。|
|Command.scala|各コマンドに共通のインタフェース|
|SaveCommand.scala|キーバリュー情報の保存を担う。|
|GetCommand.scala|指定キーに対するバリューの取得を担う。|
|ListCommand.scala|全キーバリュー情報の取得を担う。|
|RemoveCommand.scala|指定キーに対するバリューの削除を担う。|
|ClearCommand.scala|全キーバリュー情報の削除を担う。|
|HelpCommand.scala|ヘルプ情報の表示を担う。|
|EndCommand.scala|アプリの終了を担う。|

### [Main.scala]アプリ起動エントリーポイント

```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]ストア情報の管理

```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]各コマンドの管理

```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]各コマンドの親クラス

```scala:[Command.scala]
trait Command {
  def exec(args: String*): Unit
}
```

### [Helper.scala]他の言語での事例では作っていなかったヘルパークラス

JSONファイル読み書きのところがあまりにも冗長だったので trait として切り出した。

```scala:[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に変わった点以外はやることは一緒。（なので説明省く。）

#### ■保存

```scala:[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)
}
```

#### ■１件取得

```scala:[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)
}
```

#### ■全件取得

```scala:[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)
}
```

#### ■１件削除

```scala:[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)
}
```

#### ■全件削除

```scala:[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)
}
```

#### ■ヘルプ

```scala:[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()
}
```

#### ■アプリ終了

```scala:[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は難しい・・・。