Javaのオブジェクト指向について死に物狂いでまとめたから見て

プログラミング
プログラミング

Java言語に対象を絞って、オブジェクト指向という考え方についてまとめてみました。できるだけ理解しやすくなるように、独自の解釈を多く含んでおります。

当記事はその構成上、断言するような表現を多用していますが、オブジェクト指向に対する筆者個人の理解・見解であり、「これが答え!」というものではありません。もし誤認していると思われる部分がありましたら、そっとご指摘いただけるとありがたいです。

オブジェクト指向とは

オブジェクト指向プログラミング(Object Oriented Programming: OOP)とは、処理を組み合わせたオブジェクトという単位でプログラムを書いていくという考え方のことです。

有名なオブジェクト指向の言語として、
C++,Python,Java,PHP,Ruby,JavaScript,Scala,Kotlin,TypeScript,Swift
などがあります。これだけ見ても、プログラミングをする以上オブジェクト指向は避けては通れないということが実感できますね。

参考

オブジェクト指向プログラミング – Wikipedia

オブジェクト指向を使う理由

いきなり結論からになりますが、オブジェクト指向でプログラムを書く理由は、
プログラムを抽象化するため」です。
プログラムが抽象化されていると、少ない知識で多くのことをプログラミングできます。

「抽象化」という言葉自体が抽象的なので、かなり乱暴に具体的にしてみようと思います。
抽象化とは「名前をつける」ことです。

「赤くて甘い蜜を含む果実」🍎に「リンゴ」と名付けたり、「黄色くて酸っぱい果実」🍋に「レモン」と名付けたり、「橙色で中にいくつもの房がある果実」🍊に「みかん」と名付けたり 、それらの「美味しく食べられる果実」をまとめて「フルーツ」と名付けたり、これまでありとあらゆるものが誰かによって名前をつけられました。

名付けるという行為は、抽象化をしているのです。「赤くて甘い蜜を含む果実」🍎が具体なのに対して、「リンゴ」という名前がそれを抽象化した表現になります。
この抽象化のメリットは、例えば「赤色をあらゆる物事の名前を使わずに説明する」ということを考えてみると、改めて強く理解できるかと思います。きっと、とても難しいと感じるのではないでしょうか。

オブジェクト指向でプログラムを書く理由を「プログラムを抽象化するため」としました。そして「抽象化とは名前をつけること」としました。つまり、オブジェクト指向は「プログラムに名前をつけるため」の仕組みだということができます。

オブジェクト指向の3大要素

インターフェース

オブジェクト指向の3大要素は「カプセル化・ポリモフィズム・継承」と言われています。インターフェースは入っていないのですが、私は3大要素のどれよりもインターフェースの方が重要だと考えているので先に紹介します。

オブジェクト指向におけるインターフェースは、「プログラムの名前を定義する」役割を果たします。前の項でその重要性について書いた「名前をつける」という行為ですが、オブジェクト指向の中でそれを担っているのがインターフェースなのです。

「リンゴ」という名前は言葉で表されます。ですがその実体は🍎です。

この実体がもし手元になくても、「リンゴ」という名前があればリンゴについて会話することができます。
命名と実体を分けることによって、実体を意識しなくてもそのものを扱うことができるようになります。

この考え方をオブジェクト指向に落とし込んだときの、命名側がインターフェースとなるのです。
インターフェースを介してメソッドを呼び出したとき、利用する側のプログラムは、利用される側のプログラムの実体を意識しなくてよいことになります。知っておけばいいのは、インターフェースで定義されたプログラムの名前だけです。

Javaでいうと「プログラムの名前」とは「戻り値の型+シグネチャ(メソッド名+引数の型+仮引数名)」となります。
例えば、Objectクラスのequalsメソッドはboolean equals(Object obj)という「名前」になります。

プログラミングとして、命名と実体が分かれることについてのメリットは、「命名に従ってくれてさえいれば、実体にどんな変更が加わっても構わない」ということです。プログラムの呼び出し側と呼ばれる側の間にインターフェースが入ることで、お互いの依存関係がインターフェースに集約され、直接の依存関係がなくなります。そうすることで、使われる側としては命名に従うというルールのもとコードの修正が容易になります。

