オブジェクト指向に関する世界観(A.考え方)です。オブジェクト指向も参照のこと。
自分の書いた「エリカの技術・芸術日記」2021/08/19より。
オブジェクト指向のクラス設計のコツは、オブジェクト同士のメッセージのやり取りであると考えること。また、クラスは全てのオブジェクトを階層的に規定すると考えることだ。
オブジェクトとは何か。オブジェクトは、その機能を提供し、必要なデータを保持するものだ。
オブジェクトは、それぞれ単純なものでよい。そのオブジェクトを組み合わせて、プログラムを作るのだ。
自分の書いたブログ「神々とともに生きる詩人」2021/01/13より。
オブジェクト指向とは、ものを表すクラスを作る、ということです。
クラスごとに、データと手続きを実装します。
たとえば、みかんは食べ物です。みかんクラスは食べ物クラスを継承します。
食べ物クラスでは、全ての食べ物に共通の「味」「価格」などの属性があり、「食べられると栄養を与える」という機能があります。
同様に、電話クラスをスマホクラスが継承し、電話で話すことができる機能は、スマホクラスにも引き継がれるのです。
電話の内部的な情報はprivateメンバ変数で、インターフェースとなる「聞く」「話す」はpublicメソッドで実装し、そのインターフェースから、電話を使うのです。
また、クラスベースのオブジェクト指向は、そのコードの見た目から「データ構造に対して手続きを実装する」と捉えられることが多いですが、これは正しいです。
たとえば、リストデータ構造ならばList、ディレクトリならばDirectory、ファイルならばFileというクラスにし、これらに共通のnextItem()やgetContents()やopen()などのメソッドを実装することで、オブジェクト指向によるプログラミングは行われます。
もっと言えば、DirectoryとFileは同じ「基本的データ保管構造」から継承するべきであり、この基本的データ構造をDataと名付け、DirectoryとFileはDataの機能を継承し、Data[]を包括するStorageクラスを作って、実際の物理的ストレージのクラスを作ればいいのです。
このような時に、「デザインパターン」と呼ばれるクラス設計のテクニックを知っておくと、さまざまなクラス設計に役立ちます。
また、よく入門書などで「クラスは設計図、インスタンスは製品」と呼ばれることもありますが、これも正しいです。
しかしながら、僕はあえて言えば、「クラス図は回路図」のようなものであり、「メソッドは機能を提供する機械部品」のようなものであり、「メンバ変数は機械内部の詳細な情報」であり、「インスタンスはひとつひとつの部品の組み立て・製造」であり、「オブジェクトのメソッド呼び出しは機械を使うインターフェース」であり、「プログラム全体はオブジェクト全てを統括するオーケストラの演奏」のようなものだと考えると良いと思います。
自分の書いたブログ「未来のわたしの心より今のあなたへ」2021/03/22より。
プログラミングについて言えることとして、「枠組み」を作る、ということが大切です。
たとえば、Webフレームワークを実装するのであっても、まず大まかな枠組みを作り、コンポーネント間やクラス間の関係を決めます。
クラスは、何を管理し、何を行うのかから、オブジェクト指向で設計します。
クラスの実装においても、インターフェイスが重要で、作る全ての機能を、ごく簡単なものであっても、無駄なくひとつひとつ、プロパティとメソッドにします。
プロパティはカプセル化し、メソッドは汎用的な枠組み通りに作ります。
そして、できたフレームワークを継承し、「枠組みとなる骨に具体的に肉をつけていく」のです。
このような枠組み作りのために、オブジェクト指向のクラスやインターフェイスを使います。
基本としてシステムに用意されている、クラスやインターフェイスは、単に便利な機能を提供するだけでなく、さまざまな場面で、「枠組み通りにはめ込む」形で使われます。
マルチスレッドだったり、あるいはイテレータだったり、さまざまな場面で、こうした「枠組み通りに組み合わせる」ということが、大切になります。
枠組みを作るコツは、管理するデータや情報ごとにクラスを分けることです。
管理する情報ごとにクラスを作り、情報に関連した実行内容をメソッドにします。
継承を行う上では、is-a関係も重要です。
たとえば、Applicationクラスでは、一般的なさまざまなアプリケーションのための基底クラスとして、すべてのアプリケーションに共通の機能を、決められた枠組みの中で作っていきます。
そのように、オブジェクト指向のプログラミングとは、まさに「枠組みを作る」ことなのです。
IT土方のように見えるかもしれませんが、やっていることは人間の手助けであり、人間が考えたり仕事をしたりするのを助けるのだ、ということを忘れないでください。
プログラミングとは、人間のやっていることを代わりにやってあげたり、人間のできないことを機械の力で行うことです。
タスクの自動化や趣味のツール作りで、プログラミングを楽しんでください。
後日注記:実際、E-R図ではないが、実体と関連性を考えればいい。それぞれのクラスの実体があって、その間に関連性がある。そのように考えることで、クラス設計は成り立つ。たとえば親クラスの実体と関連性を書いて、それらを継承した子クラスを作ればいい。関連性は親が子になっても変わらないか、あるいは継承されて発展的になる。これこそ、正しいクラス設計だ。
自分の書いた「ニュース - 2021-05-第二週」2021/05/07より。
オブジェクト指向のような考え方で、よく「カプセル化」とか「隠蔽」という言葉がでてきますが、実際、カプセル化の意味とはなんでしょうか。
それは、「インターフェースが変わらなければ、内部はどのように変わってもいい」ということです。
メソッドの名前や、オブジェクトの使用目的、外部から見たそのオブジェクトの動作などが変わっていなければ、内部がどのように変わっても構わないということです。
これは、外部の利用者が「同じコードが動く」というだけではなく、内部の実装者も「インターフェースと動作が変わらなければ、どのように変えることも可能である」ということを意味しています。
よって、複数人での開発の際、あるいは同じコードをさまざまな場所で再利用する際に、カプセル化によって得られる恩恵は大きいのです。
また、オブジェクト指向について簡単に述べると、オブジェクトは「永続データ」であり、メソッドは「そのデータに対する操作と編集」であると言えるでしょう。
たとえば、二分探索木を用いた二分探索を行うのであれば、どこかに二分探索ツリーを保持し続けなければいけません。
C言語では、グローバル変数としてこのツリーを保管することができますが、プログラムが複雑化してくると、二分探索ツリーをすべての場所から編集できないように「守る」必要がでてきます。
このような時、オブジェクト指向の機能を使うことで、クラスのインスタンスとして二分探索ツリー、すなわち永続データを「保管」し、探索を行う時はクラスのメソッドから「データを操作・編集」するようにすることができます。
ここでは、二分探索木による二分探索を行うために、「ツリー保管クラス」を作り、このツリークラスを包有する「二分探索管理クラス」を作って、このクラスのメソッドとなるルーチンとして、ひとつひとつの単語を二分探索する「単語検索メソッド」を作るのです。
ここで重要なのは、「オブジェクトにデータが保管される」ということです。今の現在の状態を示すデータ情報は、すべてオブジェクトのインスタンスの中に保管されます。そして、このデータを使ったさまざまな機能や、データに対して変更をかける機能は、すべて「データの一部」としてメソッドで実現されるのです。プログラムがあってそれがデータを操作するのではなく、データがあってプログラムと呼ぶことができるようなルーチンを「データの一部」とするのです。これが、オブジェクト指向のポイントです。
つまるところ、ここで言えることは、「データは永続的に保管され続けるが、データに対する操作はその場その場において呼び出されるだけでよい」ということです。これは、PHPとMySQLの関係に近いでしょう。MySQLにはデータが常に永続的に保管されていますが、これをPHPスクリプトを場合場合によってインスタントにその時その時実行することでWebサービスは実現されています。オブジェクト指向についても、これと全く同じであると考えることができます。
MySQLとオブジェクト指向で違うのは、MySQLではデータは物理的にデータベースに記録されますが、オブジェクトのインスタンスはメモリ上にあるものであり、プログラムが終了すれば消えてしまいます。このような時、データを「永続化」するために、O/Rマッピング(ORM)という機能を使うこともあります。Oはオブジェクト指向のオブジェクトのこと、RはRDBMSのリレーションのことです。ORMはRuby on RailsのActiveRecordなどでも用いられている技術です。
オブジェクト指向の本質とは、「モデリング」をすること、そしてそれを「使う」ことにあると思います。
クラスのインスタンスは、さまざまな状況や場合において使われます。まず、さまざまなユースケースを想定し、オブジェクトがどのように使われるかを想定します。
オブジェクトがどのように使われるかを定義出来たら、そのオブジェクトのクラス設計を行います。どんな場合においても対応できるように、データ構造を設計し、そのデータに対して作用や処理を行うことのできるメソッドを実装します。
そして、クラス設計ができたら、このクラスをインスタンス化し、さまざまな状況において利用します。C++ではポインタと同様に*をつけますが、Javaなどであれば標準でオブジェクトは参照型となり、オブジェクトをさまざまな場所からメソッドを通じて参照できます。
つまり、「ひとつのクラスをモデリングして、そのクラスを実体化したインスタンスをさまざまな場所から(時にたくさん作って)利用する」ということがオブジェクト指向の基本になります。
必要なのは、モデリングとユースケースをどちらも考えることです。そのオブジェクトが、「ひとつのオブジェクトの内部でどのように動くのか」ということ、「どのような状況でどのように生成・破棄されて利用されるのか」ということ、この二つを照らし合わせて考えるのです。
わたしたちは、オブジェクトを扱う上で、「そのオブジェクトの内部の骨格」と、「メモリ上におけるオブジェクトの具体的な利用状況」の二つを照らし合わせて考える必要があります。そうでなければ、どんなプログラムの動作状況も考えることができないのです。
また、ここで特徴として、
1.一つクラスを作ってしまえばいくらでもオブジェクトを作れる
2.一つクラスを作ってしまえばどんな場合にでも対応できる
ということが言えます。たとえば、フォルダの情報を表示するのであれば、フォルダクラスを一つ作るだけで、どんなにフォルダが増えてもフォルダクラスひとつで対応でき、同時にどんなフォルダであっても汎用的なひとつのフォルダクラスで対応できます。また、フォルダにさまざまな機能をつけるのは継承で行い、フォルダをファイルと同じように扱うためにはインターフェースの実装を使えばいいのです。
オブジェクト指向の本質は、「データが中心にあり、それに対する機能をつける」ということです。
中心にあるのはデータです。データを確保し、データを参照することが先にあり、それに対する処理となる機能をつけていくことで、オブジェクト指向プログラミングは行われます。
また、ある意味、機能となる処理は、それ自体が外部へ提供されるインターフェースです。データがあり、機能があり、それに対するインターフェースがあって、オブジェクト指向は成立するのです。
データが中心となり、データに対する処理を書くことでプログラミングを行うのは、一見奇妙に見えるかもしれませんが、たとえば一般的なOSではファイルというデータがまずあり、そのファイルに対する機能としてのプログラミングを行うことがあると思います。
オブジェクト指向はこれと同じです。まず、確保すべきデータを定義しておいて、そのデータに対して参照や変更を行う処理を記述し、外部へのインターフェースとして提供することで、オブジェクト指向によるプログラミングは行われるのです。
オブジェクト指向がなぜ「大規模な開発に向いている」と言われるのか、それは「内部の実装の詳細が変わっても、外部から参照される時のAPIやインターフェースは変わらない」からです。
オブジェクト指向においては、インターフェースを記述し、その内部の設計をクラスとして「カプセル化」することで、もし内部の実装の詳細を変更する必要があっても、そのクラスの内部だけを変えればよく、APIをそのまま保つことで、他のプログラムコードには影響せずに実装を変えることができます。
そして、オブジェクト指向においては、「本当に必要なところだけを変える」という考え方のもと、たとえばAnimalクラスからCatクラスやDogクラスを継承して、「必要最低限の記述」を行うことができます。
そう、カプセル化と継承を用いることで、柔軟かつ最低限の、必要な部分だけの編集を行い、ほかの部分への影響を最小限にできるのです。
オブジェクト指向のプログラムコードは、そうでないコードに比べて、再利用がしやすいことが多いです。
新しいプロジェクトで、「あれ?このプログラム、以前自分が(あるいは他人が)ほかのプロジェクトで書いたプログラムと同じことをやってるな?」と思うことが、あると思います。
そのような時に、オブジェクト指向のライブラリやコンポーネントを作っておいて、あとで再利用することはとてもメリットがあります。
また、わざわざオブジェクト指向で作らなくても、構造体と関数で作ればいい、と思われるかもしれません。
ですが、構造体をもしC言語で作ったとしても、その構造体は必然的にオブジェクト指向に近いものになります。
たとえば、スタックを作るのであれば、単にそのデータを格納する構造体だけではなく、pushとpopの操作をするなんらかのAPIが必ず必要になります。スタックのデータ構造と、スタックを扱うための関数処理は、セットになることが多いのです。
キューの場合も、エンキューとデキューの操作は必ず必要ですし、ツリーであれば新しいノードの作成や、トラバース(上から順に走査していくこと)の操作は明らかに必要です。「データとメソッドを分けることはそもそもデータ構造の設計として適切ではない」のです。
データ構造を扱うプログラムであれば、データ構造自身とデータ構造を扱うためのメソッドは、どちらもセットで必要となります。だから、オブジェクト指向で設計するといいのです。
実際、オブジェクト指向の継承やオーバーライドが、もっとも顕著に表れた例としては、MFCやRuby on Railsなどのフレームワークではないかと思います。
MFCやRailsでは、継承やオーバーライドを多用します。これらの開発では、継承やオーバーライドを使うことが前提であり、原則、継承やオーバーライドを使わずにこれらのフレームワークを用いたソフトウェアを開発することはできません。
これらのフレームワークは、「基本的なクラスはフレームワークの方で用意してあり、既存のクラスを継承・オーバーライドして自分のクラスやメソッドを定義する」という、オブジェクト指向のスタイルでプログラムを開発します。
実際、覚える要素は多いかもしれませんが、MFCやRailsなどではスケルトンの自動作成機能などを用いることで、とても高い生産性でプログラムを開発することができます。