こんにちは。コミュニケーションアプリ「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 点を満たしているかを確認しましょう。
- Decorator があるかないかを、オブジェクトを使う側が気にしなくて良い。
- 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
として抽出し、PlainTextMessageModel
と EncryptedMessageModel
がそれらを持つ形にしています。(もちろん、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