ですが、これは裏を返せば「インターフェースは気安く変更することはできない」ということになります。
インターフェースにメソッドを一つ追加すれば、そのインターフェースの実体となるクラスはすべてそのメソッドをオーバーライドして実装しなければいけません。規模が大きくなればなるほど、その影響は甚大となり、コンパイルエラーやバグが多発する原因となります。

だからこそ、このインターフェースをどのように定義するかの設計を大事にする必要があります。ここがしっかり決まってしまえば、あとは使う側も使われる側も好きなように変更を加えることができるのです。
これが、どの要素よりもインターフェースが重要であるとした理由です。

カプセル化

カプセル化とは「インターフェースのとおりに実装する」ことを指します。インターフェースのとおりに実装することで、そのオブジェクトはインターフェースを介して別のオブジェクトから使用できるようになります。

例えば、インターフェースが下のように定義されているとします。

public interface Program {
    void execute();
}

インターフェースのとおりに実装する」とは、「インターフェースに定義されているメソッドだけpublicで定義する」と言い換えることができます。
これにしたがってProgramインターフェースを実装してみましょう。

public class ProgramImpl implements Program {

    private String str;

    @Override
    public void execute() {
        this.str = "Hello World!!";
        print();
    }

    private void print() {
        System.out.println(this.str);
    }
}

クラス自体のスコープを除くと、アクセス修飾子がpublicとして指定されているのはProgramインターフェースに定義されたexecute()メソッドのみとなっています。

考え方としては、「名前として定義したもののみを通してオブジェクトにアクセスさせる」というものになります。インターフェースを実装するクラスであれば、名前の定義はインターフェースに移譲されているので、インターフェースで定義されているメソッドのみがpublicとして指定できるということになります。

メンバ変数をpublicとして指定することは、名前を介してオブジェクト同士がやり取りをするというオブジェクト指向のルールを壊す結果となります。絶対にやめましょう

ポリモフィズム

ポリモフィズムについてざっと説明すると「インターフェースを介せば、処理を呼び出す側はインターフェースの実装クラスが何かを意識する必要はない」となります。

例えば、レストランに行った時のことを考えてみましょう。ハンバーグを注文することを決め、店員さんを呼ぼうとしています。
あなたはきっと「店員さ~ん」と声をかけるでしょう。そうすれば、たとえその店員さんが佐藤さんでも鈴木さんでも高橋さんでも、席まで来てくれて注文を聞いてくれます。その店員さんの名字を意識する必要はありません。

  • 私はもし店員さんが可愛かったら名字を意識します!というか覚えちゃいます!

このとき、「店員さん」がインターフェースであり、その実体は佐藤さんや鈴木さんや高橋さんとなります。
店員さんというインターフェースのおかげで、その実体が誰かということを意識することなく注文をすることができています。

プログラムにおいても、インターフェースの実装クラスを呼び出す側が意識しなくていいということはメリットが多いです。プログラムの修正としてインターフェースの実装クラスを変更した場合でも、呼び出し側のコードをほとんど修正しなくて済みます。また、動的にインターフェースの実装クラスが変更になるような場合でも、同じコードで呼び出せるので、複雑なコードが不必要になります。

ポリモフィズムを実現するためには、「呼び出す側のオブジェクトの変数の型はインターフェースにする」ことが必要です。

「カプセル化」の項目で登場したインターフェースとその実体クラスを呼び出す例です。

public static void main(String[] args) {
    Program program = new ProgramImpl();
    // 実装クラスを変数の型に指定しない
    // ProgramImpl program = new ProgramImpl();
    program.execute();
}

変数の宣言時に、型をProgramインターフェースで定義しています。
これにより、もしその実体がProgramImplクラスでなくなっても、メソッドの呼び出しの方法program.execute()は変更が不要になります。

継承

いきなりですが、継承は取り扱い注意です!
オブジェクト指向の3大要素に含まれているので、積極的に使っていくべきなのでは?と思われるかもしれませんが、安易に使うとオブジェクト指向の考え方を破壊してしまうので、要点を理解したうえで必要な場合だけ使ってください。よくわからなければ、継承は一切しない方が安全です!

