Android
Kotlin
設計

データベースと画面間で必要になった、コード値の変換をDTOクラスに任せました。

おはようございます。@naokiurです。

引き続き、以前から興味があった、KotlinでAndroid開発を、
こっそりやっています。

課題が出て、解決案を考えたので、
ケーススタディの一例として、書き留めておきたいと思います。

課題

ステータスを表す値の、文字列とBooleanの変換、どこでやる?

経緯

  1. タスク管理アプリを作成している
  2. チェックボックスでTODOを DOINGからDONEへステータスを変えたい!
  3. SQLiteのテーブルに Statusカラムを追加したぜ!
  4. SQLiteはBoolean型がないので、コード値の文字列、 0, 1で管理するぜ!
  5. あ…AndroidのCheckBox Viewは、Booleanか…
  6. テーブルから取得した際…変換しないと…変換して…テーブルに入れないと…
  7. あれ…どこで…? 誰が…?

環境

  • MacBook Pro (Retina 13-inch、Early 2015)
  • macOS High Sierra Capitan 10.13.2
  • Kotlin 1.2
  • Android Studio 3.1.2

目標

いい感じのクラスに、変換のロジックの責務をお願いする

結果

案3を採用、
いい感じのクラスに責務を負わせることができ、
また、読みやすくなったと感じ、目標が達成できたと感じました。

現状

前回、こそこそ作成しているタスク管理アプリに、
タスクを追加する機能を実装する、という記事を書きました。

そこでは、 残念なことですが 以下のように、コメントがないと なんで0を入れているのかがわからないソースコードになってしまっています。

AddActivity.kt
save_button.setOnClickListener { view ->
    val task = TaskModel(
        task_name.text.toString(),
            "0", // 初期値を `Doing`とする
        beginDate,
        limitDate,
        Calendar.getInstance().timeInMillis
    )
    val result = taskDatabaseHelper.insertTask(task)

    if (result) {
        Toast.makeText(this, "new Task Added!", LENGTH_LONG).show()
    }

}

また、登録したタスクを表示する機能とActivity、
CheckBox Viewをクリックした際に、ステータスを更新する機能を、暫定的に作成しました。

TasksDatabaseHelper.kt
    fun updateCheck(id: Long, status: String): Boolean {
        val db = writableDatabase

        val values = ContentValues()
        values.put(DBContract.TaskEntry.STATUS, status)

        db.update(
                DBContract.TaskEntry.TABLE_NAME,
                values,
                DBContract.TaskEntry.ID + " = ?",
                arrayOf(id.toString())
        )

        return true
    }

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val taskDatabaseHelper = TasksDatabaseHelper(this)
        val taskList:ArrayList<TaskModel> = taskDatabaseHelper.selectAll()

        // 暫定的になので、1つめのレコードを、画面に表示する
        if (taskList.size != 0) {
            val  firstTask = taskList[0]

            first_task_name.text = firstTask.name
//            first_status.isChecked = firstTask.status 

            first_status.setOnClickListener { _ ->
//                taskDatabaseHelper.updateCheck(firstTask.id, first_status.isChecked)

            }
        }

Doingのとき、CheckBox Viewはチェックなし(isChecked == false)、
Doneのとき、CheckBox Viewはチェックあり(isChecked == true)としたいところですが、
上記の通り、SQLiteはBoolean型がなく、コード値で管理しているため、
当然ながら、 first_status.isChecked = firstTask.statusは、エラーです。

また、TasksDatabaseHelper.kt#updateCheckは、idとstatus(String)を渡すようにした上に、
同様にCheckBox ViewのisCheckedはBooleanなので、
当然ながら、 first_status.isChecked = firstTask.statusは、エラーです。

解決案

案1 if文を追加し、変換する

愚直にif文を追加し、変換します。

MainActivity.kt
        // 暫定的になので、1つめのレコードを、画面に表示する
        if (taskList.size != 0) {
            val  firstTask = taskList[0]

            first_task_name.text = firstTask.name
            first_status.isChecked = firstTask.status == "1" // firstTask.statusが'0'ならfalse, '1'ならtrue 

            // firstTask.statusが'0'ならfalse, '1'ならtrue 
            first_status.setOnClickListener { _ ->

                if (first_status.isChecked) {
                    taskDatabaseHelper.updateCheck(firstTask.id, "1")

                } else {
                    taskDatabaseHelper.updateCheck(firstTask.id, "0")

                }
            }
        }

