LINEヤフー Tech Blog

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

コード品質向上のテクニック:第62回 二匹目の close

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

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

二匹目の close

以下のコードは、あるリソースを管理するためのクラスです。リソースの使用後は close を呼ぶことでリソースの開放をすることを期待しています。close を呼び出したときにリソースがすでに解放済みだった場合は、error によってランタイムエラーが投げられます。

class FooResource {
    var isClosed = false
        private set()

    ...

    fun close() {
        if (isClosed) {
            error(...)
        }

        ... // Release resources
        isClosed = true
    }
}

このコードに何か改善点はありますか?

同じことを聞く

上記のコードで close を安全に呼ぶためには、isClosed の確認が必要になります。

if (!fooResource.isClosed) {
    fooResource.close()
}

この close を呼ぶ箇所が増えてくると、isClosed のチェックを漏らしてしまい、意図せずランタイムエラーを発生させてしまう可能性があります。

FooResource.close に関しては、Java や Kotlin の AutoCloseable と同様に 冪等 (idempotent) にすると良いでしょう。冪等とは、1 回呼び出した結果と、2 回以上呼び出した結果が同じになるという性質です。以下の実装では、close を 1 回呼び出した場合と、close を 2 回以上呼び出した場合で、結果は等しく「リソースをリリースし、isClosed を true にする」というものになります。

class FooResource {
    var isClosed = false
        private set()

    ...

    fun close() {
        if (isClosed) {
            return
        }

        ... // Release resources
        isClosed = true
    }
}

関数が冪等性 (idempotency) を満たすことで、以下のような利点を享受できる可能性があります。

  • 関数を呼び出す前に状態チェックを行う必要がなくなる
  • 関数呼び出し後の状態が予測しやすくなる
  • 「初期状態」が存在することを隠蔽できる

この内、「関数を呼び出す前に状態チェックを行う必要がなくなる」と「関数呼び出し後の状態が予測しやすくなる」という利点は、冪等でない関数にも応用できます。エラーとなる呼び出しで、「例外を使う代わりに、何もしない」という動作にするだけで良いです。

戻る場合でも

状態遷移が巡回する場合でも、「状態チェックを行う必要がなくなる」と「関数呼び出し後の状態が予測しやすくなる」の 2 つの利点を活かすことができます。以下の Animation クラスには "running" と"not-running" の 2 つの状態があり、それぞれ setRunning(true)setRunning(false) を呼ぶことで相互に遷移可能です。このとき、すでに "running"/"not-running" であるときに setRunning(true)/setRunning(false) を呼び出した場合の動作は、「何もしない」になります。

class Animation {
    private var isRunning = false

    fun setRunning(isRunning: Boolean) {
        if (this.isRunning == isRunning) {
            return
        }
        this.isRunning = isRunning

        ...
    }
}

状態遷移は次のようになります。

setRunning の状態遷移図。setRunning(true) で not-running と running のどちらからでも running へ、setRunning(false) で not-running と running のどちらからでも not-running へ遷移。

この状態遷移では、setRunning(true)setRunning(false) をそれぞれ別の関数とみなせば、冪等な関数と言うことができます。

このように、すでに遷移済みの場合は何もしないという挙動にすることで、複数のイベントハンドラが setRunning を呼び出す場合に、コードを単純にしやすくなります。現在の状態にかかわらず、setRunning の引数によって最新の状態が決定されるということが保証されているためです。

3つの状態でも

この考え方は、2 つの関数をもつ 3 状態の場合にも適用できます。代表的な例として、次の 2 つを紹介します。

  1. 特定の関数が他の関数よりも優先される場合
  2. 最初に呼び出された関数が他の関数よりも優先される場合

特定の関数が他の関数よりも優先される場合

以下の Task には startfinish という関数があり、それぞれ "RUNNING" と "FINISHED" に遷移します。ただし、startfinish が両方とも呼ばれたならば、順序にかかわらず finish が優先されるという仕様です。

class Task(...) {
    private var state: State = State.INITIAL

    fun start() {
        if (state != State.INITIAL) {
            return
        }
        state = State.RUNNING
        ...
    }

    fun finish() {
        if (state == FINISHED) {
            return
        }
        state = State.FINISHED
        ...
    }

    enum class State { INITIAL, RUNNING, FINISHED }
}

状態遷移は次のようになります。

start と finish の状態遷移図。INITIAL で start を呼ぶと RUNNING に遷移。それ以外では start は現状維持。finish はどこからでも FINISHED に遷移。

これは、見方を変えると "INITIAL" と "RUNNING" が大きな 1 つの状態を構成し、finish を呼ぶことでそれらから "FINISHED" に移行するとみなせます。このような解釈のもとでは、finish は冪等な関数と言えるでしょう。

start と finish の状態遷移図の INITIAL と RUNNING をひとまとめにした状態遷移図。

逆に、"RUNNING" と "FINISHED" を 1 つの状態にまとめて見ることで、start も冪等な関数とみなすことができます。

どちらかの状態遷移が優先し、かつ、それ以外の呼び出しは現状を維持することで、リソースやタスクのライフサイクル管理を容易にする事ができます。

最初に呼び出された関数が他の関数よりも優先される場合

以下の Task では、finish で正常終了する可能性と cancel でキャンセルされる可能性があります。先程の例とは異なり、finishcancel はどちらが優先されるかはあらかじめ決まっておらず、先に呼び出されたほうが優先されるというものになります。

class Task(...) {
    private var state: State = State.RUNNING

    fun finish() {
        if (state != State.RUNNING) {
            return
        }
        state = State.FINISHED
        ...
    }

    fun cancel() {
        if (state != State.RUNNING) {
            return
        }
        state = State.CANCELLED
        ...
    }

    enum class State { RUNNING, FINISHED, CANCELLED }
}

そして、状態遷移は次のようになります。

finish と cancel の状態遷移図。RUNNING で finish や cancel を呼ぶと、それぞれ FINISHED と CANCELLED に遷移。それ以外では現状維持。

ここで、"FINISHED" と "CANCELLED" 1 つの大きな状態とし、finishcancel の関数も 1 つの関数とみなしてしまえば、そのまとめられた関数は冪等であると言えます。

finish と cancel の状態遷移図の FINISHED と CANCELLED をひとまとめにした状態遷移図。

このような遷移設計は、特に異なる呼び出し元が非同期に finishcancel を呼び出す状況で有効です。たとえば、クエリの実行が完了されたときに finish が呼ばれるが、ユーザによっていつでも非同期に cancel される可能性がある場合、finishcancel を呼び出した時点でもう一方がすでに呼ばれている可能性があります。そのような状況でも、現在の状態を確認せずとも関数を安全に呼べるようにすることで、呼び出し元のコードを単純にできます。

一言まとめ

遷移済みのときに例外を投げる代わりに、何もしないという動作にすることで、コードを頑健にできる可能性がある。

キーワード: idempotency, state transition, error handling

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

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