まず、継承を使用するにはある条件があります。
スーパークラスをA、サブクラスをBとした場合に「すべてのBAである」場合のみ継承をすることができます。
例えばA自動車で、Bトラックであれば「すべてのトラック自動車である」となり、成り立つことになります。
これは「is-a」の関係と表現されます。

  • ちなみに、「is-a」の関係はまさに抽象化そのものです。「{具体} is a {抽象}」「すべての{具体}は{抽象}である」これはすべての具体と抽象の関係性で当てはまります。「すべてのマグロは魚である」「すべてのマクドナルドは飲食店である」といった感じです。また、いくつかの具体の共通点を抽出してグループ化し、そのグループに名前をつけることが「抽象化」です。「is-a」の関係はこの世の中に溢れています。

この関係でない場合は、クラスの役割がめちゃくちゃになってしまうので、一切継承は使用してはいけません

「is-a」の関係であれば、継承をしても構わないということにはなるのですが、私の見解としてはやっぱり継承はオススメしません!
なぜオススメしないかというと、カプセル化を破壊するからです!

カプセル化は「インターフェースの通りに実装する」と説明しました。インターフェースに定義されていないものはアクセス修飾子のPublicをつけてはならないと。
例えば、Carクラスでは、インターフェースの実装としてrun()メソッドのみが定義されていたとします。その子クラスであるTruckクラスは、インターフェースはCarクラスと同じなのでrun()メソッドのみpublicにできます。
え?トラック🚛なんだから荷物を積み込むload()メソッドをpublicで定義したい?インターフェースに定義されていないのでダメです!

つまり「メソッドを追加する目的で継承を使用しない」ということを守る必要があるのです。

インターフェース継承

「継承するな」と言われても、では「is-a」の関係は定義できません、となると困ってしまうと思います。
Javaの世界でこの関係を表すためには、インターフェース継承を使用しましょう!
これはとても単純な考え方で、「自動車」のインターフェースを引き継いで「トラック」のインターフェースを用意するというものです。

public interface Car {
    void run();
}

public interface Truck extends Car {
    void load();
}

このTruckインターフェースは、 run()メソッドとload()メソッドというプログラムの名前を持つことになります。
このインターフェースの実体となるクラスは、2つのメソッドをそれぞれ実装する必要があります。
これにより、「is-a」の関係でのメソッドの追加を行うことができます。

これだと、継承のメリットとしてよく言われる「処理の共通化」ができないじゃないか、と思われるかもしれません。ですが、「処理の共通化」というメリットをとるよりも、「カプセル化が壊れる」というデメリットを回避することを選ばなければならない、という風に考えてください。共通化は他の方法でもできますが、カプセル化への対策は継承しないことしかないのです。

コンポジション

上記のインターフェース継承では、「既存のインターフェースではなくクラスを独自に拡張したい」という要件は満たすことはできません。でもクラスの継承はしてはいけない。。。まったく同じ機能を持ったクラスを作って独自機能をそのクラスに足す?さすがにそれはアカンだろ、となりますよね。

そんな時に使えるのがコンポジションです。コンポジションとは「拡張元のクラスのインスタンスをprivateなメンバ変数として持ち、そのインスタンスが持つメソッドを呼び出すメソッドを用意する」ことです。

「自動車」と「トラック」の関係を例に、実装例を見てみましょう。

public class Car {
    public void run() {
        System.out.println("自動車は走りだしました。");
    }
}

public class Truck {
    private final Car car;

    public Truck(Car car) {
        this.car = car;
    }
    public void run() {
        car.run();
    }
    public void load() {
        System.out.println("トラックに荷物を積みました。");
    }
}
  • この例ではインターフェースの定義を省略しています。

Truckクラスでは、拡張元となるCarクラスをprivateなメンバ変数として定義しています。そして、コンストラクタにてそのCarクラスのインスタンスを受け取ってメンバ変数に格納しています。
Carクラスのrun()メソッドに対して、Truckクラスはそれをただ呼び出すだけの同名のrun()メソッドを定義しています。拡張元のクラスにあるpublicなメソッドすべてに対してこのように処理を転送するメソッドを定義してください。
それに加えて、Truckクラス独自の処理であるload()メソッドを追加で定義しています。

コンポジションを使用して元のクラスを拡張したクラスを、ラッパークラスと呼びます。
コンポジションを使うことにより、本質的に拡張元のクラスとラッパークラスが(依存はしているが)別のものとなるので、変更に強くなり、融通が利くようになります。

  • Effective Javaでは、コンポジションについて「転送クラス」というクラスを作成し、それを継承することによってコンポジションを実現する方法で記載されていますが、ここでは理解を容易にするため簡略化しています。

