OOPを使うべき理由をSemVerのチェック実装を例に解説

[KINTO Technologies Advent Calendar 2021 - Qiita](https://qiita.com/advent-calendar/2021/kinto-technologies)の21日目の記事です。

[Semantic Versioning](https://semver.org)に従ったアプリのバージョンチェック実装方法を例に、OOPじゃないコードとOOPのコードを比べてなぜOOPにするべきかを説明します。

バージョンは`1.2.3`みたいに`MAJOR.minor.Patch`の三つの数字でできている文字列で、単純に文字列を比べたらバージョンの前後の判断が出来ない。例えば、`1.0.10`と`1.0.9`を比べると`1.0.9`の方が新しいバージョンだと認識してしまう問題が有るため、３つに分けてそれぞれ比べる必要があります。`<pre-release>`や`<build>`があるバージョンも正しく比較できません。

## Take #1 : 専用メソッド

簡単にするため`private`メソッドでバージョンをチェックするロジックを実装。

```kotlin
class Take1 {
    fun someMethod(currentVersion: String, minVersion: String) {
        if (updateRequired(currentVersion, minVersion)) {
            // Update
        }
    }

    private fun updateRequired(currentVersion: String, minVersion: String): Boolean {
        val current = currentVersion.replace("[^.\\d]".toRegex(), "").split(".")
        val min = minVersion.replace("[^.\\d]".toRegex(), "").split(".")

        return current[0].toInt() < min[0].toInt() ||
                (current[0].toInt() == min[0].toInt() && current[1].toInt() < min[1].toInt()) ||
                (current[0].toInt() == min[0].toInt() && current[1].toInt() == min[1].toInt() && current[2].toInt() < min[2].toInt())
    }
}
```
```kotlin
fun main(args: Array<String>) {
    Take1().someMethod(args[0], args[1])
}
```

バージョンチェックは出来るが、

1. `private`メソッドのため、unit test作成できない。
1. 再使用できない。
1. 更新が必要かどうか以外のことは判断できない。たとえば、
    - どのバージョン番号(`MAJOR`, `minor`, `Patch`)で判定が変わるか。
    - `<pre-release>`や`<build>`が有るかどうか。
    - その他

の限界が有る。

## Take #2 : Helperオブジェクト

更新が必要かどうか判断ロジックを別クラスに分離して、`public`にする。

```kotlin
object Helper {
    fun isUpdateRequired(currentVersion: String, minVersion: String): Boolean {
        val current = currentVersion.replace("[^.\\d]".toRegex(), "").split(".")
        val min = minVersion.replace("[^.\\d]".toRegex(), "").split(".")

        return current[0].toInt() < min[0].toInt() ||
                (current[0].toInt() == min[0].toInt() && current[1].toInt() < min[1].toInt()) ||
                (current[0].toInt() == min[0].toInt() && current[1].toInt() == min[1].toInt() && current[2].toInt() < min[2].toInt())
    }
}
```
```kotlin
class Take2 {
    fun someMethod(currentVersion: String, minVersion: String) {
        if (Helper.isUpdateRequired(currentVersion, minVersion)) {
            // Update
        }
    }
}
```
```kotlin
fun main(args: Array<String>) {
    if (Take2.isUpdateRequired(args[0], args[1])) {
        // Update
    }
}
```

アップデートが必要かの判定ロジックを`Helper.isUpdateRequired`に分離した。これで`Take2`のコードは`Take1`に比べて短くなり、バージョンチェックのロジックは

1. unit testの作成がやりやすくなる。
1. 判定ロジックの再使用が出来る。

ようになった。だがまだ更新が必要かどうか以外のことは判断できないという問題は残っている

## Take #3 : `SemanticVersion`オブジェクト

[Semantic Versioning](https://semver.org)のルールに合う文字列を解析してオブジェクト化できるclassを定義する。

```kotlin
class SemanticVersion : Comparable<SemanticVersion> {
    @Suppress("MemberVisibilityCanBePrivate")
    companion object {
        const val PATTERN = "^(\\d+)\\.(\\d+)\\.(\\d+)(-[^+]+)?(\\+.+)?$"
        val REGEX = PATTERN.toRegex()
    }

    val major: Int
    val minor: Int
    val patch: Int
    val preRelease: String?
    val build: String?

    constructor(version: String) {
        val result = REGEX.find(version)
            ?: throw IllegalArgumentException("illegal version : version=$version")

        major = result.groupValues[1].toInt()
        minor = result.groupValues[2].toInt()
        patch = result.groupValues[3].toInt()

        preRelease = result.groupValues[4].let {
            if (it.isEmpty())
                null
            else
                it.substring(1)
        }
        build = result.groupValues[5].let {
            if (it.isEmpty())
                null
            else
                it.substring(1)
        }
    }

    override fun compareTo(other: SemanticVersion): Int {
        var rv = major.compareTo(other.major)
        if (0 == rv)
            rv = minor.compareTo(other.minor)
        if (0 == rv)
            rv = patch.compareTo(other.patch)

        return rv
    }

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as SemanticVersion

        return major == other.major
                && minor == other.minor
                && patch == other.patch
                && preRelease == other.preRelease
                && build == other.build
    }

    override fun hashCode(): Int {
        var result = major
        result = 31 * result + minor
        result = 31 * result + patch
        result = 31 * result + (preRelease?.hashCode() ?: 0)
        result = 31 * result + (build?.hashCode() ?: 0)
        return result
    }

    override fun toString(): String {
        var rv = "$major.$minor.$patch"
        if (null != preRelease)
            rv = "$rv-$preRelease"
        if (null != build)
            rv = "$rv+$build"
        return rv
    }
}
```
```kotlin
class Take3 {
    fun someMethod(currentVersion: String, minVersion: String) {
        val current = SemanticVersion(currentVersion)
        val min = SemanticVersion(minVersion)

        when {
            current > min -> // Do something
            current < min -> // Update
            else -> // Do something
        }
    }
}
```
```kotlin
fun main(args: Array<String>) {
    Take3().someMethod(args[0], args[1])
}
```
`SemanticVersion`に[Semantic Versioning](https://semver.org)の仕様を実装し、バージョンの上下を比べるだけではなく色んな機能ができた。

1. 上下判定後にもバージョンの詳細情報を使えるようになった。
1. 各バージョン番号と上下の判定に関わらない`<pre-release>`と`<build>`も取れるように変更。（再使用性改善）
1. `preRelease`と`build`属性が`null`かどうかでも判断できるが、`hasPreRelease`みたいな属性も簡単に追加できる。（拡張性改善）
1. `Comparable<SemanticVersion>`インタフェースを実装することで`<`, `==`, `>`などのsymbolが使えるようになった。（コーディングの生産性改善）
1. 正規表現式でバージョン文字列が正しいか判定できる。（安定性の改善）

[全体コードはこちら](https://gist.github.com/garamoi-choi/5baf335934cdd7f77d76618a50274fb1)

## それでOOPの話は？

`SemanticVersion`を定義する前もclassを使ったのになぜ`SemanticVersion`のみOOPか？

OOPはデータとそのデータを使うロジックを一緒に管理するデザインパターン。色んなプログラミング言語が採用し、OOPがやりやすい文法の最も重要な概念になっている。(理論的にはassemblyもOOPはできるが現実的ではない)

[Semantic Versioning](https://semver.org)は`major`, `minor`, `patch`, `pre-release`, `build`のデータ(state)と上下を比べる機能(operation)が必要。

- データ(state) : `major`, `minor`, `patch`, `preRelease`, `build`
- ロジック(operator) : `constructor`(s), `compareTo(SemanticVersion)`, `toString()`などのメソッド。

データとロジックを一緒にハンドリングできるようにまとめるclassが`SemanticVersion`。

`Take1`と`Take3`のコードを比べると

```kotlin
class Take1 {
    fun someMethod(currentVersion: String, minVersion: String) {
        // ここにはstateへアクセスできる方法がない。
        if (updateRequired(currentVersion, minVersion)) {
            // 文字列からmajor, minor, patchのstateを取得したがreturn時になくなり使えない。
        }
    }

    private fun updateRequired(currentVersion: String, minVersion: String): Boolean {
        val current = currentVersion.replace("[^.\\d]".toRegex(), "").split(".")
        val min = minVersion.replace("[^.\\d]".toRegex(), "").split(".")

        return current[0].toInt() < min[0].toInt() ||
                (current[0].toInt() == min[0].toInt() && current[1].toInt() < min[1].toInt()) ||
                (current[0].toInt() == min[0].toInt() && current[1].toInt() == min[1].toInt() && current[2].toInt() < min[2].toInt())
    }
}
```

`updateRequired`メソッドの実行中にstateを取得したが`return`と共になくなり使う方法が無くなる。つまりバージョンを比べた結果はあるがその判断元のデータはどこにも存在しない。`currentVersion`と`minVersion`文字列から取得できるが文字列はその機能(operator又はmethod)を持っていない。


```kotlin
class Take3 {
    fun someMethod(currentVersion: String, minVersion: String) {
        val current = SemanticVersion(currentVersion)
        val min = SemanticVersion(minVersion)

        // current.compareTo(...)が使える。
        when {
            current > min -> // Do something
            current < min -> // Update
            else -> // Do something
        }
        // ここからもcurrent.majorなどのstateを取得できる。
        // 何か他のメソッドを呼ぶときcurrentをパラメタで渡すと向こうのコードもバージョンの全てのstateとoperatorが使える。
    }
}
```

`Take3`はオブジェクト化した`current`のリファレンスを持っている限り`current.major`のようにstateを何回も取得できるし、オブジェクトのリファレンスを渡すことでどんなコードでも漏れや間違いなく`current`のデータ(state)と機能(operator)が使える。

## 結論

ソフトウェアが複雑なら複雑ほどデメリットよりメリットの方が大きい。**だからOOP使いましょう。**

### メリット

1. データ(state)と機能(operator)が一緒にいるためデータの使い方が明確。
1. ロジックの多い部分をオブジェクトに任せて呼ぶ方のコードがシンプルになる。
1. ロジックのcontextの多い部分をオブジェクトが要約するため、コードが読みやすい。
1. 変更の影響範囲が明確な傾向がある。

### デメリット

1. オブジェクトを再使用しないとコードが無駄に長い傾向がある。
1. 再使用されていると変更の影響範囲が広い。
1. 再使用されているとバグがあったら影響範囲が広い。

当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
[KINTO Technologies コーポレートサイト](https://www.kinto-technologies.com/)