こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。
この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 59 回です。 LINEヤフー社 内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)
class の facade に水
FooValue
というデータモデルと、それを保存する FooValueStore
というクラスがあるとします。 FooValueStore
は repository
というパッケージ内で定義され、save
や load
、delete
といった基本的な操作を提供しています。
// In repository package
class FooValueStore {
fun save(id: FooId, value: FooValue) { /* ... */ }
fun load(id: FooId): FooValue? { /* ... */ }
fun delete(id: FooId) { /* ... */ }
fun getAllIds(): List<FooId> { /* ... */ }
}
// In caller package
class Caller(private val valueStore: FooValueStore) {
fun doSomething() {
// Uses `valueStore`
}
}
その後、すべての FooValue
エントリを削除するという新たな機能が必要になったとしましょう。この機能を実現するため、FooValueStore
と同じ repository
パッケージに FooValueStoreClearer
というクラスが作成されました。このクラスの clearAll
は、delete
と getAllIds
を利用して、すべての FooValue
エントリを削除します。このクラスを使うためには、FooValueStore
とは別に FooValueStoreClearer
のインスタンスを保持する必要があります。
// In repository package
class FooValueStoreClearer(private val valueStore: FooValueStore) {
fun clearAll() {
valueStore.getAllIds().forEach { valueStore.delete(it) }
}
}
// In caller package
class Caller(
private val valueStore: FooValueStore,
private val clearer: FooValueStoreClearer
) {
fun doSomething() {
// valueStore と clearer の両方を使用
}
}
この構造について、なにか改善すべき点はありますか?
自らの Facade を見るための鏡
このコードの問題は、FooValueStore
と FooValueStoreClearer
を正しく使用するためには、それらの関係を理解する必要がある点にあります。例えば、Caller.valueStore
と Caller.clearer.valueStore
が同じインスタンスであることが必須かどうかを知るためには、FooValueStore
と FooValueStoreClearer
の実装を確認する必要があります。仮に、「同じインスタンスでなければならない」とすると、その制約を Caller
が守っているかを調べるためには、Caller
のコンストラクタ呼び出しをすべて調べ上げる必要があります。他にも、FooValueStore
と FooValueStoreClearer
は並列して使用してよいかなども、実装を見ないことには確認できないでしょう。
この問題は、FooValueStore
の責任を小さくしようと意識しすぎるあまり、clearAll
の実装を別のクラスに分けてしまったことにあります。その結果、呼び出し元の Caller
が複雑になってしまいました。これを改善するためには、FooValueStore
の責任を無理に小さく留めようとするよりも、呼び出し元のコードに見せるインターフェースの簡潔さ に焦点を当てると良いでしょう。今回の例では、clearAll
を FooValueStore
に内に直接実装してよいです。(もちろん、言語によっては拡張関数として定義する方法もあります。)
class FooValueStore {
fun save(id: FooId, value: FooValue) { /* ... */ }
fun load(id: FooId): FooValue? { /* ... */ }
fun delete(id: FooId) { /* ... */ }
fun getAllIds(): List<FooId> { /* ... */ }
fun clearAll() {
getAllIds().forEach { delete(it) }
}
}
class Caller(
// Requires only `FooValueStore` here
private val valueStore: FooValueStore
) {
fun clearAllValues() {
valueStore.clearAll()
}
}
clearAll
メソッドを FooValueStore
内に実装することで、Caller
は単純に FooValueStore
の機能について知っていれば十分になります。2 つのオブジェクトの関連を気にする必要がなくなるため、最小知識の原則にも沿うような設計にもしやすいでしょう。
一言まとめ
クラスの責任範囲を決めるときは、呼び出し側から見てインターフェースが単純かどうかを意識する。
キーワード: interface
, encapsulation
, single responsibility principle