オブジェクト指向と10... - Qiita
分析結果
- カテゴリ
- IT
- 重要度
- 45
- トレンドスコア
- 9
- 要約
- オブジェクト指向と10年戦ってわかったこと #プログラミング教育 - Qiita 5759 いいねしたユーザー一覧へ移動 5788 X(Twitter)でシェアする Facebookでシェアする はてなブックマークに追加する more_horiz 記事を削除する close 一度削除した記事は復旧できません。 この記事の編集中の下書きも削除されます。 削除してよろしいですか? キャンセル 削除する delete info この記事は最終
- キーワード
オブジェクト指向と10年戦ってわかったこと #プログラミング教育 - Qiita 5759 いいねしたユーザー一覧へ移動 5788 X(Twitter)でシェアする Facebookでシェアする はてなブックマークに追加する more_horiz 記事を削除する close 一度削除した記事は復旧できません。 この記事の編集中の下書きも削除されます。 削除してよろしいですか? キャンセル 削除する delete info この記事は最終更新日から5年以上が経過しています。 @ tutinoco オブジェクト指向と10年戦ってわかったこと オブジェクト指向 プログラミング教育 プログラミング作法 5759 最終更新日 2021年01月09日 投稿日 2015年11月18日 この記事の内容 オブジェクト指向は難しい!わかった気になって実践すると詰みます... ウギャー この記事は10年以上オブジェクト指向と戦った筆者が、通常とは異なるアプローチでオブジェクト指向を解説したものです。 筆者はJavaを使って本格的なシステム開発をしたことがありませんが、オブジェクト指向言語として最もポピュラーなJavaをベースにオブジェクト指向について解説させていただきました。 また、この記事の続編にあたります「 なぜオブジェクト指向は難しいのか 」を更に2年の時を経て執筆させて頂きました!是非こちらも一読していただけると嬉しいです。 オブジェクト指向三大要素の謎 オブジェクト指向三大要素ってありますよね。オブジェクト指向は「カプセル化」「継承」「ポリモーフィズム」の3つの要素で成り立つと言われています。最近では、この三大要素が語られる傾向は薄いようですが、一度は耳にしたことがあるのではないでしょうか? この「オブジェクト指向三大要素」ですが、実はオブジェクト指向を理解する大きな妨げになってしまっているのです。 オブジェクト指向に不可欠なのは「ポリモーフィズム」であり、***オブジェクト指向を超えて重要な原則は「カプセル化」と「正しい名前付け」***です。 これから三大要素の「継承」「ポリモーフィズム」「カプセル化」を解明していきます。それから、なぜ「カプセル化」と「正しい名前付け」がオブジェクト指向を超えて重要なのか解説させていただきます。 それでは本題に入る前に、なぜオブジェクト指向で書くのかという根本的な所から立ち返ってみましょう。 なぜオブジェクト指向で書くのか なぜオブジェクト指向で書くのか考えたことはありますか?意外にもこのことを意識せず、なんとなくオブジェクト指向でプログラミングしてる方は多いと思います。 オブジェクト指向で書く理由、それは 変更に対して柔軟に対応 するためです。 プログラムは日々変化する必要があります。更新しなければ、そのプログラムは水を与えられない植物のようにジワジワ枯れていきます。植物を育てるには定期的にメンテナンスする必要があるように、プログラムもまたメンテナンスが必要です。GitHubなどでフレームワークやライブラリが日々更新されているのは、プログラムが枯れないようにするためです。 オブジェクト指向によるアプリケーション開発は、変更されない箇所を軸に、 頻繁に変更されるであろう箇所をクラスに抽出する プログラミングスタイルです。 例えば、店舗がどんどん増えていくファーストフードのシステムを開発することになったとしましょう。店舗が増えていくということは、そこは「頻繁に変更される箇所」なのでクラスに抽出して設計する必要があります。そうすると近い将来起こる「店舗が増える変更」に対して柔軟に対応できるようになります。 一見、システムの柔軟性というものは素晴らしいものに思えます。しかし柔軟性を取り入れすぎると、逆にシステムのメンテナンスを面倒なものに変えてしまうことがあります。そのため、柔軟性と保守性のバランスが大切です。 今回例に取り上げたのは、ファーストフードのシステムなので「食品以外のものを販売する」というような変更が発生することは、あまり考えられません。「もしかしたらガソリンやタイヤを販売するかもしれないじゃないか!可能性はゼロではないのだから柔軟に対応できるように設計するべきだ!」と思うかもしれません。しかし、将来的に変更のない箇所を無駄に柔軟に設計してしまうことは過剰実装であり、システムの設計を複雑なものに変えてしまいかねません。 システムには、変更の可能性が「高い箇所」と「低い箇所」とが存在し、オブジェクト指向は、変更の可能性が低い箇所を土台に、高い箇所に気を配って設計する必要があります。そのため、オブジェクト指向でシステムの設計をするときは、予め変更が起こりうるであろうポイントを広い視点で予測整理しておくことが大切です。 なぜオブジェクト指向で書くのか?それは、予め 頻繁に変更されるであろう箇所をクラスに抽出する ことで、システムが 変更に対して柔軟に対応 できるようにするためなのです。 また、オブジェクト指向の最大の価値は「わかりやすさと利便性」にあります。このことにつきましてはこの記事の続編にあたる「 なぜオブジェクト指向は難しいのか 」をご覧下さい。 さて、オブジェクト指向でなぜ書くのか大まかな理由がわかったところで、次は「継承」「ポリモーフィズム」「カプセル化」の三大要素を解明していきます! 継承 みなさん大好き継承。継承は親クラスの機能を受け継ぐ機能です。しかしこれは継承の本質ではありません。 継承の本質はインターフェイス です。Javaでは interface を使ってインターフェイスを定義できますが、継承もまたインターフェイスと同じ役割を果たします。 オブジェクト指向は難しいですが、継承は簡単に理解できるため、オブジェクト指向をわかったつもりになれます。これは、オブジェクト指向の混乱の原因のヒトツです。 クラスの継承と interface の違いは、継承はスーパークラスから機能を受け継ぐということです。 そのため、継承はクラス同士の関係が「AはBである」と表現できる時にクラスを抽象的にまとめられるものということになります。つまり「馬は動物である」はOK。「虫はトンボである」はNGです。 そんなの当たり前だ!本にたくさん書いてあるよ!しかし、どうもコードの海に溺れていると他のクラスの機能を使いたいがために安易に継承してしまい、気が付いたらこの「AはBである」という原則を破ってしまうことがあります。 ひどい時なんかは、子クラスで重複したメソッドやプロパティをなんでもかんでも親クラスに定義した神クラスが出来上がり、オブジェクト指向でプログラミングしない方がよかったのでは?といった状態になることもあります。 抽象(スーパークラス)とは実態の無いただの概念です。犬や猫の髭を引っ張ることはできても、誰も「動物」という抽象的なものに触れることはできません。もしあなたが何らかの動物に触れているのならば、それは「動物」ではなく「犬」や「猫」といった、もっと具体的なものになります。 オブジェクト指向は時折「現実世界をそのままプログラムに表現できる」と言われますが、いったいこれはどう解釈すれば良いでしょうか?もしも何らかのスーパークラスを修正した場合、この修正を現実にどう反映して解釈できるのでしょう? じつのところ、解釈できないと思います。無理やり解釈するならば 「スーパークラスを書き換える」ということは「魚」と「カエル」の間のような抽象的遺伝子情報を操作し「もしも遺伝子がこうなってたら〜!」と呪文を唱えて世界を再構築することを意味する でしょうか。 いったいこれのどこが「現実世界をプログラムに表現」なのでしょうか!?確かに、人は遺伝子操作が可能なったので、このように置き換えて解釈することはできるかもしれません。しかし、こんな解釈では混乱を深めるばかりです。わかりやすくするためにわざわざプログラムを現実世界に当てはめたはずなのに... 実は「生命」を取り除くことで、オブジェクト指向の世界をイメージしやすくする思考法があります。「ものづくり」の世界に創造できないものを持ち込むと混乱するのです。 人は神様ではないので、現実世界でモノを作るときに「車」や「自転車」は作れても「犬」や「猫」のような生きた動物は作れません。人が作るモノは基本的に「カラクリ」であり、「車」は、エンジン、ハンドル、ブレーキ、ホイール、などの部品で構成されていて、機能の受け継ぎなど行っていません。 詳しくは「 オブジェクト指向にdogやanimalを持ち込むと混乱する話 」をご覧ください。 継承の説明がなされるとき、必ずと言っていいほど「犬」や「猫」を使った動物の例え話がなされ、実際にDogやCat、Animalといったクラスを作成したことがある方もいると思います。 しかし、システムを構築するときに作成するオブジェクトは、エンジンやタイヤような部品であり、それらをまとめ上げた車などを作ります。 継承は、親クラスから機能を受け継ぐためのものではなく、 継承の本質は、交換可能なパーツを作成するために共通点を「規格」としてまとめ上げられるインターフェイス なのです。 こんなことを言うと混乱させてしまうかもしれませんが、オブジェクト指向ではクラスを拡張する目的で継承を利用することもできます。これは、既に存在する具象クラスの役割を後からインターフェースの役割に転換させるようなことを可能にしますが、考え方は違っても技術的には同じことをしているだけです 継承が機能受け継ぎでない証として、異なるクラスの機能を利用するために「オブジェクトコンポジション」を利用するという方法があります。コンポジションを使えば人がパーツを組み合わせて「車」を作るような自然なものづくりができるし、実際に継承よりコンポジションの方がよく使います。 コンポジションの使い方は、下記のコードを見ただけですぐ理解できるでしょう。 Engine engine = new JetEngine (); Handle handle = new QuickHandle (); Brake brake = new AntilockBrake (); Wheel wheel = new StudlessWheel (); Car car = new Car ( engine , handle , brake , wheel ); このように作った方が、自然なオブジェクト作りができるだけでなく、インスタンス生成時にエンジンを変えたり、ハンドルを変えたりすることが容易となり、プログラムに柔軟性が生まれます。 組み合わせごとに大量のクラスを作る必要もありません。よく作る組み合わせのオブジェクトがある場合には、それらを生成するファクトリを作成すれば、何度も部品から作る手間を省くこともできます。 継承は親クラスの機能を受け継ぎますが、これは開発効率を上げるための優しさ的仕様であり、継承の本質はインターフェイスなのです。もしあなたが継承の本質を「機能の受け継ぎ」と解釈してしまった場合、オブジェクト指向はあなたに牙を剥くでしょう。 ポリモーフィズム 継承の本質はインターフェイスであると説明しましたが、ポリモーフィズムはその インターフェイス(抽象・規格)に対してプログラムする ということです。 もっと具体的に言うと Animal animal = new Dog(); としたり Animal animal = new Cat(); としたりして、犬だろうが猫だろうが動物だよねってことで、動物という抽象概念に対してプログラムするということです。 Animal animal = new Dog(); animal.bark(); // dog.bark();でないため抽象に対してプログラミングできている このように抽象クラスに対してプログラミングすることで、抽象クラスに属するクラスのインスタンスは、何でも動かすことができるようになります。 Javaにおいてはクラスの継承の他に、 interface を使うことができますが、この interface は、犬と車を「鳴く奴ら」という概念でまとめて、犬も車も「鳴く物」として扱うことができるというものです。犬は「ワン」と鳴き、車は「ぷっぷー」と鳴きます。 このポリモーフィズムの考え方はプログラムに留まりません。例えば、電子レンジは食べ物を温めてくれる便利な道具ですが、電子レンジの本質は「マイクロ波を出す装置」です。そのものの本質を理解していると意外な使い方ができたりします。電子レンジに「食べ物を温めるもの」という制限はありません。(説明書には変なもの入れるなって書かれてるだろうけれど!) 身近なもので言えば、iPhoneもまたポリモーフィズムに溢れていると言えます。iPhoneはAppleが発売前には想像もしなかったアプリやアクセサリが登場しました。 コードも同じ。なるべく様々な使い方ができる様に、本質的な、抽象的なコードを書くことが大切です。そして抽象に対して作用するプログラムを構築すればポリモーフィズム(多態性)が生まれます。 特に意識していないのにtoString()メソッドが機能して思わぬメッセージが出力された経験はありませんか?あれは、まさに想定していなかった動作がポリモーフィズムによって問題なく機能した瞬間です ポリモーフィズムの理解が深まったところで、視点を変えてみましょう。 ポリモーフィズムを意識したコードを書くには「抽象」が大切ですが、抽象ばかりに気を取られてはなりません。抽象に対してプログラムするということは、逆に 具象に対してプログラムしないようにする ということです。 ここで衝撃的な事実をお伝えしましょう!***実は「new」は具象です!***ですから、ポリモーフィズムを意識する上でnewの扱いには最大限の注意を払う必要があります。 実は、 new はポリモーフィズムを破壊するとんでもない奴です。 new を使わずにプログラムが動けば良いのですが、必ずどこかで new を使わなければならない。では、どこで new すればいいのでしょうか?そうです、ファクトリです! ポリモーフィズムの破壊を閉じ込めるため new をクラスに抽出するということです!つまり、以下のようにします。 Animal animal = animalFactory.create('dog'); animal.bark(); 一見 new Dog(); を遠回しに実装しただけじゃないか!と思うかもしれませんが、この遠回しが重要。 ファクトリの内部では new Dog(); が行なわれているため animal 変数には Dog インスタンスが代入されます。しかし、ファクトリを通してインスタンスを取得すると実態は Dog であるものの Animal 型のインスタンスが得られることとなります。そのためファクトリを使ってインスタンス生成したプログラマは否が応にも抽象度の高い Animal インスタンスを扱うことを強要されます。 そのため、何も考えずともファクトリを使ってインスタンス生成していれば、抽象に対して自然とプログラミングすることができ、ポリモーフィズムの破壊が守られます。 もしファクトリを利用せず animalFactory.create('dog'); を new Dog(); にした場合、困ったことにそのコードを書いたクラスは Dog クラスに依存してしまい Dog クラス無しでは動かなくなってしまいます。 しかし、ファクトリを通して Dog インスタンスを Animal として受け取れば、クラスの依存は Dog から抽象の Animal へシフトし、具象への依存を避けられます。 何言ってんだ!新たに Factory の依存が増えるじゃないか!と思われるかもしれません。しかし重要なのは、他のプログラムに依存するクラスの数が増えるこではなく、具象クラスに依存してしまわないようにすることです。 プログラムにはレイヤーが存在し、低レベルレイヤーのプログラムが、高レベルレイヤーのプログラムに依存するようなことがあってはなりません。もし、レイヤーの異なるプログラムが依存してしまった場合、抽象度の高いプログラムはモジュール性や疎結合性を大幅に失うこととなります。 自分の作ったプログラムがどのプログラムに依存しているか簡単に見分ける方法があります。 import です!ソースコード上部にまとめて記述されることの多いこの import を見ればそのプログラムがどのプログラムに依存しているかがわかります。そしてもちろん、具象に対する import が使われていないほど、そのプログラムは疎結合性が高いということであり、コードの再利用性があること表します えー!ファクトリ作るとか面倒すぎ!と、思うかもしれません。しかし、必ずしもファクトリを作る必要はありません。実は、ファクトリをこんなにオシた僕はほとんどファクトリを作成したことがありません。 もし、ファクトリの必要がないプロジェクトにファクトリを作成してしまったら、それは過剰実装です。前述した「ファーストフードでガソリンを売ることを考慮」すること同じで、システムを複雑にし保守を面倒なものに変えてしまいます。 そのため、ファクトリを作るまでもないインスタンス生成はメインクラスで行うようにしましょう。メインクラスで new したインスタンスを他のインスタンスに渡すのです!メインクラスはファクトリと同様 new の利用が許された場所です。 なぜなら、メインクラスは調理場のような存在であり、メインクラス自体に疎結合性やモジュール性は必要ありません。そのため、メインクラスが new の接着剤でベトベトに汚れてしまっても困ることはありません。 もちろんメインクラスとファクトリ以外でも new の利用が許される箇所があります。 Scene クラスや Page クラスや Router クラスを継承したサブクラス内部などがそれに当たります。しかし、それらサブクラスに対してもコンポジションを利用してメインクラスからインスタンスを渡した方がインスタンスを再利用できるというメリットがあるため、結局のところメインクラスかファクトリ以外で new することは無いかもしれません。また、 String のような言語レベルで実装されたクラスの new は疎結合性をまったく破壊しないので気にしなくて良いです また、このような new したインスタンスをコンストラクタに渡して利用する手法は、インスタンスの無駄な生成が省けるだけでなく、コンストラクタのパラメータを確認すれば、そのクラスが何を必要として動くのか一目瞭然となります。しかもこの実装方法どこかで見たことありますよね?そう、コンポジションです! そのためパーツから自動車を作るような自然なオブジェクト作りができることに繋がるだけでなく、柔軟性をも持たせることができるのです。 ここで言うコンポジションは、オブジェクト注入(Dependency Injection)と言うべきかもしれません。コンポジションは、あるクラスに他のクラスのインスタンスを持たせることで、そのクラスに存在しない機能を持たせることができるテクニックであり、オブジェクトを外から動的に注入して使うことが多いため実質DIと同じと言えます。しかし、コンポジションは厳密にオブジェクト注入の意味を含みません。今ではDIと呼ばれるよりわかりやすい用語が存在するため、オブジェクト注入(Dependency Injection)と呼んだほうが良さそうです。 カプセル化 実のところ、***最も重要かつ難しいのがこの「カプセル化」***です。カプセル化は、継承やポリモーフィズムとは比較にならないほど重要なものです。 そんな大げさな!と思うかもしれませんが、事実「カプセル化」はオブジェクト指向どころかプログラミングを越えた重要な原則となります。(僕はプログラミング以外の分野でカプセル化という用語をよく使います) カプセル化のことをゲッターとセッターだと思ってる人がいますが、これは大きな間違いです。カプセル化とは抽象化のことであり、外から見てそのものが複雑でない状態を作るということです。そしてその状態を作るのはとても難しいのです。 僕は自動車に詳しくないので、自動車のカプセルを開けたら(つまり車を分解したら)バラバラになった自動車を元に戻せなくなるでしょう。しかし、僕はそんな自分では管理しきれない複雑な鉄の塊を運転することができます。なぜなら、ハンドルを操作しアクセルを踏めば前に進むということを知っているからです。 しかし、小刻みにブレーキを踏まなければスリップし、小まめにギアチェンジする必要があり、カーブするときは倒れないように体重移動をしなければ倒れてしまう自動車があったら、僕はそれを運転できません。複雑だからです。しかしブレーキにABSを搭載し、ギアはオートマチックで、カーブするときは倒れないよう重心が設計されていれば、あれこれ余計なことを考えずに運転できます。 つまり カプセル化とは無駄を省き洗練させてわかりやすいものを作るということ です。 もしもエレベーターにアクセルとブレーキが搭載されていたら、出勤時はエレベーターをギロチンマシーンに変えぬよう、最善の注意を払って運転しなければならなくなります。もちろん、プロのエレベータードライバーが24時間つきっきりで操作してくれるのであれば、もしかしたら現在よりも無駄な動きの無い素早い移動を提供してくれた可能性はあります。 しかし、エレベータードライバーという職業は残念ながら存在しません。どうやら初期のエレベーター設計者は、エレベーターを手動でコントロールさせることは危険だと判断し、アクセルとブレーキの概念をカプセルの内側に閉じ込めておいてくれたようです。そのおかげで、私たちもエレベーターをボタンで操作することができるようになりました。 このカプセル化を行う上で意識しておくと良いことがあります。それは クラスの役割は一つにする ということです。 クラスの役割が一つ以上になってしまうと洗練されたカプセル化からかけ離れてしまうことになります。これはビールの栓抜き借りたが、よく考えたら十得ナイフを持っていた。というような話に関連づけるとわかりやすいかもしれません。 十得ナイフは便利ですが、コンピュータの世界において十得ナイフは必要ありません。もし現実世界においても四次元ポケットが存在したら十得ナイフはいらなくなるでしょう。栓抜きを必要とした時、四次元ポケットから何を取り出すでしょうか?わざわざ十得ナイフを取り出して十得ナイフの栓抜きを使うようなことをするでしょうか?答えはNOです。四次元ポケットからは栓抜きを取り出して使います。 このようにコンピュータの世界は四次元ポケットが存在する世界なので「なんでもできる便利な道具」より「何ができるか明確な道具」の方が利便性が高まることになります。プログラミングの世界では欲しい時に欲しいものを手に入れることができるため、十得ナイフのような複数の異なる役割を持ったクラスやモジュールは必要ないのです。 また、カプセル化とは直接的な繋がりはないものの、カプセル化に強く関連する重要な仕上げが存在します。それは、 正しい名前付け です。 自動車には「自動車」という名前が、栓抜きには「栓抜き」という名前が付けられています。もしあなたが何かをカプセル化した場合、そのものにまだ名前が付いていないならば、それに 正しい名前をつけるということが、カプセル化の最後の仕上げ となります。 適切な名前付けの重要性については「 正しい名前を付けることが大切な理由 」にも記載させて頂いております。 実はこの「無駄を省き洗練させてわかりやすくする」というカプセル化はオブジェクト指向の原則というより、デザインの原則でありデザインそのものなの