よくなったところ

  • エラーがなくなった。

アレなところ

  • 使用する度に、if分岐を追加しなければならず、ロジックが分散する。
  • 相変わらずコメントがないと なんで0を入れているのかがわからないソースコードである。
    • 定数化すれば、まだマシになる。
      • けれど、Activityにその定数を持つことになってしまい、複数Activityでステータスを用いる場合、定数が乱立する。
        • グローバル定数的なクラスが必要になる。

タスク管理アプリでは、今のところこの2箇所で済んでいますが、
もっと複雑なアプリの場合、こういった箇所がたくさん出てきてしまうと考えられ、
読みにくい場所が乱立してしまうと思われます。
あんまりよくないですね。

案2 Converterクラスを作成し、変換する

TaskModelクラスのStatusのための、StatusConverterクラスを作成し、
変換する責務を負ってもらいます。

StatusConverter.kt
class StatusConverter {
    companion object {
        private const val DOING_CODE: String = "0"
        private const val DONE_CODE: String = "1"

        fun toCode(isDone: Boolean): String {
            return if (isDone) DONE_CODE else DOING_CODE
        }

        fun toIsDone(code: String): Boolean {
            return code == DONE_CODE
        }
    }
}
MainActivity.kt
        // 暫定的になので、1つめのレコードを、画面に表示する
        if (taskList.size != 0) {
            val  firstTask = taskList[0]

            first_task_name.text = firstTask.name
            first_status.isChecked = StatusConverter.toIsDone(firstTask.status)

            first_status.setOnClickListener { _ ->
                taskDatabaseHelper.updateCheck(firstTask.id, StatusConverter.toCode(first_status.isChecked))
            }
        }

よくなったところ

  • ロジックが集約された。

アレなところ

  • TaskModelクラスとStatusConverterクラスの紐付きを、意識しなければならない。
  • 定数化したけれど、どっちがtrueでどっちがfalseだっけ、が、ひと目ではわかりにくい。

比較的、MainActivity.kt内の記述量が減ったので、
読みやすくはなったと思います。

ただ、案1の問題と同様ですが、
複雑なアプリの場合、コード値を持つものがたくさん出てくると考えられ、
その際は、Converterクラスが乱立、
紐付きが見えにくくなり、読みにくいソースになってしまうと思われます。

案3 DTOであるModelクラスにInner Enumクラスを作成し、変換する。

良さげ、と思っているものです。
個人的に、(Java触ってたときから)Enum大好き人間である、という、ある種趣味全開なものです。

Inner Enumクラスを作成し、
変換する責務を負ってもらいます。

TaskModel.kt
class TaskModel(val name: String, val status: String, val beginData: Long, val endDate: Long, val updateTime: Long) {
    var id: Long = 0
    constructor(id: Long = 0, name: String, status: String, beginData: Long, endDate: Long, updateTime: Long) : this(name, status, beginData, endDate, updateTime) {
        this.id = id
    }

    enum class Status(val value: String, val isDone: Boolean) {
        DOING("0", false),
        DONE("1", true);

        companion object {
            fun fromValue(value: String): Status {
                return values().first { it.value == value }
            }

            fun fromIsDone(isDone: Boolean): Status {
                return values().first { it.isDone == isDone }
            }
        }
    }
}
MainActivity.kt
        // 暫定的になので、1つめのレコードを、画面に表示する
        if (taskList.size != 0) {
            val  firstTask = taskList[0]

            first_task_name.text = firstTask.name
            first_status.isChecked = TaskModel.Status.fromValue(firstTask.status).isDone

            first_status.setOnClickListener { _ ->
                taskDatabaseHelper.updateCheck(firstTask.id, TaskModel.Status.fromIsDone(first_status.isChecked).value)
            }
        }

よくなったところ

  • ロジックが集約された。
  • 1クラスに集約されたため、紐付きがわかりやすくなった。
  • Enumにしたことにより、コード値とBooleanのマッピングが読みやすくなった。
  • private const val書かなくてよくなった。
    • 個人的に、private const valはあまり乱立させたくないため…。

アレなところ

  • 一旦Enumに変換し、String/Booleanを取得する、というのが、冗長に見えるかもしれない。
    • 個人的には、あんまり感じていないが、人によってあるいは…?

今後

より良いソースコードを、見つけていきたいです!