継承を使っていい場合

散々継承のことをディスってきましたが、「じゃあ継承はもう一切禁止なの?」と言われると、そうではありません。
一定の条件のもと、用法用量を守って正しく使えば、継承が持つ本来のパワーを活用することができます!

絶対的な条件となるのは「カプセル化を保つ」ことと「継承する方法が明文化されている」ことです。
前者については、おおもとのインターフェースに定義されていないメンバ変数やメソッドをpublicで定義しないということです。
後者については、Javadocであれ、なにかしらのドキュメントであれ、サブクラスを作る人がどのように作ればいいかを理解できるように文章で提示されている必要があるということです。

後者についてはもう少し詳しく説明します。
サブクラスはスーパークラスと一心同体となります。ですから、継承をするときにはスーパークラスのことを知らなければいけません。「知る」とは、ソースコードのレベルでスーパークラスを把握することです。そうしなければ、実装上の理由によりスーパークラスとサブクラスで思わぬ競合が発生し、コンパイルエラーや実行時エラーが起こる可能性がわずかにでも存在してしまうことになります。
ですが、複雑なクラスのソースのすべてを理解するのはあまり現実的ではないです。

そこで、継承されることを前提とするクラス(abstractクラス)を作る場合は、そのクラスを継承する方法や注意点をスーパークラスの作成者が責任をもって明示する必要があります。サブクラスの作成者は複数人になりますが、スーパークラスの作成者は一人ですので、解釈のばらつきがありません。

このことは、裏を返せば「継承されることが前提となっていないクラスは継承してはいけない」ということになりますし、「継承させる気のないクラスを作るのであれば、finalをつけて継承を不可能にするべき」ということにもなります。

継承される前提のクラスの例として、フレームワークや開発プラットフォームなどで、それらが用意した抽象クラスを継承して独自の処理を定義することで特定の機能を使用できる場合があります。この場合はおそらく、公式ドキュメントかJavadocに継承の方法が記載されていると思います。その方法に従って継承してください。

また、独自に継承される前提のクラスを作る例として、GoFのデザインパターンの中からTemplate Methodが挙げられます。これは、処理の流れを抽象クラスにメソッドとして定義し、サブクラスに独自の処理をさせたい箇所は、abstractメソッドとして定義してサブクラスに実装を強制させ、そのメソッドを処理の流れを定義したメソッドから呼び出すといったものです。
このパターンは、サブクラスが行う拡張の内容を制限し、なおかつすべきことを明確にするので、大規模な開発などで処理を共通化する場合に有効な手段となります。

例外として、「スーパークラスがパッケージプライベートであり、同じパッケージ内で継承が行われる場合」は継承をしても構いません。スーパークラスに変更を加えても、影響範囲がそのパッケージ内で収まるためです。同様に、外部にクラスを公開することのないプロジェクト内であれば、継承をしても構わないです。この場合でも、変更の影響範囲がすべて手の届く範囲にあることになります。といっても、大規模なプロジェクトであれば、変更の影響が手に負えなくなる場合もあるかもしれません。慎重に判断するようにしましょう。


  • 継承はとても解釈が難しく、「ここに書いたことが真理だ!」と自信を持つことはできないというのが正直なところです。ただそれでも「学んだこと、解釈したことのアウトプット」として書かないわけにはいきませんでした。先に謝罪しておきます。もし全然めちゃくちゃなことを書いていたらごめんなさい。
    (めっちゃ保険かけるやん!)

オブジェクト指向を乗りこなすには

オブジェクト指向は抽象的な概念であり、その奥深くまでを理解しきることはとても難しいです。
それでも私たちは、オブジェクト指向の言語で開発をしなければいけません。この難解さに打ち勝ち、味方につける必要があるのです。

そこで、いくつかの「オブジェクト指向を乗りこなすコツ」を示そうと思います。これがすべてではありませんが、裸一貫で立ち向かうよりは、1つでも多くの武器を持っていたほうがいいのではないでしょうか。

正しく名付ける

当記事では、オブジェクト指向を「プログラムに名前をつけるため」として紹介してきました。名前をつけることの重要性はご理解いただけたのではないかと思います。

