LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第58回 言葉にできない

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 58 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)

言葉にできない

以下のような、ABC と XYZ という 2 種類のパラメータの文字列形式があるとします。

  • ABC: a=foo,b=bar,c=baz
  • XYZ: x:foo;y:bar;z:baz

これらの文字列からパラメータ (foo, bar, baz) を取得したいです。そこで以下のように、正規表現を定義するオブジェクト Regexes とパースを実行するオブジェクト Parser を定義しました。(Kotlin の object はシングルトンを作成するキーワードです。)

class AbcParams(val a: String, val b: String, val c: String)
class XyzParams(val x: String, val y: String, val z: String)

object Regexes {
    val ABC_REGEX: Regex = """a=(?<a>\w+),b=(?<b>\w+),c=(?<c>\w+)""".toRegex()
    val XYZ_REGEX: Regex = """x:(\w+);y:(\w+);z:(\w+)""".toRegex()
}

object Parser {
    fun parseAbcParams(string: String): AbcParams? {
        val groups = Regexes.ABC_REGEX.matchEntire(string)?.groups
            ?: return null
        val a = groups["a"]?.value.orEmpty()
        val b = groups["b"]?.value.orEmpty()
        val c = groups["c"]?.value.orEmpty()
        return AbcParams(a, b, c)
    }

    fun parseXyzParams(string: String): XyzParams? {
        val values = Regexes.XYZ_REGEX.matchEntire(string)?.groupValues
            ?: return null
        return XyzParams(values[1], values[2], values[3])
    }
}

このコードには何か問題がありますか?

秘密を心に秘めておく

Parser の関数 parseAbcParams は、正規表現 ABC_REGEX が名前付きグループ a, b, c を持つことを期待しています。同様に parseXyzParams は、正規表現 XYZ_REGEX 内に x, y, z がこの順番で存在することを期待しています。

しかし、これら関数が正規表現に「期待」していることは明示的ではありません。正規表現を変更したときに、関数を更新するのを忘れてもコンパイルエラーにはならず、実行時にはじめてバグに気づく...という事態になりかねないでしょう。

この問題を軽減するためにも、「暗黙の期待」をする関係を、クラスや関数などのスコープに閉じ込める ことが望ましいです。今回の文字列からパラメータを取得する場合は、 RegexesParser でオブジェクトを分けるのではなく、ABC のパーサと XYZ のパーサでクラスを分けるとよいでしょう。

object AbcParser {
    private val REGEX: Regex = """a=(?<a>\w+),b=(?<b>\w+),c=(?<c>\w+)""".toRegex()

    fun parse(string: String): AbcParams? {
        val groups = REGEX.matchEntire(string)?.groups
            ?: return null
        val a = groups["a"]?.value.orEmpty()
        val b = groups["b"]?.value.orEmpty()
        val c = groups["c"]?.value.orEmpty()
        return AbcParams(a, b, c)
    }
}

object XyzParser {
    private val REGEX: Regex = """x:(\w+);y:(\w+);z:(\w+)""".toRegex()

    fun parse(string: String): XyzParams? {
        val values = REGEX.matchEntire(string)?.groupValues
            ?: return null
        return XyzParams(values[1], values[2], values[3])
    }
}

関数や値の間に暗黙の期待がある場合、それらを近く (同じ関数、クラス、モジュール内) に置くことで、その関係を明確にしやすいです。

公然の秘密

このような暗黙の関係はさまざまな状況で発生します。以下はその例です。

  • 対となるロジック: シリアライズとデシリアライズ、await と notify
  • 関数の呼び出し順序: setup -> execution -> teardown
  • 文字列中のプレースホルダの数や種類: UI テキスト、SQL クエリ
  • エッジケース除外済み/未除外の値: 0 を除外した UInt, 空文字列を除外した String
  • 未処理/処理済みの文字列: 暗号化された文字列と平文、エスケープされた/されていない文字列

もちろん、関係を型で明示的に定義できる場合は、それに越したことはありません。暗号化されたテキストと平文を区別したい場合は、それぞれ EncryptedTextPlainText を定義するのが安全です。しかし、型を定義することが過剰になる場合や、ランタイムでないと検証できない場合もあります。

次善の策は、暗黙の関係を関数/クラス/モジュールに限定するか、ユニットテストやインテグレーションテストで保証する ことです。それも難しい場合は、暗黙の関係が存在することをドキュメンテーションコメントとして明記するとよいでしょう。

一言まとめ

暗黙の期待が必要な場合、その期待の関係を関数/クラス/モジュールに閉じ込める。

キーワード: implicit expectation, dependency, encapsulation

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