3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

KINTO Technologies - トヨタ車のサブスク「KINTO」開発中!Advent Calendar 2021

Day 21

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

Last updated at Posted at 2021-12-17

KINTO Technologies Advent Calendar 2021 - Qiitaの21日目の記事です。

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

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

Take #1 : 専用メソッド

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

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())
    }
}
fun main(args: Array<String>) {
    Take1().someMethod(args[0], args[1])
}

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

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

の限界が有る。

Take #2 : Helperオブジェクト

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

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())
    }
}
class Take2 {
    fun someMethod(currentVersion: String, minVersion: String) {
        if (Helper.isUpdateRequired(currentVersion, minVersion)) {
            // Update
        }
    }
}
fun main(args: Array<String>) {
    if (Take2.isUpdateRequired(args[0], args[1])) {
        // Update
    }
}

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

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

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

Take #3 : SemanticVersionオブジェクト

Semantic Versioningのルールに合う文字列を解析してオブジェクト化できるclassを定義する。

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
    }
}
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
        }
    }
}
fun main(args: Array<String>) {
    Take3().someMethod(args[0], args[1])
}

SemanticVersionSemantic Versioningの仕様を実装し、バージョンの上下を比べるだけではなく色んな機能ができた。

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

全体コードはこちら

それでOOPの話は?

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

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

Semantic Versioningmajor, minor, patch, pre-release, buildのデータ(state)と上下を比べる機能(operation)が必要。

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

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

Take1Take3のコードを比べると

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と共になくなり使う方法が無くなる。つまりバージョンを比べた結果はあるがその判断元のデータはどこにも存在しない。currentVersionminVersion文字列から取得できるが文字列はその機能(operator又はmethod)を持っていない。

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)が一緒にいるためデータの使い方が明確。
  2. ロジックの多い部分をオブジェクトに任せて呼ぶ方のコードがシンプルになる。
  3. ロジックのcontextの多い部分をオブジェクトが要約するため、コードが読みやすい。
  4. 変更の影響範囲が明確な傾向がある。

デメリット

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

当社では、トヨタ車のサブスク「KINTO」等の企画/開発を行っており、エンジニアを募集中です。
KINTO Technologies コーポレートサイト

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?