LINEヤフー Tech Blog

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

コード品質向上のテクニック:第59回 class の facade に水

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

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

class の facade に水

FooValue というデータモデルと、それを保存する FooValueStore というクラスがあるとします。 FooValueStorerepository というパッケージ内で定義され、saveloaddelete といった基本的な操作を提供しています。

// 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 は、deletegetAllIds を利用して、すべての 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 を見るための鏡

このコードの問題は、FooValueStoreFooValueStoreClearer を正しく使用するためには、それらの関係を理解する必要がある点にあります。例えば、Caller.valueStoreCaller.clearer.valueStore が同じインスタンスであることが必須かどうかを知るためには、FooValueStoreFooValueStoreClearer の実装を確認する必要があります。仮に、「同じインスタンスでなければならない」とすると、その制約を Caller が守っているかを調べるためには、Caller のコンストラクタ呼び出しをすべて調べ上げる必要があります。他にも、FooValueStoreFooValueStoreClearer は並列して使用してよいかなども、実装を見ないことには確認できないでしょう。

この問題は、FooValueStore の責任を小さくしようと意識しすぎるあまり、clearAll の実装を別のクラスに分けてしまったことにあります。その結果、呼び出し元の Caller が複雑になってしまいました。これを改善するためには、FooValueStore の責任を無理に小さく留めようとするよりも、呼び出し元のコードに見せるインターフェースの簡潔さ に焦点を当てると良いでしょう。今回の例では、clearAllFooValueStore に内に直接実装してよいです。(もちろん、言語によっては拡張関数として定義する方法もあります。)

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

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

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