ですが、ただ名付けるだけではいけません。大事なことはプログラムとして最適な名前をつけることです。
プログラマの方であればもう口酸っぱく言われてきているかもしれませんが、「正しい名前をつける」ことは常にこだわり続けてください!

プログラムとして最適な名前をつけるための指針は「名は体を表す」です。「わたしを呼び出すと、こういうことが行われ、ああいう結果が得られます」という情報を、メソッド名と仮引数名でできる限り鮮明に表すのです。

もっと詳細に伝えるために、Javadocなどのドキュメントを添えるのもいいでしょう。ですが、ドキュメントがあるからと言って名付けをおろそかにしてはいけません。名前にこだわることは最優先です。

適切な名付けはとても難しいです!正解はないものですし、経験や知識、センスも求められます。私自身、まだまだ修行中の身ですし、このために英語を勉強しようかと思うくらいです。成果が見えづらいのでこのスキルを向上させようというモチベーションも持ちにくいです。
ただ、それでも立ち向かわなければいけません!名付けはエンジニアに必須のスキルです。
極端に言えば、最新の技術を使いこなし、複雑なコードをかける技術のあるエンジニアだとしても、テキトーな名前しかつけられなければ雑魚です。
いくつかの参考文献を読み、ご自身の書くプログラムの中のクラス名、メソッド名、変数名それぞれに対し、良い名付けができるように練習していきましょう。

名付けの参考に

リーダブルコード – Amazon
どう足掻いても名著です。よい名付けの方法が明確に示されています。

リーダブルコードを3年ぶりに読み返してみて見つけた たった1つの大原則 – Qiita
リーダブルコードの名づけに関する項目についてわかりやすくまとまっています。

プログラミングでよく使う英単語のまとめ【随時更新】- Qiita
名前に使用する単語について網羅的に解説されています。

うまくメソッド名を付けるための参考情報 – Qiita
こちらの記事は要点スッキリといった感じです。

Naming -名前付け- – Qiita
わかりやすいです。

codic
日本語をいい感じにプログラムっぽく翻訳してくれるサービスみたいです。

依存関係をコントロールする

とあるクラスAが、クラスBのインスタンスを生成して使用している場合、「クラスAはクラスBに依存している」という状態になります。このような依存関係のつながりによって、ソフトウェアは構成されていきます。

なにも意識なしに好き放題依存関係を結んでいると、所謂スパゲッティコードと呼ばれるようなあちこちに処理が飛ぶソースコードとなり、大幅な修正やリファクタリングなんかはほぼ不可能な状態となってしまいます。
ですが、小規模なプロジェクトであれど、依存関係なしではソフトウェアは作れません。仲良くしていくしかないのです。

依存関係を乗りこなすにはルールが必要です。コードを書き始める前に、このルールについて調べて自らのプロジェクトに落とし込み、それを明文化してメンバーに周知しましょう。それほど重たい作業ではないはずです。このために使った3時間は、近い将来の5人月と同等の価値だと思ってください。

ルールを決めるために考えるべきことは、「グループ」と「依存方向」です。

例えば、よくあるMVCアーキテクチャのwebシステムを開発しているとします。このシステムを完成させるにはどんなクラスを作る必要があるでしょうか?

データベースのレコードを保持する「Entity
データベースへSQLを投げてアクセスする「Dao
業務処理を担う処理の中軸「Service
リクエストを各処理に振り分ける「Controller
画面と値を送受信する「Form
汎用的な処理はみんなで使おう「Utility

ざっと思いついたところでこんなものですかね。もちろんもっと細かな括りもありますが、一旦はこの辺で。
これらは、MVCにおけるクラスの役割ごとにグループ化したものとなります。
このグループを真っ先に定義してください。考えうるすべてのクラスを何かしらのグループに属させる必要はありません。ですが、「業務処理を書く」など大枠での役割が同じのものがいくつもあるような場合は、漏れがないようにグループ化してください。

グループを洗い出して定義できたら、そのグループ間で依存を許可する関係に矢印を引きます。
上記の例であればこのようになるかと思います。

例えば、「Controller」は「Service」に依存しても構いません。リクエストに対応するServiceを呼び出すのがControllerの仕事ですから、これは許可されるべきです。
ですが「Dao」から「Service」への依存は許可されていません。Daoの役割はデータベースアクセスであり、それらが業務処理を司るServiceを呼び出す理由はないはずです。

