ikenox.info

Naoto Ikeno

Naoto Ikeno

Backend Engineer, Software Architecture & Design, Perl, Golang, GCP

継承と委譲の使い分けと、インターフェースのメリット

August 28, 2018

継承、委譲、インターフェースは普段特に悩むことなく使い分けができていたのですが、いざ「継承と委譲はどういった基準で使い分けているか?」「インターフェースは何が嬉しいのか?」と聞かれると意外ときちんと言語化できなかったので、この記事に書き起こして言語化を試みました。

TL;DR

  • 子に親と同じ役割が期待される場合、親と同様に振る舞えるようになる継承を使うのが良い
  • 子に親と同じ役割が期待されない場合、子にとって親は単なるツールである可能性が高いため委譲を使うのが良い
  • インターフェースを使うことで責務の分離を強制することができ、その結果テストの容易性を得られたりする

委譲の使いどき

例として、「SQLデータベースからユーザー情報を取り出したり保存したりする機能」を作ろうとしたとき、ぱっと思いつく方針としては、

  • DBの接続状態を保持したりSQLクエリを実行したりする部分のロジックは、ユーザー情報の取得に限らず、データベースから情報を取得するとき全般で使いまわせそう
    ⇒ DBの操作周辺に関して使いまわせそうな部分はクラスに切り出しておいて(ここではDatabaseクラスとする)、ユーザー情報の取得や保存をするためのクラスUserDatabseからDatabaseクラスを何らかの方法で利用するようにすれば、DatabaseクラスのDB操作機能を使いまわせて良い感じ

という感じになるかと思います。 ここで、UserDatabaseからDatabaseクラスを利用する方法としては、継承または委譲が思い浮かびます。

結論から言うと委譲を使ったほうが良いのですが、継承を使って以下のようにやってしまいたくなるかもしれません。

委譲を使うべき場面で継承を使った例

class Database {
    void connectToDatabase(String dbHost, String dbName, ...){
        // DBへの接続ロジック
    }

    Result doSQL(String sql){
        // 接続しているDBにクエリを投げ、その結果を返すロジック
    }

    // そのほかデータベースの接続状態の管理など
}
class UserDatabase extends Database {
    User getUser(int userId) {
        String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);
        Result result = this.doSQL(sql)
        // クエリの実行結果をもとにユーザーオブジェクトを作って返す
    }
}

継承を使ったこの実装だと、UserDatabaseDatabaseを継承しているのでDatabaseと同等の振る舞いをできるようになります。
が、そのことによって不自然な点が生じてしまいます。

UserDatabaseは、「特定のユーザーを取得する」という本来持たせたかった役割の他に、Databaseの持つ「DBとの接続状態の保持や、DBへのSQLクエリ送信」といった役割も獲得してしまっています。これは単一責務の原則に反することになります。
UserDatabaseの利用側がUserDatabaseに期待している役割は「特定のオブジェクト(この場合はユーザー)を取得する」ことであり、直接データベースの操作をしたり、データベースとの接続状態を管理してほしい訳ではないはずです。
UserDatabaseからすると、DatabaseはDB接続のための単なるツールとして使いたいだけで、Database自身になりたいわけではありません。

1つのクラスが複数の責務を持つと、それぞれの責務に関係した変更があった際に影響を受けてしまうようになり、改修の影響範囲が大きくなってしまいます。
また、Databaseがパブリックメソッドを持つような場合、それらのメソッドはUserDatabaseのメソッドとしても呼び出せるようになります。結果、ユーザーの取得には関係のないDB操作のメソッドもUserDatabaseの利用側が使えるようになり、UserDatabaseが意図とは異なる使い方をされてしまう可能性が出てきます。

UserDatabaseDatabaseのように期待される役割が異なっている2つのクラス間で継承関係を持つと、上記のようにいくつか不自然な点が出てしまいます。
そういう場合は代わりに委譲を使うのが適していると言えます。

委譲を使って書き直した例

Databaseは先程と同じ実装のままで、UserDatabaseが変更になります。

class UserDatabase {

    Database database;

    UserDatabase(Database database){
        // UserDatabaseのインスタンス生成時にDatabaseのインスタンスをセット
        this.database = database;
    }

    User getUser(int userId) {
        String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);
        Result result = this.database.doSQL(sql)
        // クエリの実行結果をもとにユーザーオブジェクトを作って返す
    }
}

継承はせず、Databaseのインスタンスを保持しておいて、必要に応じてそれを呼び出す(委譲)ようになっています。
Databaseのインスタンスは、UserDatabaseのオブジェクト生成時のコンストラクタなどで渡すようにします。

