LY Corporation Tech Blog

We are promoting the technology and development culture that supports the services of LY Corporation and LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam).

This post is also available in the following languages. Japanese

Improving code quality - Session 58: Words cannot express

The original article was published on February 13, 2025.

Hello, I'm Munetoshi Ishikawa, a mobile client developer for the LINE messaging app.

This article is the latest installment of our weekly series "Improving code quality". For more information about the Weekly Report, please see the first article.

Words cannot express

Suppose there are two types of parameter string formats, ABC and XYZ, as follows.

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

We want to extract the parameters (foo, bar, baz) from these strings. Therefore, we defined an object Regexes to define regular expressions and an object Parser to perform parsing as follows. (In Kotlin, object is a keyword to create a singleton.)

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])
    }
}

Is there any problem with this code?

Keep secrets in your heart

The function parseAbcParams in Parser expects the regular expression ABC_REGEX to have named groups a, b, c. Similarly, parseXyzParams expects x, y, z to be present in this order within the regular expression XYZ_REGEX.

However, what these functions "expect" from the regular expressions is not explicit. If you forget to update the functions when changing the regular expressions, it won't result in a compile error, and you might only notice the bug at runtime.

To mitigate this issue, it is desirable to confine the relationship of "implicit expectations" within the scope of classes or functions. When extracting parameters from strings, it is better to separate the objects into an ABC parser and an XYZ parser rather than separating them into Regexes and Parser.

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])
    }
}

When there are implicit expectations between functions or values, placing them close together (within the same function, class, or module) makes it easier to clarify that relationship.

Open secret

Such implicit relationships occur in various situations. Here are some examples.

  • Paired logic: serialization and deserialization, await and notify
  • Order of function calls: setup -> execution -> teardown
  • Number or type of placeholders in strings: UI text, SQL queries
  • Values with or without edge cases excluded: UInt excluding 0, String excluding empty strings
  • Unprocessed/processed strings: encrypted strings and plaintext, escaped/unescaped strings

Of course, if you can explicitly define the relationship with types, that's ideal. If you want to distinguish between encrypted text and plaintext, it's safe to define EncryptedText and PlainText. However, there are cases where defining types is excessive or cannot be verified until runtime.

The next best approach is to limit implicit relationships to functions/classes/modules or ensure them with unit tests or integration tests. If that's difficult, it's a good idea to explicitly state the existence of implicit relationships in documentation comments.

In a nutshell

When implicit expectations are necessary, confine the relationship of those expectations within functions/classes/modules.

Keywords: implicit expectation, dependency, encapsulation

List of articles on techniques for improving code quality