このように、グループごとの役割を考えて矢印を引いていきます。矢印はできるだけ少ないほうが望ましいです。
また、「ControllerService」のような双方向の矢印と、「Controller」⇒「Service」⇒「Dao」⇒「Controller」のような循環する矢印を引いてはいけません。このような矢印は依存関係をめちゃくちゃにします。

こうして決定した依存関係の矢印こそがルールとなります。あとは、プロジェクト全体がこのルールに従いながら依存関係を構築してコーディングしていくのです。

ここで行いたいのは「依存関係の管理」です。「依存関係を無くす」ことを目標にはしていません。完璧より最善なのです。
Daoが壊れればControllerに影響が出るのは当然のことです。これらすべての要素が関わってソフトウェアの一つの機能となります。
大事なのは、「依存される側の部品を変更する際に、その影響範囲を素早く漏れなく特定し、それに依存している部品への影響を最小限に抑える」という考え方です。それを実現するための仕組みがこの「グループ」と「依存方向」となります。

ここで書いている以上の、イマドキでスマートな依存関係を実現するためには、DI (依存性の注入) という概念を導入するといいです。奥が深くてここで詳しく触れてしまうと話が脱線してしまうので紹介だけとさせていただきます。
依存関係の参考に

プログラムの依存関係とモジュール構成のこと – Qiita
依存関係についての図解がわかりやすいです。

クラスの役割分担を明確にする

「いい名前をつける」「依存関係のルールを作る」と、オブジェクト指向を乗りこなす方法をお伝えしてきましたが、これらを行うために必ず考えるべき重要な要素があります。それは「クラスの役割」です!

役割分担が明確に出来ていると、それはクラス名に表れます。「名は体を表す」の前提のもと、クラス名を見てそのクラスの役割が鮮明に理解できる状態を目指して、クラスの役割分担をしてください。
役割分担の単位については、明確な指標はないです。「1クラス1テーブル」「1クラス1機能」「1クラス1画面」といった風にルールを設定してあげることも効果的だと思います。

もし、記述量の多いクラスを作ってしまったときは、コミットの前に「このクラスは役割を持ちすぎていないかな?」と自分に問いかけてみてください。重要な役割を複数抱えているようであれば、その役割に応じてクラスの分割を検討してあげてください。

役割分担が明確になっていると、そこからクラスをグループ分けして、依存関係のルールを設定することも容易になります。

  • ちょっと前に「けものフレンズ」というアニメが流行りましたが、それになぞらえて「あなたは○○をするフレンズなんだね!」と言いながら作ったクラスの役割を確認することをオススメします。試したことはないですが、たぶん有効じゃないかなと思います。試したことはないですが。

やりすぎない

ここまであーだこーだと書いてきましたが、最後を締めくくるのは「やりすぎない」です。
要するに「べき論にとらわれ過ぎずケースバイケースで書いていくんやで」ということです。我ながらひどい責任逃れですね!!

ですがこのケースバイケースというのを正しく判断するためには、その前段で言われている「べき論」の部分をしっかり理解する必要があります。「うまいことやる」ためにも、果てしないオブジェクト指向との戦いに挑むべきですね。

私もまだまだ修行中の身です。一緒に頑張っていきましょう!!

参考

オブジェクト指向と10年戦ってわかったこと – Qiita
ポジティブなコメントからネガティブなコメントまでちゃんと全部返していてすごいなと思いました。
小学生みたいな感想になりましたが、とても良い記事です。

具体と抽象 – Kindleストア|Amazon
名著。理解が難しい「抽象」について書かれているのに短くまとまっていて読みやすいです。

Effective Java 第3版 – Kindleストア|Amazon
偉大なるJavaのトリセツです。これに書いてあることがJavaの真理です。ただ、10ページぐらい読むだけで体力がごっそり持っていかれるほど理解が難しい。。。

Effective Java 第3版 「ほぼ全章」を「読みやすい日本語」で説明してみました。 – Qiita
ここまでまとめるのはどれだけ大変か、、本当にありがたい限りですね。
私もいつかはこれに準じたことをこのブログでします。Java熱が保たれていれば。。。

コメント

タイトルとURLをコピーしました