オブジェクト指向プログラミングとは、オブジェクト(もの)を中心に考えていくプログラミング手法のことで、数年前から特に注目を集めているものの、実際にはもっと古くからある手法です。
巷にある本の解説には、「もの」ということを取り上げて、解説していますがが、それではオブジェクト指向が何を意味しているのかわかりづらいものです。
オブジェクト指向に対するプログラミング手法には、手続き型プログラミングがあり、それと対比をするとわかりやすくなります。
以下の説明はまだ洗練されていないので、時々内容がアップデートするかもしれませんがご了承ください。
オブジェクト指向:Java, C++, Smalltalkなど
手続き型:Basic, C, COBOL, アセンブラなど
プログラミングとは、コンピューターに対する一連の命令を作成することです。
例えば、画面に文字を表示する、計算をする、などをコンピューターに対して命令します。その命令文は、たいてい目的語・対象が存在します。
ex)
表示せよ | HelloWorld |
平方根を出せ | 36 |
この命令を中心に考えていくのが手続き型プログラミングであり、対象を中心に考えていくのがオブジェクト指向プログラミングです。
例えば、ドア(door)を開けろ(open)、という命令をする場合、
手続き型では、 open(door) ;となりますが、
オブジェクト指向では、 door.open();となります。
つまり、オブジェクト指向では、もの・対象・オブジェクトが主役であり、命令(処理)はオブジェクトにくっついている脇役になっています。手続き型では、命令にオブジェクトを引き渡すのに対して、オブジェクト指向では、オブジェクトにメッセージを送って、オブジェクトがメッセージに反応してメソッドを実行するという形になります。
(簡略のため正確にソースを書いてません。またJavaのメソッドに当たるものは、Cでは関数と言いますが、まとめてメソッドと表現しています)
実際に、CとJavaのソースを見比べてみると、
Cのソース// オブジェクト(構造体)の宣言 typedef struct {...} door ; // メソッド(関数) open(引数){ ... } // メインメソッド(関数) main() { open(door) ; }
// オブジェクト(クラス)の宣言 class door { // メソッド open() { ... } } class Main { // メインメソッド main() { door.open() ; } }
Cでは、メソッドが直に書かれていますが、Javaではメソッドが必ずクラスの中に書き込まれています。
Cのソースは、@オブジェクト(変数)の宣言、Aメソッド、Bメインメソッドから構成されています。Cでは、オブジェクトとメソッドが別々に存在し、処理を行なう際は、メソッド(オブジェクト);というふうに、メソッドの引数としてオブジェクトを渡します。なので、オブジェクトの宣言を見ただけでは、そのオブジェクトに対してどういう操作が行なわれるのわかりません。
ところがJavaになると、オブジェクト(クラス)の中にメソッドが宣言されているので、そのオブジェクトに対して、どういう操作が行なわれるのか、ソースを見るだけでわかります。
ここでBasicも加えて、C、Javaのコードを対比すると、
--* Basic *-- 10 PRINT "Hello World" --* C *-- void main(int argc, char *argv[]) { printf("Hello World" ); } --* Java *-- public class Hello { public static void main(String args[]) { System.out.println("Hello World"); } }
Basic、Cはともに手続き型ですが、大きな違いがあります。Basicには(形式上)メソッドはなく、Cはメソッドがあり、Javaはさらにクラスがあります。
Cは構造化プログラミングといわれ、Basicで使われていた、GOTO文は使用せずに、for, while文などの制御文で済ませるようにし、またすべての処理を関数というブロックの中で行うようにします。
クラスが使われていても、Javaでも手続き型のようにプログラムを組むことができます。まずBasicライクに書くと、
class Main { main() { ・・・ ・・・ ・・・ ・・・ } }
となり、main()メソッドの中にすべてプログラムを書いてしまいます。これだとプログラムが長くなり、同じ処理を繰り返し使いたいとき、すべての箇所に同じことを書かねばならずかなり効率が悪く、見通しも悪いのです。そこで次にメソッドを追加して書きます。
class Main { int x ; // すべてのメソッドで共通に使う変数 int y ; main() { ・・・ a() ; ・・・ b() ; } a() { ... } b() { ... } }
しかし、これも行き詰まりを見せます。メソッドが増えてくると収拾つかなくなり、変数の扱いも面倒になります。
そこで、
class Main { main() { ・・・ A.a() ; A.b() ; B.a() ; } } class A { int x ; a() { ... } b() { ... } } class B { int y ; a() { ... } c() { ... } }
というふうにクラスを使って、メソッドを種類別に分け、変数もそれぞれのクラスに分散させます。
つまり、Javaではオブジェクトの中にメソッドがあるというふうにもいえるし、メソッドを集めたクラスに変数を保持することができる、ともいえます。
オブジェクト指向では、オブジェクト自身に状態を持たせることができますが、手続き型で同じことをするのは困難です(ただしオブジェクトへの参照を保持しておく必要はありますが)。手続き型では、基本的に呼び出し側で状態を管理する必要があります。
例えば、ある一連のデータを加工して何パターンかの出力を得たい場合、オブジェクト指向ではデータを最初にセットして、あとはその何種類かの出力を呼び出せばよいのです。手続き型では、基本的に毎度データを渡さないといけません。
※staticを使えば関数の中に状態を保持できますが複数の状態を保持することはできません。同じクラスの複数のオブジェクトとなると、手続き型では対応できないのです。
まとめると、Cなどの手続き型言語と違い、オブジェクト指向言語では、
1.データ(構造体)にメソッドを付属させることができる
2.メソッドの集まりに状態を保持させることができる。
というメリットがあります。
そして、通常、オブジェクトのメンバー変数はprivateにして、外部から直接操作させません。必ずメソッドを通して行なわせます。内部の構造は隠蔽し、外にはメソッドだけ(インターフェース)を見せるようにします。これがカプセル化です。
オブジェクト指向には、
のといった重要な特徴があります。
オブジェクト指向以前のプログラミングでは、オブジェクト(構造体)と手続きは切り離されており、オブジェクトの管理は手続きを使う側で管理しなければなりませんでした。ところがオブジェクト指向になると、オブジェクトに処理を依頼するだけでよくなり、データの管理はオブジェクト自身がすることになったのです。呼び出し側は内部構造について一切知る必要がないのです。オブジェクトは外に対してメソッドのみ提供します(=カプセル化)。それによって、今まで呼び出し側で行っていた処理の多くをオブジェクトに委譲できるようになりました。
また、オブジェクトは他のオブジェクトを継承して作ることができ、容易に拡張が可能になり、またこのことはオブジェクトをグループ化し、同一グループに対して同じインターフェイスでアクセスすることを可能にしました(ポリモーフィズム)。それだけではなく、実行時に使用するオブジェクトを切り替えられるようになったのです。
継承が重要なのは、容易に拡張が可能ということに加え、共通化ができるということです。オブジェクト指向では、オブジェクトの側にメソッドを書くということなので、当然出てくる疑問は、複数のクラスに同じメソッドを適用しなければならない場合、すべてのクラスに同じメソッドを書かなければならないのかというものです。回答は、それらのクラスに対して共通の親クラスを作り、親クラスの中に共通のメソッドを書き、その親クラスを継承してクラスを作成すればいい、ということです。
カプセル化によって、内部データを抽象化したのに対して、ポリモーフィズムではインターフェースを使うことで、実装を抽象化します。そうして、設計と実装を分離するのです。こうすることで、オブジェクト指向では抽象度を極度に高め、保守性・再利用性を高めようとしているのです。抽象化についての議論はこちらを参考にしてください。
オブジェクト指向の本質は、対象オブジェクトに関連する処理を任せてしまうことにあります。呼び出し側であれこれしなくてもよくなります。オブジェクト指向の利点は、呼び出し側・使用側の管理コストの減少にあります。手続き型では、呼び出し側で属性を管理する必要があり、実装をある程度意識する必要がありました。
オブジェクト指向を使うと、その管理をすべてオブジェクト側にさせることができ、インターフェースさえ知っていれば、実装のことは知らなくてよくなります。
それから、呼び出し側で関連する処理について条件文でいろいろとわずらう必要もなくなります。ポリモーフィズムを使えば、そこもすべてオブジェクト側に任せることができるのです。
「餅は餅屋で」ということであるべきところにあるべきものを持っていったという、ただそれだけのことです。管理コストがゼロになるわけでも、コーディングレスになるわけでもありません。そこが妙に大げさに取り上げられ、拡大解釈され、哲学や神話レベルに持ち上げられているのが現状です。
属性を意識させない仕組みとしてカプセル化があり、実装を意識させない仕組みとしてポリモーフィズムがあります。カプセル化によって実際にそのオブジェクトが属性を持っていようが、他のオブジェクトをラップしているだけなのかもどうでもよくなります。そのオブジェクトへの参照とインターフェースさえ知っていればいいのです。
プログラミングの記述は処理をだらだら書くのではなく、機能とデータを併せ持ったモノを「配置」していけばいい。その配置の組み合わせによって、アプリケーションを実現するのです。
オブジェクト指向の解説本で、しばしば構造化プログラミングとの違いについて述べていますが、そこでは、
・再利用性
・大規模なプロジェクト
という点では、オブジェクト指向のほうが有利であると述べられています。しばしば、著者が構造化プログラミングをよく知らずに書いているのではないかと見受けられるときがあります。
再利用性は、すでに構造化プログラミングでライブラリという形で実現されています。再利用性という点で何が違うのかというと、関数を使うかオブジェクトを使うかということです。関数で扱う構造体と関数をセットで利用すれば、オブジェクトと同じことが出来ます。確かに、オブジェクト指向の方が、構造化プログラミングよりもできることが多いので、その点で、再利用しやすくなっている部分もあるかもしれませんが、劇的に向上しているということはどの本も示せていないように思います。
継承によるオブジェクトの再利用については、オブジェクト指向で実現できることですが、最近は継承よりも委譲の方が推奨されている現状を見ると、特にこの点で再利用性が優れているとは言えなくなります。
ポリモーフィズムについては、構造化プログラミングにはないため、機能追加が容易になっているという利点は挙げられます。とはいえ、構造化プログラミングに対する利点は、あくまでも修正・拡張が可能なように設計した場合のみです。
これまで述べてきたオブジェクト指向の利点は実際には、すでにモジュール指向でも実現されていたことでもあります。モジュールもオブジェクトと同様に状態をカプセル化しています。違いはモジュールが1セットしか状態を持てないのに対してオブジェクト指向では複数持つことができる点(マルチプルインスタンス)です。
手続き型、構造化プログラミングをどのようにみなすかでオブジェクト指向のみにある利点というのは変わってきます。実際には、構造化プログラミング、モジュール指向、オブジェクト指向という区分で考えるのがより正確です。モジュールという言葉も人によって定義がばらばらで関数を指す場合もあれば、状態と手続きがセットになったものを指す場合もあります。モジュール指向という言葉が流行らなかったために、多くの解説書ではモジュール指向は取り上げず、モジュール指向の利点はすべてオブジェクト指向独自のものとされてしまっています。
参考
しばしばJavaの世界では、MVCなどレイヤーをきちんと区切ってプログラミングを行います。そのようにして行ったプログラミングをオブジェクト指向だと思っている人が多いのですが、私は違和感を感じます。確かに、そこではインターフェースを使ってプログラミングされ、ポリモーフィズムが使われているのですが、そこで出てくるオブジェクトのほとんどがシングルインスタンスなのです。
シングルインスタンスということは、内部に状態を持つことはできません。複数のスレッドから同時にアクセスするために、個々の状態を持つことはできません。処理も一回のメソッドの呼び出しで完了します。
EJBのセッションBeanでは、ステートフルSessionBeanとステートレスSessionBeanがありますが、デフォルトで、また推奨される方は、ステートレスの方です。ステートフルではセッションごとに状態を保持しなければならず、インスタンス管理が厄介になるため、あまり使われません。しかし、ステートレスということは、このオブジェクトはただ単に処理を受け持つだけの存在です。これは手続き型の手法と変わりません。単なる処理の分割は、オブジェクト指向ではなく、すでに構造化プログラミングで実現されていることです。
やはりオブジェクト指向らしさは、マルチプルインスタンス、ステートフルにあると思います。クライアントとオブジェクトの間で何度もやり取りする。オブジェクトが状態を持っているから、利用側が状態を管理する必要がない、これがオブジェクト指向の利点だと思います。
もっとマクロな視点で例えると、コマンドやバッチ処理が手続き的で、対話型のGUIアプリケーションはオブジェクト指向的な感じがします。プログラム、アプリケーションが単なるモジュールなのかオブジェクトなのかはこの辺で分かれる気がします。Windows上のアイコンはオブジェクト指向のいいたとえです。見かけ上プログラムが内包されているインスタンスです。実際には単にプログラムが関連付けられているだけなのですが。
デザインパターンにSingletonというものがあって、それがいかにもオブジェクト指向だというような形で紹介されているのも変です。Singletonは唯一あるインスタンスなので当然個別に状態を持つことはできまん。機能的にはstaticなメソッドだけを実装したクラスを作るのと代わりはありません。
ステートレスなクラスを作るのであれば、それは単に関数を分類するために、クラスが使われているだけでたいした意味はないと思います。
またDTOのように、単純なセッターとゲッターしか持たないデータ保持用のクラスも構造体となんら変わりません。
別に理想的なオブジェクト指向にこだわる必要はありませんし、SingletonやDTOを使うのには利点があるのでいいのですが、ただそれをオブジェクト指向だと思っていたとしたら、勘違いだと思います。
■オブジェクトは生き物か
初めてオブジェクト指向について学んだとき、オブジェクトは生き物だと教わったことがあります。しかし、実際それは正しくありません。生き物は自立的に動きますが、オブジェクトは単にコンピュータ上のメモリに置かれたデータに過ぎず、決められたとおりに決められたタイミングにしか動きません。
しかし、オブジェクトを自律したものととらえるのは、手続き型プログラミングから一歩踏み出すために必要な発想の転換です。
■メッセージパッシング
オブジェクト指向では、メソッド(関数)の呼び出しは、メッセージパッシングだと言われます。メソッドや関数を呼び出すという言い方は、呼び出し側が使っているという印象がありますが、メッセージパッシングになると、相手に処理を依頼しているという印象が強くなります。
オブジェクト指向では、オブジェクトをひとつの独立した主体と仮定し、そのオブジェクトが自主的に動くと考えます。メソッドの使用はそのオブジェクトへのメッセージとしてとらえ、呼び出し側が使っているのではなく、オブジェクトが自主的にメッセージを読んで動いていると考えます。メッセージとは、インターフェースの部分、つまりメソッドのシグネチャ、つまりメソッド名と引数と戻り値の型であり、メソッドとは実装、実際に処理を行っている部分のことで、メッセージとメソッドは厳密には異なります。
このことはメソッド名についてよく考えるとわかるでしょう。メソッドは「オブジェクトが」実行することです。しかし、メッセージは、他のオブジェクトがそのオブジェクトに対し依頼ないし命令することです。例えば、getMember()というメソッドを考えてみると、それはそのオブジェクトが自分のところに取り入れるという意味ではなく、外のオブジェクトにとっての意味です。オブジェクトが自立した存在で、オブジェクトがメソッドを持っていると考えると、これは混乱を招きます。もちろん、そのオブジェクトがどこからかMemberを取ってきて自分のお腹に蓄えるという意味で、getMember()というメッセージを送ることもできます。
基本的にメソッド名は他のオブジェクトとってのメッセージ名を表していて、getMember()というメッセージを送ることで、「Memberが欲しい」という意図を伝え、オブジェクトは受け取ったメッセージに応じて処理を行って結果を返すのです。
■オブジェクトが主体とは
オブジェクトが主体ということですが、実際には、オブジェクト指向言語を使ったとしても、単にプログラムの実行の制御がそのメソッドに移るだけで、プログラムを動かしている主体は呼び出されたオブジェクトでもなければ呼び出し側のオブジェクトでもありません。プロセスもしくはスレッドです(さらに言えばCPU、もっと厳密に言えば主体などないのですが)。
また、そのオブジェクト自体を別プロセス、別スレッドで動かして、呼び出す側と別個に動くようにすることで、「生きている」ようにすることはできます。それはそれでプロセス間通信やスレッド間の通信で大きなテーマですが、ここで言うメッセージパッシングはそういうことではありませんし、オブジェクトが動き続けるようなイメージを持つのは正しい理解ではありません。実際オブジェクトが生き物のように動き続けることはありません*。
ずっと動き続けるプログラム(=常駐プログラム)は、何らかのイベントが発生するのを待機しています。それはユーザが画面をマウスでクリックすることかもしれませんし、インターネット経由でリクエストが来ることかもしれませんし、タイマーが発生することかもしれません。そういうイベントが発生すると、常駐プログラムは動き出し、イベントやリクエストのデータを処理するオブジェクトに引き渡します。このとき常駐プログラムはオブジェクトの応答を待っている場合もあれば、新しいプロセスやスレッドを作ってオブジェクトに制御を渡し、自分は即座に再び次のイベントを待つようになる場合とがあります。いずれの場合も、オブジェクトに引き渡した段階からオブジェクト間でメッセージパッシングにより相互作用が起こり、処理が行われます。
*ウイルスのように無限に、増殖を試みる処理を繰り返していると生き物のように見えるかもしれませんが、それはオブジェクトの自立性とは違います。
次のように考えると、オブジェクト指向のメタファと実際が合致するかもしれません。各オブジェクトは基本的に待機状態にあり、他のオブジェクトからキックされると動き出し、仕事を終えるとまた待機状態に入る、という感じです。また、呼び出し側のオブジェクトも、依頼したオブジェクトがレスポンスを返してくるまで待機状態になります。
最初にキックするのは人間がコマンドを打つことですが、その後は次々とオブジェクトが呼び出され、最初に呼び出されたオブジェクトはプログラム終了までずっと待機状態にあるということもあります。生き物といっても、普段は何もしない生き物です。
ある意味、オブジェクトはサーバーに似ているかもしれません。他のオブジェクトからリクエストを受け取り、処理してからレスポンスを返します。呼び出し側はその間待機しています。
■擬人化するメリット
プログラムの処理を分割・分類する際、それぞれのオブジェクトが主体となって何かを行う、という発想にすると分類がしやすくなり、わかりやすくなります。機能だけに注目して、メソッドをグルーピングしたり、データだけに注目して構造体を作るよりも、データと機能を一体化した「オブジェクト」の方が主体性を持つことができるるため、理解しやすく、きちんと整理されます。
手続き型では、メソッド(関数)と構造体が別にあるので、どうしてもオブジェクトに依頼するという発想にはなりません。データがあって、それを加工するメソッドがある、という感じで、使う側ですべてを管理する中央集権的な発想になってしまいます。
では、オブジェクトのインターアクションを思い浮かべるとき、そこで使われるすべてのオブジェクトを考慮するのでしょうか。そうではありません。メソッドの中の処理は、intやcharなどの基本形を使った処理以外は、オブジェクトを使っています。Javaで標準に提供されているAPIを使用したとしても、それもメッセージパッシングによってオブジェクトに依頼しているのです。こうすると実際おびただしい数のオブジェクトが登場することになるのですが、設計を行う場合そこまで考えません。
クラスには、コントロールを行うクラス、主要な処理を行うクラス、仲介を行うクラス、データを保持するクラスとタイプが分かれます。設計を行う際には、自分で作るクラスの中で重要な役割を担っているクラスを登場人物として取り上げるのです。メソッドの中で既存のAPIを呼び出して使っている場合は、手続き型同様「使っている」という発想でよいでしょう。
*オブジェクトは、呼び出し元が使うともいえるし、メッセージを送って処理をお願いしているとも言えます。自分のメンバーに原始型があった場合は呼び出し元のオブジェクト自身が持っているといえますが、それ以外のオブジェクト型はメンバーにあったとしても、それは参照だけで、つまり連絡先を持っているに過ぎません。そのため、複数のオブジェクトが同じオブジェクトを持つということがあるからです。この点は関連が集約なのかそうではないのかで解釈が違ってきます。
以下の本に、同じプログラムを、手続き型とオブジェクト指向とで、それぞれ組んで対比している、よい例および説明が掲載されています。
『ゼロから学ぶオブジェクト指向設計』 日経ソフトウエアムック
これならわかるオブジェクト指向 Part4 構造化プログラミングには限界がある
*ソースはここで手に入ります。
またマーチン ファウラー『リファクタリング―プログラムの体質改善テクニック 』の本の最初のサンプルもオブジェクト指向とは何かを理解する絶好のサンプルです。手続き型との対比を見るのが一番の早道でしょう。
オブジェクト指向でプログラムを組む場合、主体となりうるものを取り出していきます。それらの登場人物を並べ、各役割を明確にしていきます。ポリモーフィズムを理解するとともに、この点を理解することも大切です。
規格の統一
何か類似した処理を行う場合、形は統一しておいた方がいいものです。ポリモーフィズムを使ったり、オーバーロードを使ったり、オブジェクトの作成を外出しにしたりして、何かの処理の際、インターフェースを統一することができます。
オーバーロードの場合、引数の数や型が違っても同じ名前で呼び出すことができます。
ex) System.out.println(int i); System.out.println(String s);
オーバーロードは、オブジェクト指向3原則に含まれていませんが、大切な特徴であることは間違いありません。
ポリモーフィズムでは、同じシグネチャで呼び出すことができます。
規格の重要性という場合、例としてCDを取り上げてみます。規格がないとどうなるかといと、A社のCDメディアとCDプレーヤーがあった場合、C社のCDメディアを買ってきても使えないということが起こりえます。C社のCDを使うためには、C社のCDプレーヤが必要となります。規格が統一されていれば、CDメディアとプレーヤーの結びつきが減少します。そうやって規格を統一することによって、依存性が減少し、モジュールとモジュールの間の結合が、疎になるのです。
ポリモーフィズムは多態性と訳されますが、動的結合(実行時に使用するオブジェクトを決定する)によって、switch文やelse if文を排除することができます。手続き型だと、条件分岐の追加が必要になりますが、オブジェクト指向だとサブクラスを追加するだけでよく、そのサブクラスに決められた手続き・メソッドを実装させればよくなります。
ex) ポリモーフィズムを使わない場合
void exec() { if (条件A) { ... 処理A ... } else if (条件B) { ... 処理B ... } }
ex) ポリモーフィズムを使った場合
void exec(Model model) { model.処理(); } class ModelA implements Model { void 処理() { ... 処理A ... } } class ModelB implements Model { void 処理() { ... 処理B ... } }
ただ、しばしば例に出てくるものでは、外出しにして該当のクラスからif文やswitch文を排除したとしても、呼び出す側で使っていたりするものです。例えば、上記の例で言うと、exec()メソッドを呼び出す際に、
Model model; if (条件A) { model = new ModelA(); } else if (条件B) { model = new ModelB(); } exec(model);
としてしまうと、結局同じif文が別のところで出てきてしまいます。外に追い出したとしても、どこかでついて回ってきます。結局、どこかで条件に応じて指定しなければならないのだから、やっぱりif文やswitch文は避けて通ることができません。
とはいえ、これでも意味のあることなのです。できるだけ具体的なものは外に追いやり、アプリケーションの中核部は抽象度の高いものにすることができるのです。そうすることで汎用性のあるアプリケーションを作成することができます。
外・外・外に追いやった具象クラスの指定は、最初にキックするメソッドまで追い出すことができますが、さらに完全にソースからif文やswitch文を追いやることもできます。それは外部ファイルにクラス名記述し、リフレクションの機能を使うことです。さらに条件もプロパティファイルに書いておくと、動的に条件文を生成することができます。また、具象クラスの指定は実行時の引数で指定することもできます。
パターン1 aaa.properties loadclass=com.xxx.AbcFactory パターン2 aaa.properties value.1=com.xxx.AbcFactory value.2=com.xxx.XyzFactory
外部ファイルに記述したクラスやメソッドをプログラムの中で使用するには、Javaの持つReflectionパッケージを使用します。外部設定ファイルに、クラス名やメソッド名を書いておいて、実行時に指定文字列を元にインスタンスを生成したり、メソッドを呼び出したりすることができるのです。
例)最初のプロパティファイルから読み込むメソッドは別個作ります
String sClassName = readProperty("EXECUTE_CLASS"); Class clazz = Class.forName(sClassName); Object obj = clazz.newInstance(); Method method = clazz.getMethod(sMethodName ,new Class[]{ Parameters.class}); Results result = (Results)method.invoke(obj, new Object[]{parameters});
カプセル化とは、オブジェクトの内部の変数をprivateにして外からの直接のアクセスを禁止して、メソッド経由でアクセスするようにすることです。最初これがよくわかりませんでした。というのは、多くの場合、publicのsetter,getterを作ってメンバーにアクセスできてしまうので、結局のところ何も変わっていないと思いました。
実際、setterやgetterに特別な仕掛けがない限り、また大半のクラスが全部setterとgetterを実装している限りは、実質上カプセル化の意味がありません。もちろん、これでも内部構造が隠蔽され抽象データ型となり、すべてメソッドを通じてやり取りするように統一されているという意味はあります。ただ厳密にカプセル化を実現するためには、public,protected,package,privateの4つのアクセス制限だけでは足りないでしょう。特定のクラスにのみアクセスを許すような仕組みが必要です。Javaで呼出元をチェックすることは可能ですが少し厄介です。
また、setterやgetterを実装する場合、以下のことに気をつけなければなりません。
ex)class A { private Vector vec; public Vector getVec() { return vec; }
これはカプセル化されているでしょうか? これはカプセル化ではありません。なぜならメンバーへの参照が持ち出され、何をされるかわからないからです。class Aの知らないところで勝手にAのメンバーが変わってしまう可能性があります。とはいえ、こういうケースは頻繁にあります。この場合仮にvecへの参照を返さなくても、vecに格納されているオブジェクトへの参照を返せば同様の問題が起きます。
結局コピーを渡すか、Immutableオブジェクトを返すのでないかぎり厳密なカプセル化とはなりません。Immutableパターンでは、コンストラクタでは引数のクローンをメンバーに持たせ、セッターは使用せず、ゲッターではコピーを渡します。
この場合はCollections.unmodifiableSet()メソッドを使って、読み出し専用のオブジェクトにして返します。
自分しか参照を持っていないのは、コンポジット集約といい、そのオブジェクトとの結びつきが強く、自分が消滅すればそのオブジェクトも消滅します。参照が共有されているのは、知り合い(aquaintance)といい、そのオブジェクトの結びつきは、コンポジット集約より弱く、自分が消滅してもオブジェクトは消滅しません。メンバーが真にカプセル化されているのは、コンポジット集約の場合です。
最近の解説本では、従来のオブジェクト指向の「現実世界を反映してプログラミングする」ということに批判が集まり、本来のプログラミングがどのようなものであるかを明らかにしようとしています。
オブジェクト指向はあくまでも比喩であって、比喩を本当のことだと思って、コンピュータがそのとおりに動いていると思うと間違います。
現実世界を模した形で行うのは、あくまでもプログラムを組む人間のためです。コンピュータが処理しやすいしにくいを判断するわけではありません。むしろオブジェクト指向の方が処理時間やリソース使用量などで劣っています。プログラムを書く人間が、ソースコードを理解しやすく、拡張しやすくするためのものです。
逆にオブジェクトを現実世界にたとえると、人にたとえられます。人といってもただの人ではありません。仕事をする人、何かのサービスをする人です。ただの人と仕事をする人の違いは何かというと、ただの人は、何か命令しても、従うとは限らず、何かの刺激を与えたとしてもどう動くかはわかりません。自発的な意思を持っているからです。オブジェクトが自発的ではないのはこの点から明らかです。
ところが、仕事をする人は、役割を持っているので、その動作はかなり規定されています。なので、リクエストを送れば、レスポンスは決まっています。もちろん、人間なので、能力差はあるし、間違いはありますが。
そしてこれは、人が行なうビジネスにマッチします。そもそもコンピュータは人が行なうことを代わりにするものです。人が行なう遊びや感情表現の代わりではなく、仕事の代わりです。だから、オブジェクトはみな仕事を持っています。
つまり方向が逆なのです。現実世界をプログラムの世界に反映させるのではなく、プログラムの世界を擬人化、擬物化する方が適切なのです。
データ保持を中心とするオブジェクトは、物にたとえられ、処理を中心とするオブジェクトは人にたとえられます。
参考