見出し画像

誰も教えてくれない、継承を理解するためのたった3つの視点


忙しい人のために

単体テストが書きやすくなるといった視点を持ってください。これは、DBクライアントを引数から入れる場合に、継承を利用するとモックに差し替えられるということです。
引数に入れない場合、依存が切れずに単体テスト時に毎回DBにつなぎに行かなくてはならないです。



オブジェクト指向とは?

オブジェクト指向と検索すれば下記の3つがまずでてきますよね?

  1. カプセル化: データとそのデータを操作するメソッドを一つのエンティティにまとめ、外部からのアクセスを制限することができます。これにより、プログラムの安全性やセキュリティが向上し、データの不正な変更やアクセスを防ぐことができます。

  2. 継承: 既存のクラスを拡張し、新しいクラスを作成することができます。これにより、コードの再利用性が向上し、同様の機能を持つ複数のクラスを効率的に作成することができます。また、継承を使用することで、クラスの階層構造を構築し、より複雑な機能を実現することができます。

  3. ポリモーフィズム: 同じ名前のメソッドを異なるクラスで定義することができ、それぞれのクラスで異なる振る舞いを実現することができます。これにより、コードがより柔軟で拡張性があり、異なる状況や要件に対応することができます。

3つもオブジェクト指向の要素についてあげられてましたが、はっきり言って継承だけわかっていれば別にあとは勝手についてくるものだと思っています。

なので、まずは継承について理解していきましょう。

頭ではわかっているが何が嬉しいのかわからない。。。

まぁ、まずは継承って何かっていう話ですよ。具体的なコードで見ていきましょう。これはPythonのコードで継承を使ったものです。

3つの視点で継承を眺めてみよう

ここでは、抽象クラスからの継承と実装クラスの継承を分けて考えないようにしています。どちらでも継承の力は発揮できるの問題ありません。

共通処理をまとめておける

いわゆるutil的な処理を親クラスにおいておくことで、コードがよりスッキリします。

  • ログの出力にかかわる部分

  • 設定情報の出力

  • クライアントをラップする

クライアントをラップするとうのはHTTPクライアントなどを指します。BASEのURLを親クラスで定義しておけば呼び出し時に、いちいちそれらを書かなくて済みます。またURLの変更も親クラスだけを編集するだけでよいのです。

実装の強制ができる

普段個人開発をしていたり、これからプログラムを学ぼうとしている方にはピンとこないかもしれないですが、プログラムは自分以外の誰かとチームを組んで行うことが多いです。そのときに実装を強制させることができるというのは大きなメリットを持ちます。

  1. メソッド名を統一させる

  2. 必ず入れたい処理を入れる

例えば、GuestクラスとAdminクラスがあった時にどちらにもファイルアップロードの処理を入れたいとします。
片方のクラスには `upload` でもう片方のクラスは `fileupload` といったような書き方をされると、コードの重複は起きるし、メソッド名が異なるので同じ処理だとは思いにくいです。

継承元である親クラスに一つファイルアップロード用のメソッドをはやしておけば、このようなことはなくなります。

単体テストが書きやすくなる

個人的にはこれが一番大きいですかね。例えば、DBに接続するためのクラスがこんな感じであったとします。(型を意識するほうが説明がわかりやすいためJavaで書いてます)

※ 実際にコメントアウト部分は何らかの処理が書かれているものとします。

public class DBClient {
    private String host;
    private int port;

    public DBClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void connect() {
        // DBに接続する処理
    }

    public void disconnect() {
        // DBから切断する処理
    }

    public Object query(String sql) {
        // SQLクエリを実行して結果を返す
        return null;
    }
}

次に、このDBに接続してクエリを使う処理を performQuery とします。コンストラクタで本番環境に接続するようになっています。

public class DBService {
    private DBClient dbClient;

    public DBService() {
        this.dbClient = new DBClient("honban_host", 5432);
   }

    public Object performQuery(String sql) {
        dbClient.connect();
        Object result = dbClient.query(sql);
        dbClient.disconnect();
        return result;
    }
}

このサービスクラス(webでは何かの処理をするクラスをサービスと名付けることが多いです)は下記のように呼び出します。

public class Main {
    public static void main(String[] args) {
        DBService dbService = new DBService();
        Object result = dbService.performQuery("SELECT * FROM table_name");
    }
}

DBService がコンストラクタで呼び出されると、内部で DBClient が本番環境に接続します。

さて、このときにこの DBService クラスをテストしなさいと言われました。テストコードをどのように書いたらいいでしょうか?

~ シンキングタイム ~

おそらくこうなると思います。

public class Main {
    public static void main(String[] args) {
        DBService dbService = new DBService();
        Object result = dbService.performQuery("SELECT * FROM table_name");

        if (result == correct_result) {
            // OK
        } else {
            // NO
        }
    }
}

ここで、correct_result はクエリを叩いたときに出てくる想定の結果です。

一見何の問題もないように見えますが、よく考えてください。これってどこのDBに接続してますか?

そうです。本番環境に接続してるんです。本番環境ということは、データも変わる可能性がありますよね?そしたらテスト結果も毎回一致するとは限りませんよね?

じゃあ、テスト用のDBを作ればいいんだ!! → その通りです。やってみましょう。

あれ?うまくできないですね。それもそうです。サービスクラスを呼び出した時点で本番環境に接続されてしまってるのです。これを解決するのが継承の概念です。

まずは、どのようにするかを見ましょう。

public class DBService {
    private DBClient dbClient;

    // コンストラクタに入れるようにした!
    public DBService(DBClient dbClient) {
        this.dbClient = dbClient;
    }

    public Object performQuery(String sql) {
        dbClient.connect();
        Object result = dbClient.query(sql);
        dbClient.disconnect();
        return result;
    }
}

変更点はコンストラクタからデータベースの接続クラスを入れるようにしただけです。これによって何が起きるかというと、 DBClient クラスを継承して入れさえすれば、どんなクラスをいれてもよいということになります。

つまり、テスト用のDB接続情報をもったクラスを作成してもいいですし、はたまた、中身のまるっきりないクラスでもいいんです。

// テスト用のコード
public class TestDBClient extends DBClient {
    @Override
    public void connect() {
        pass
    }

    @Override
    public void disconnect() {
        pass
    }
}

こんな感じで書くときってどんなときかというのは、DBにかかわる部分ではないところの処理をテストしたいときなどがあげられます。

このように継承は単体テスト時に大きな力を発揮するのです

なんでもかんでも継承しなさいとは言ってない

継承は必要な時に必要なものだけ行うようにするのが一番いいです。よく諸学者の方にこう聞かれます。

「どうやって書くのが正解ですか?」

プログラムの書き方に正解はありません。プログラムはあくまで手段であって目的ではありません。やりたいことが実現できていれば、実際はどんなふうにプログラムを書いてもいいのです。

これからも改良を加えたり、機能を追加するようなときにより、頑健で保守のしやすいプログラムにしたい場合は、継承などのテクニックを使ってアーキテクチャを利用した実装を考えたほうがいいかもしれないです。

全てはトレードオフになっているので、自分がどこを目指しているのかをよくよく考えることがこそが真の正解と言えるでしょう。

私は何事も多面的に物事を見ることで理解が進むという風に考えています。プログラムも、概念を理解し、実際のプログラムを見て理解するといった2面で理解することが大事だと思います。

この記事が気に入ったらサポートをしてみませんか?