Help us understand the problem. What is going on with this article?

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

お題

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

試行Index

実装・動作確認端末

# 言語バージョン

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

ソース全量

https://github.com/sky0621/book_scala/tree/v0.2.0

解説

全ソースファイル

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は難しい・・・。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away