LINEヤフー Tech Blog

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

コード品質向上のテクニック:第33回 シェフの気まぐれデコレーション

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

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

シェフの気まぐれデコレーション

メッセージ送受信をするアプリケーションを作っているとしましょう。以下の MessageModel は送受信するメッセージのデータモデルで、MessageRemoteClient はサーバと送受信を行うためのクラスです。

class MessageModel(
    val messageText: String,
    val timestampMillis: Long,
    ...
)

class MessageRemoteClient(...) {
    fun sendMessage(messageModel: MessageModel): SendResult { ... }

    ...
}

ここで、MessageModel.messageText を暗号化するように仕様が変わったとします (end-to-end 暗号化など)。この暗号化を MessageRemoteClient の内で実装するというのも一つの案です。しかし、もし同じレイヤに MessageModel を使うクラスが他に存在すると、暗号化の実装忘れをしてしまう可能性もあります。例えば、以下の MessageRemoteClient はメッセージの暗号化を行っていますが、MessageBackupClient は暗号化の適用を忘れてしまっています。

class MessageRemoteClient(...) {
    fun sendMessage(messageModel: MessageModel): SendResult {
        ...
        val encryptedMessageText = encrypt(messageModel.messageText, ...)
        val encryptedMessageModel = MessageModel(encryptedMessageText, ...)

        return querySendMessage(encryptedMessageModel, ...)
    }
}

class MessageBackupClient(...) {
    fun backupAllMessages(messageModels: Collection<MessageModel>): BackupResult {
        ...
        
        val responses = messageModels.asSequence()
            .chunked(...)
            .map { queryMessageBackup(it) }

        return ...
    }
}

このような事態を避けるために、ある開発者が Decorator pattern を MessageModel に適用しました。「暗号化」されたメッセージは EncryptedMessageModel として表現されます。

open class MessageModel(
    open val messageText: String,
    val timestampMillis: Long,
    ...
)

class EncryptedMessageModel(
    original: MessageModel,
    encryptionData: EncryptionData
) : MessageModel(
    encrypt(original.messageText, encryptionData),
    original.timestampMillis,
    ...
) {
    companion object {
        private fun encrypt(
            text: String,
            encryptionData: EncryptionData
        ) : String { ... }
    }
}

class MessageRemoteClient(...) {
    fun sendMessage(messageModel: MessageModel): SendResult { ... }
}

このコードにより、複数の ...Client を実装する場合でも、暗号化のロジックは EncryptedMessageModel に集約されます。しかし、この変更によって別の問題が引き起こされてしまいました。それは何でしょうか。

注文外のデコレーション

Decorator pattern を使う場合、以下の 2 点を満たしているかを確認しましょう。

  1. Decorator があるかないかを、オブジェクトを使う側が気にしなくて良い。
  2. Decorator を任意回数、任意順序、任意の組み合わせで適用できる。

EncryptedMessageModel はこれらの条件を満たしていません。...Client 側では、暗号化されているかどうかは重要な情報で、区別する必要があります。もちろん、sendMessage のパラメータ型を EncryptedMessageModel に変えれば、平文の MessageModel を受け取れなくするはできますが、今度は Decorator pattern を使う必要がなくなります。また、暗号化をかけるレイヤが曖昧になると、暗号化を複数回かけてしまうというバグの原因にもなりえます。

今回の場合は、Decorator pattern を使う必要はなく、平文と暗号文でモデルを分けて定義し、暗号化/復号化のユーティリティ関数を別途用意しておけば十分です。以下の実装では、コンストラクタを private にすることで暗号化済みのインスタンスを自由に作れないようにし、代わりに fromPlainTextMessage を使って取得できるようにしています。このようにすることで、意図せず複数回暗号化してしまうバグを回避できます。

class PlainTextMessageModel(
    val messageText: String,
    val timestampMillis: Long,
    ...
)

class EncryptedMessageModel private constructor(
    val encryptedMessageText: String,
    val timestampMillis: Long,
    ...
) {
    companion object {
        private fun fromPlainTextMessage(
            plainTextMessageModel: PlainTextMessageModel
        ): EncryptedMessageModel { ... }
    }
}

class MessageRemoteClient(...) {
    fun sendMessage(messageModel: EncryptedMessageModel): SendResult { ... }
}

このように型を分けた際に、共通するプロパティが多い場合、それらをまとめて別のデータモデルとして抽出することも考慮してください。以下の実装では、共通するプロパティを MessageMetadata として抽出し、PlainTextMessageModelEncryptedMessageModel がそれらを持つ形にしています。(もちろん、messageText 以外は暗号化されないという前提です。)

class MessageMetadata(
    val timestampMillis: Long,
    ...
)

class PlainTextMessageModel(
    val messageText: String,
    val messageMetadata: MessageMetadata
)

class EncryptedMessageModel private constructor(
    val encryptedMessageText: String,
    val messageMetadata: MessageMetadata
) { ... }

一言まとめ

Decorator pattern を使うときは、その条件を確認する。

キーワード: decorator pattern, type safeness, data model

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

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