UserDatabaseDatabaseを継承していないのでDatabaseとしては振る舞えず、DBからユーザーを取得するための最小限のロジックのみ知っている状態になっています。
「期待される役割が異なっている」クラスの機能を利用したい際は、継承ではなく委譲を用いるのが適切と言えます。

継承の使いどき

逆に「子には親と同じ役割が期待される」場合には継承が適しています。

たとえば、Car(車)クラスとElectoricCar(電気自動車)クラスを考えます。 CarのほうがElectoricCarよりも意味が広く抽象度は高いですが、Carを利用するときもElectoricCarを利用するときも「加速する/減速する」「窓を開ける/閉める」といった基本的な動作は共通して可能であることが期待されます。
「ハイブリッドカーを運転する」ことを、「車を運転する」と言い換えても間違いではないように、CarElectoricCarに期待される役割は基本的な部分では一緒であると言えます。

このような場合に委譲を使うと以下のようになります。

継承を使うべき場面で委譲を使った例

class Car {
    void SpeedUp(){
        // ...
    }

    void OpenWindow(){
        // ...
    }

    // ...
}
class ElectoricCar {
    Car car;

    void SpeedUp(){
        car.SpeedUp();
    }

    void OpenWindow(){
        car.OpenWindow();
    }

    // ...

    // 電気自動車特有のロジック
    void Charge() {
        // ...
    }
}

加速や減速、窓の開閉など、Carの持っている機能の数だけElectoricCarにボイラープレートコードが増えていきます。Carに「ライトを点ける」動作が増えた場合にはElectoricCarにも変更を加えなくてはいけません。新たにHybridCarを増やそうとすると更に辛くなっていきます。 また、2つのクラスは継承関係に無いので、ダックタイピングを許さないような言語だとElectoricCarCarとして扱うこともできません。

継承を使って書き直した例

Carは先程と同じ実装のままで、ElectoricCarが変更になります。

class ElectoricCar extends Car {
    void Charge() {
        // ...
    }
}

委譲を使った際と比べて圧倒的にスッキリします。Carに新たなメソッドが増えるなどの変更が入ってもElectoricCarに変更を入れる必要はありません。「子には親と同じ役割が期待される」場合は、委譲ではなく継承を用いるのが適切と言えます。

ここまでのまとめ

  • 子に親と同じ役割が期待される場合、親と同様に振る舞えるようになる継承を使うのが良い
  • 子に親と同じ役割が期待されない場合、子にとって親は単なるツールである可能性が高いため委譲を使うのが良い

インターフェースの使いどころ

UserDatabaseについて再び考えると、極端な話UserDatabaseの利用側は、userさえ取得できればそれで良くて、その裏側にあるのがデータベースだろうが、.txtファイルに保存されていようが、オンメモリだろうがなんだって良いはずです。 こういう「裏の実装は複雑な状態などを取りうるかもしれないが、使う側としては目的が達成できれば何でもいい」場合はインターフェースの出番であることが多いです。
そこで、UserRepositoryという「ユーザーを取得・保存する」役割のインターフェースを作り、UserDatabaseUserRepositoryを実装するようにしてみます。

interface UserRepository{
    User getUser(int userId);
    void saveUser(User user);
}
class UserDatabase implements UserRepository{

    Database database;

    UserDatabase(Database database){
        this.database = database;
    }

    User getUser(int userId) {
        String sql = String.format("SELECT * FROM users WHERE user_id=%d", userId);
        // ...
    }

    // ...
}

UserRepositoryの利用側のロジックは例えば以下のようになります。

class SomeApplicationService {
    UserRepository userRepository;

    void changeUserEmail(String email){
        User user = userRepository.getUser(userId);
        user.setEmail(email);
        userRepository.saveUser(user);
    }
}

UserRepositoryはインターフェースのため、実装に関して全く言及をしていません。そのため、UserRepositoryを利用しているSomeApplicationService側としては、「とにかくuserが取得できることを期待しており、実装は何でも良い」ということをコードで表明していることになります。
結果、以下のようなメリットを得られるようになります。

  • ユーザーデータのハンドリングが主な役割であるSomeApplicationServiceがデータベースへの接続などについて言及することはなくなり(できなくなり)、責務の分離を強制することができます。
  • SomeApplicationServiceはDBについては言及していないので、例えばSomeApplicationServiceのテスト用にUserRepositoryを実装したインメモリのInMemoryUserDatabaseを用意しておいて、テストのときだけuserRepositoryの実装を差し替えるようなことが可能となり、よりテストが容易になります。