こんにちは。コミュニケーションアプリ「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 がこの順番で存在することを期待しています。
しかし、これら関数が正規表現に「期待」していることは明示的ではありません。正規表現を変更したときに、関数を更新するのを忘れてもコンパイルエラーにはならず、実行時にはじめてバグに気づく...という事態になりかねないでしょう。
この問題を軽減するためにも、「暗黙の期待」をする関係を、クラスや関数などのスコープに閉じ込める ことが望ましいです。今回の文字列からパラメータを取得する場合は、 Regexes
と Parser
でオブジェクトを分けるのではなく、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
- 未処理/処理済みの文字列: 暗号化された文字列と平文、エスケープされた/されていない文字列
もちろん、関係を型で明示的に定義できる場合は、それに越したことはありません。暗号化されたテキストと平文を区別したい場合は、それぞれ EncryptedText
と PlainText
を定義するのが安全です。しかし、型を定義することが過剰になる場合や、ランタイムでないと検証できない場合もあります。
次善の策は、暗黙の関係を関数/クラス/モジュールに限定するか、ユニットテストやインテグレーションテストで保証する ことです。それも難しい場合は、暗黙の関係が存在することをドキュメンテーションコメントとして明記するとよいでしょう。
一言まとめ
暗黙の期待が必要な場合、その期待の関係を関数/クラス/モジュールに閉じ込める。
キーワード: implicit expectation
, dependency
, encapsulation