オブジェクト指向に関する世界観(C.考え方3)です。オブジェクト指向も参照のこと。
オブジェクト指向とは、プログラミングについての現代の設計方針やパラダイムのことです。
オブジェクト指向においては、まず、「オブジェクト」という特有のデータがあります。これはデータであり、クラスと呼ばれる設計図を元にした、インスタンスと呼ばれる「実際のデータが格納されるもの(=オブジェクト)」をまず第一に取り扱います。そして、プログラムはこのオブジェクトの「操作を行う作用」として、オブジェクトの作用の中に「隠れて存在する」ようになります。
つまり、昔ながらのプログラミングでは、プログラムの中にデータがありました。フローとして流れるプログラムのさまざまな場面で、変数を定義して代入したり、参照したり、変更したりし、それらすべては変数名やメモリアドレスによって場所が決まっていました。
オブジェクト指向では、逆に、データの中にプログラムがあります。データの設計図であるクラスがまず最初にあり、プログラムはクラスを外部から操作するメソッドとして存在します。データをインスタンス化した段階で、データはそのオブジェクトひとつ分、メモリに確保されます。そして、そのインスタンスに対してメソッドを呼び出すことで、このデータを外部から操作することができます。
オブジェクト指向の利点とは、「変更の容易さ」と「安全に扱える」というところがあります。
クラスは継承と呼ばれる変更差分の適用によって、ある一部のメソッドだけを変更することができます。継承したクラスでは、継承元のクラスのすべての機能を、派生クラスから利用することができ、派生クラスでは親クラスに機能を付け足したり、上書きしたりすることができます。また、同じ継承関係を持っているクラス(同じ親クラスを元に継承した派生クラス)は、同じようなインターフェースから扱うことができ、これをポリモーフィズムと言います。
また、オブジェクト指向の設計を行うことで、プログラムデータを安全に扱うことができます。メソッドにはアクセス修飾子(アクセス属性)を指定することで、外部から参照・変更できるのか、できないのか、あるいは継承する際だけにできるのかを指定することができます。これにより、データを不正に書き換えられたりする恐れを防ぐことができます。
後日注記:C++やJavaなど多くの言語にprivate, public, protectedのようなアクセス修飾子がありますが、Pythonにはこれがありません。Pythonでは原則すべてのメソッドが公開されます。ですが、マングリング(メソッド名の先頭に_や__などをつける名前つけ慣習)によって、外部から操作してほしくないメソッドであることを明示することができます。
一部の内容で以下の書籍を参考に執筆しました。
以下のQiitaの記事に、オブジェクト指向という考え方に辿り着くまでの歴史が書いてあり、参考になります。基本的に、モジュール化における「良い結合」とは、副作用に依存せず、カプセル化し、データとアルゴリズムをまとめることであり、そこから抽象データ型(つまりクラス)が生まれた、ということのようです。
僕は、オブジェクト指向は単に「もの」というより、すべてのものを指す、抽象的な言葉だと思います。
たとえば、そこにあるテレビはものです。パソコンもものです。本や書籍ももの、ノートももの、鉛筆ももの、棚や機械もものです。
オブジェクト指向では、こうしたすべてのものを指す言葉としてもっとも一般的な言葉、すなわち「オブジェクト」という用語を使います。
この世界にあるあらゆるものはオブジェクトです。地図であっても地球儀であっても、オブジェクトであることには変わりがありません。
ですが、地図や地球儀には、ノートや鉛筆にはない性質があります。それは世界各国の地図を表しており、それぞれの地図となる「地図や地球儀のようなもの固有のデータがある」ということです。
また、地図や地球儀には、固有のデータだけではなく、固有の「ふるまい」があります。それはそれぞれの国の名前と場所を教えてくれるということです。
このオブジェクト固有のデータのことを「フィールド」や「属性」とし、固有のふるまいのことを「メソッド」とします。
そして、オブジェクトは階層的に、まず「もの」から「文房具」という派生クラスが生まれ、親クラスの「文房具」の子クラスである「鉛筆」や「消しゴム」があります。鉛筆や消しゴムは、文房具が持つすべての機能を引き継ぎます。それは、たとえば鉛筆もマジックもペンも同じ「筆記用具」であり、筆記用具には「文字を書く」という機能がありますが、筆記用具から派生した「絵筆」では「色のある絵を描く」という機能が追加されます。
このように、この世界にあるすべてのものを、すべてのものを指す言葉として「オブジェクト」と命名し、そのオブジェクトを論理的に分類していくこと、そこにあるデータとふるまいをひとつひとつ派生させていくこと、そして、その「もの」たちをきちんと上手くプログラムが働くように構造化し、クラスライブラリとして提供すること、これが、オブジェクト指向であると僕は思います。
クラスやインスタンスの考え方は、このような論理的なオブジェクトの考え方をベースにしているのです。GUIツールキットはまさしくツールキットという名前の通り「道具箱」ですが、むしろオブジェクトひとつひとつが「部品」のようなものであり、オブジェクトを組み合わせて別のオブジェクトを作っていく、と考えれば、オブジェクト指向が工場のようなものであり、わたしたちは工場そのものを作っているのだということが分かることでしょう。
オブジェクト指向について言われることとして、クラスはオブジェクトの設計図を表す「型」であり、インスタンスはその型によって作られた、ひとつひとつの固有のデータと記憶領域を持つ「製品」であるということが言われます。
たとえば、よく出されるのはたい焼きのたとえ話です。たい焼きの型がクラスに書かれたオブジェクトの設計図であり、ひとつひとつのたい焼きはクラスから生成されるインスタンスである、という話です。
これには、英語の「インスタンス」という言葉を考えるとよく分かります。インスタンスとは「実例」とか「実際の状況」とか「ひとつひとつの実製品」という意味です。
プログラミングを行う上で、プログラムの手続きとなるコードと、それが使われる「具体的な状況」を分けて考えることは大切です。たとえば、x + yをプリントする単純なプログラムは、xとyが与えられればどんな数値でも計算できます。x = 3、y = 4の状況では3 + 4 = 7を出力し、x = 10、y = 20の状況では10 + 20 = 30を出力します。
オブジェクト指向では、単に具体例を代入するだけではなく、それぞれのオブジェクトが固有のデータを保持することができます。たとえば、x = 10、y = 40を保持するオブジェクトと、x = 2、y = 8を保持するオブジェクトでは、クラス定義が同じでも別々のデータを常にそれぞれの記憶領域に保管しています。保持されているデータを変えるには、public宣言されたメソッドをそれぞれのオブジェクトに作用させなければなりません。
そう、オブジェクト指向とはこのような考え方です。プログラムというものは、そもそもデータとプログラムを分けて考えることはできません。データはその都度違ったものを保持し、プログラムはその具体的な状況に応じて違った動作をします。このように考えることで、バグのないプログラムを記述することができるのです。
たい焼きのたとえ話については以下の書籍が参考になります。
オブジェクト指向を本質的に理解するためには、「オブジェクト同士のメッセージのやり取り」であると考えることです。
オブジェクトとは、要するにプログラムとデータが確保されるメモリ領域を一緒にしたものです。
プログラムの中で、オブジェクトをクラスから生成・破棄し、そのオブジェクトにメッセージを与えたり、オブジェクト同士がメッセージ通信を行うことで、オブジェクト指向は成り立ちます。
ひとつの流れるフローにプログラムを書くのではなく、オブジェクトひとつひとつがデータを持った「ふるまいとしてのプログラム」となっており、そのオブジェクトが複数集まって、互いにメッセージをやり取りすることで、オブジェクト指向は行われます。
オブジェクトは、それぞれ共通の(同じものから派生した)命令規則を持っており、クラスは論理的な継承関係(クラス階層)を持ちます。このように、「ものとものとが話し合う」ことで、プログラミングを行うこと、これがオブジェクト指向の基本となります。
オブジェクト指向のクラス設計のコツは、アプリケーションにとって必要な機能ごとにクラスを考えることです。
まず、アプリケーションを作る際に、そのアプリケーションにとって必要な機能とは何かを洗い出します。
そして、その機能から必要なクラスを具体化し、データ構造とメソッドを持ったオブジェクト指向のプログラムにします。
そして、汎用的なルーチンを記述し、それを利用する制御部分を書いて、どのような制御モデルで動くのかを具体化すれば、プログラムは記述できます。
ここまでうだうだとどうでもいい理屈を述べてきましたが、オブジェクト指向を簡単に表現すれば、データを中心に関連するメソッドをまとめるだけです。
メソッドで扱う共通のデータがあって、そのデータの作成・参照・変更を行う観点から、関連する機能をまとめるだけです。
まず、プログラムに必要な機能を、「データ」という単位で分析しましょう。必要なデータと、その間の関連性(包有関係、継承関係、やり取りの関連性など)を洗い出しましょう。そして、このデータひとつひとつに対して、「機能」をつけていきましょう。これが、オブジェクト指向のプログラミングです。最後に、イベント駆動などの「制御」を作ることで、プログラムは完成するのです。
なぜ、このようにクラスによってデータを中心にまとまったプログラムのことを、オブジェクト指向と言うのでしょうか。
それは、プログラミングの世界では、ひとつひとつのまとまったプログラムのことを「オブジェクト」と呼ぶからです。
たとえば、gcc -c hoge.cをすると、hoge.oが吐き出されます。このhoge.oのことをオブジェクトファイルと呼びます。
そのように、コンピュータの世界では、プログラムのまとまりのひとつひとつの単位のことをオブジェクトと呼びます。
オブジェクト指向とは、クラスと呼ばれる設計図をもとに、プログラムをプログラムの中でオブジェクトとして扱い、オブジェクトとして操作する方法であると捉えることができます。
C言語などでは、int型の整数やchar型の文字は演算子とリテラルと変数名で、そのまま操作できます。
ですが、場合によっては、単に操作するだけではなく、複雑な処理が必要になることがあります。
このような場合、C言語では、上から下へと流れるフローによって、データを操作します。
ですが、もし、ある決まった「型にはまった操作の流れ」があるのであれば、それをすべてひとつのmain関数に書くのではなく、関数やサブルーチンにしたほうが楽ができます。
しかしながら、たとえば、ある決まった型の操作がもしあるのであれば、データを操作する際、いつもその操作を使ってデータを処理するように、データに付属するAPIとしてカプセル化することはできないでしょうか。
たとえば、配列を用いてスタックやキューを実装する際に、ポップやプッシュ、エンキューやデキューといった操作が必要になります。
サブルーチンを使わない場合であっても使う場合であっても、この操作はデータを処理するために必須の「定型の流れ」となります。
データに絶対にこの操作が必須ならば、むしろ「データを操作する時はいつでもこのAPIで処理をしなければいけないようにする」という発想が生まれます。
そして、まさしく、これがオブジェクト指向です。
オブジェクト指向では、データの操作に必須の処理をインターフェースとしてラッパー化します。データを処理する時には、必ずその操作を行って、「ラッパーの上から操作しなければならない」ようにします。
こうすることで、プログラミング言語の表現力の向上につながります。たとえば、C言語では、四則演算の演算子はあっても、より高度で複雑な処理をしたければ、当たり前ですが自分で書かなければいけません。関数にすることでサブルーチンにすることはできますが、まだ何かが欠けています。
C++やJavaなどのオブジェクト指向言語では、まず中心となるのをプログラムの流れとフローではなく、データにします。そして、そのデータを外部からやり取りする、ひとつひとつの定型的な方法、すなわちメソッドを定義するのです。
こうすることで、制御を上から下へと書く、という制限は、より柔軟なデータを中心としたラッパーであるメソッドになります。
そして、このようにすることで、むしろ、考え方がシンプルになります。なぜなら、プログラムをサブルーチンを用いた「ジャンプ」によって考えるのではなく、データを中心とした「インターフェース」として、「外部と内部を明確に区別して記述」することができるからです。
そう、クラスとメソッドにより、データはインターフェースをラッパー化し、外部とのやり取りをいったん排除して、内部だけに注目してクラスを記述し、利用する時は、外部とインターフェースの「やり取りの関係」だけを考えればいいのです。
そういうわけで、オブジェクト指向は、大規模なプログラミングに向いているのです。
後日注記:たとえば、スタックのデータ構造を作るとして、たとえばmain()関数に配列を定義した上で、main()関数の中にその配列の宣言から初期化、プッシュ、ポップ、破棄のコードをそのまま書くこともできますが、このような場合、生成とプッシュとポップと破棄の操作をクラス内の配列に付属するメソッドやコンストラクタ・デストラクタにして、データを中心としたオブジェクトの一部にしてカプセル化したほうが、明らかに楽ができますし、同じコードが分散しないため、再利用性や保守性も高いです。このオブジェクトを生成・操作・破棄する時は必ずメソッドを通じて行うようにするのです。
(放送大学「コンピュータとソフトウェア ('18)」を参考に、メートル単位ではなく通貨の例として執筆しました。)
オブジェクト指向の考え方の実例として言えるのが、データに付属的な属性を付け加えるような場合です。
オブジェクト指向は、データを単なる単一の変数ではなく、さまざまな属性を持った「オブジェクト」として扱う、という考え方です。
たとえば、通貨の場合を考えてみましょう。データを単に数値で管理している場合、その金額が日本円によるものなのか、米国ドルによるものなのか、区別できません。
そのため、プログラマが注意を払ってプログラムを実装しなければ、このデータを日本円で管理していたのに、いつの間にか米国ドルの値が入ってしまったり、米国ドルとの意図しない足し算が行われることもあるかもしれません。
このような場合に、通貨オブジェクトに、その金額だけではなく、どの通貨単位でその金額を表しているのか、という情報を追加します。
そして、金額を円で取得するにはgetYen()メソッド、円で金額を代入する時にはsetYen()メソッドで、データを操作することにします。
そして、円が必要なすべての場面で、このオブジェクトを、データそのものにアクセスするのではなく、getYen()メソッドあるいはsetYen()メソッドからアクセスします。
こうすることで、プログラムの保守性が向上して、バグを減らすことができるのです。
オブジェクト指向における「カプセル化」が、その中のデータを外部の不正な操作によって破壊されないように「隠蔽する」ために存在するなら、「継承」は、その中身を自由自在に書き換えて、データやメソッドの操作の処理を「付け加える」ために存在します。
オブジェクト指向では、一方ではクラスの中身を「守る」ためにカプセル化を行いながら、もう一方ではそのクラスの中身を「改造する」ために継承を行います。
そのように、カプセル化と継承は「外部からの操作インターフェースを用意する」という意味では対義語のようなものであると考えられます。カプセル化によって変更できないように内部の処理構造を守り、継承によって変更できるようにその中で変更してもよい箇所を決めて、この2つの観点からインターフェースを用意するのです。
クラス設計の基本は、クラスとクラスの連携手段と分割の方法を考えることです。
連携とは、つまり、クラスからどのようにクラスが使われるのか、ということです。
継承、包有、あるいはクラスのメソッドとして別のクラスの引数を与える場合など、クラスとクラスはさまざまな方法で連携します。密接に繋がり合うこともあれば、緩やかに連帯する場合もあります。
クラスの設計をする際には、また、「1対1」「1対多」「多対1」「多対多」といった、オブジェクトが複数になる場合のリレーションシップも重要です。
また、クラスをどのように生成するのか、どのように機能を使うのかということも重要です。クラスの内部で別のクラスのオブジェクトを生成するのか、それとも外部から注入してやるのか、機能と値は内部データとして保持するのか、継承で保持するのか、それともメソッドの引数として外部へのインターフェースを提供して行うのか、などが重要になります。
(以下の内容は、自分がDiscordのD-Techs Circleに書いた内容に基づきます。)
まず、オブジェクト指向とは、グローバル変数をできるだけ使わず、代わりにデータを中心に関数(メソッド)などの機能をまとめて、オブジェクトと呼ばれる単位でプログラムを作ろうという考え方のことです。
また、誰かが作った既存のプログラムに、正しい方法ではあとになってから機能を追加できるようにし、正しくない方法ではプログラムを使えないようにするということです。
Javaのクラスライブラリは、使いやすくまとまっていますが、基本の機能だけでは十分でない場合、クラスライブラリの機能を継承して、自らの思う機能を追加することができます。そのために、多くのクラスをpublicで公開しています。
従来のC言語の開発を複数人でする場合、グローバル変数などの名前付けがかぶらないようにするために、Web担当者はweb_、データベース担当者はdb_などの名前を変数名などにつけなければなりませんでした。名前空間やオブジェクト指向のある言語では、代わりにオブジェクトを使うことができます。
C++やJavaのクラスは、一見すると構造体の中に関数を詰め込んだような見た目をしていますが、実際はグローバル変数の手法に近いものです。クラスのメンバ変数は各メソッドから共有され、同じクラスの中でさまざまなメソッドからメンバ変数にアクセスしたり変更したりできます。ですが、この内部に存在するメンバ変数は、外部には公開されないようにすることができます。
そのような考え方の結果、オブジェクト指向では、大規模なプログラミングのために便利な機能を提供しています。
クラスをどのような場面でどのように作ったらいいかが分からない人は、「データ構造の応用と活用」だと思えばいいと思います。
たとえば、スタックやキューがあるとします。このスタックやキューに基づく、より高度な「応用プログラム」を作ることができるでしょう。
たとえば、括弧があったとして、前括弧に対する後括弧を検索し、それをツリー構造に変換するプログラムは、スタックを用いることで作ることができます。
このようなプログラムを、クラスとして作り、オブジェクトのメンバ変数としてデータを保持しながら、関連する小さな機能をメソッドにすることで、「データを中心として機能をデータのふるまいにする」というところから開発していくことができます。
このように、基本となるデータ構造がまずあって、それを応用し、活用するために、クラスを使う、というのがまずひとつです。これはどちらかというと「機能のクラス階層」であると言えます。
もうひとつは、クラスごとにプログラムの役割を分担させる、という方法があります。たとえば、MVCフレームワークがあったとして、モデル、ビュー、コントローラに対応する基本のモデルクラス、ビュークラス、コントローラクラスを作り、それらによる抽象化と実装のクラス階層を作ることができます。このクラス階層は、基本となるベースクラスにおいて、それぞれの関係性がしっかりと結びつけられており、ベースクラスを継承してもその関係性は変わらず、簡単に抽象化されたAPIを実装できるようにするのです。これはどちらかというと「実装のクラス階層」であると言えます。
オブジェクト指向とは、そのように、「データ構造の応用」(機能のクラス階層)や「それぞれのクラスに役割を分担させる」(実装のクラス階層)から始まるのです。
オブジェクト指向とは、クラスを用いて関数(メソッド)の間でデータ(状態)を維持し、共有する仕組みのことです。
通常、関数には引数を与えて返り値を返すことしかできません。一度実行が終了すれば、その時点で終わりであり、情報は破棄されます。
ですが、オブジェクト指向のクラスを使うことで、関数の実行が終わっても、オブジェクトが作成されて破棄されるまでの間、関数の持つべき状態をデータとして保持することができます。
このデータは、同じクラスに属するメソッドで参照・共有されます。なので、クラスを実装する時は、状態として維持され、共有されるデータを中心に、そのデータを参照したり変更したりするための関数を、そのクラスのメソッドとして定義していきます。
このようにすることで、関数が一回限りで終わりになるのではなく、オブジェクトが存在する間、関数に状態を与え、維持させることができます。
また、オブジェクト指向には、アクセス修飾子と継承の考え方があります。アクセス修飾子を使うことで、クラスのプライベートなデータメンバにクラスとは無関係の処理からアクセスすることを禁止できます。これにより、クラスの内部を隠蔽できます。これを「カプセル化」と呼びます。
このように、クラスの内部を間違った操作から保護するだけではなく、クラスそのものに機能を追加しやすくするという目的のために、継承と呼ばれる機能があります。継承では、アクセスを禁止するのとは逆に、アクセス可能な範囲で自由にデータメンバやメソッドを付け加えたり、上書きしたりできます。
このようにすることで、クラスは間違った方法では操作できなくなり、正しい方法では機能を追加しやすくできます。アクセス修飾子は、この「何を許して何を禁止するか」ということを実装の際に設定するために使います。
つまり、クラスとは、「どのようなプログラム形態を可能にするか」ということに関するプログラムの実装であると考えられます。これはすなわち、「プログラムの中でどのように、状態としてデータを維持し、メソッドの間でデータを共有し、無関係の処理からデータを隠蔽し、再利用と拡張のためにデータを書き換え可能にするか」ということです。
オブジェクト指向とは、要するに状態を持つプログラムということです。
状態を持つということは、すなわち、状態遷移とよく似た考え方がそのまま適用できるということを意味します。
つまり、状態に左右され、状態を変更するプログラムということです。状態がプログラムの制御を行い、プログラムの中でさまざまなタイミングで状態を変更することで、プログラムそのものの動作を変えられるようなプログラムということです。
状態には、さまざまなものがあります。特に、オブジェクト指向のクラスにおける「データ」とは、たくさんの種別のデータが含まれます。たとえば、
・プログラムが読み書き・編集などの処理を行うためのメモリ上のデータ(テキストファイルや画像データ、データフレーム、バッファやキャッシュなど)
・カレントディレクトリやURLのパスのような、プログラムにおいて重要な役割を持つ情報(関数であれば引数に渡すような情報)
・プログラム自身のアルゴリズムや制御を左右させるような、真偽値やハッシュ(連想配列・辞書)などのフラグ(制御を担当する)
・プログラムのさまざまな挙動を変えるような設定パラメータ(機能のON/OFFなどを担当する)
・GUIコントロール、データベースハンドラ(DbManagerのような専用のクラスを作ることが多い)、ファイルポインタ・カーソルのような、UIや外部リソースへのアクセス手段(外部情報にアクセスできることを保証する)
・アルゴリズムと密接にかかわり合うデータ構造(データの表現とアクセスのためのスタックやキューやツリーやグラフなどのノード情報など)
・プログラムの目的を果たすための重要なデータ(顧客データ、ブログの記事データ、HTMLテンプレート、ゲームキャラクターの戦闘パラメータなど)
そして、このようなさまざまな情報をメンバ変数とし、それらの情報をメソッドから参照・変更しながら、それらの情報によってプログラム自身の挙動が変わるようにプログラムを書くことができます。
実際のクラス設計では、このような基本型のデータだけではなく、オブジェクトの中でオブジェクトをメンバ変数とすることもあります。また、メンバ変数やメソッドの設計だけではなく、継承関係も重要です。プログラム全体のクラスとクラスとの間の継承関係、あるいはインターフェースの実装関係が、プログラムを「何をどこにはめ込むか」とか「どのような形態のインターフェースの中でそのオブジェクトを使用するか」ということを決定するのです。
発展として、仮想関数やポリモーフィズムの考え方を使って、共通の基底クラスを使ってクラスの継承関係を設計したりすることもできます。あるいは、並列処理やマルチスレッドを活用したり、非同期処理の考え方を使って、イベントに対してコールバック関数を実行するようにしたりすることでも、クラスの設計や振る舞いを美しく柔軟にすることができるでしょう。
オブジェクト指向の基本は、インターフェースを想定しながら、内部の仕組みを作っていくことだと思います。
つまり、プログラムの目的を考えて、外部からの操作体系を想定しながら、その操作体系の通りの挙動を振る舞うように、内部の仕組みを作っていけばいいのです。
このような時に、まずはもっとも単純な操作体系を持つ、基本となるクラスを作り、そのクラスを継承して、派生クラスを作るようにします。
クラスとクラスは、オブジェクト同士のやり取りをして、プログラム間の関係性を実現します。これがクラス設計です。
オブジェクト指向の基本とは、データに対するインターフェースを記述することです。その延長線上としてプログラムコードを書いていくことで、統一された共通規則のある美しいAPIを提供し、プログラムのさまざまな場所からクラスやインスタンスとして利用することができるようになるのです。
オブジェクト指向の本質として言えるのは、クラスとは自分でデータ型を定義するということです。
型とは、変数において、その変数の持つ性質や振る舞いを決める、変数の属する「設計図」のようなものです。
よく言われるたい焼きの喩えのように、たい焼きひとつひとつが変数の実例すなわちインスタンスであるとしたら、そのたい焼きを作り出すおおもとの型をクラスと呼びます。
たとえば、クラスにおいて、メンバ変数は、数値型の変数で言うところのデータそのものの値や付属する情報のことを意味します。また、メソッドはその変数を操作する演算子のような操作体系を意味します。
クラスを用いることで、システムの標準には存在しない、自分で定義したデータ型とその変数を使うことができるのです。
同時に、MFCやJavaクラスライブラリのような、言語やプラットフォームのクラスライブラリは、「使える型がこんなにたくさんあります」ということを示していると言えます。
オブジェクト指向プログラミングを行うということの本質は、そのように「型を自分で定義して使う」ということなのです。
また、ひとつのクラスですべてのインスタンスに対応する必要はありません。たとえば、ゲームのキャラクター型を作った時に、ひとつのキャラクタークラスだけですべてのキャラクターに対応することは難しい場合があります。勇者には勇者の、魔法使いには魔法使いの、特別の機能や振る舞いも存在します。このような場合は、キャラクター型を継承して、勇者型や魔法使い型を作ります。
継承が行われた際に、基底クラスの機能や振る舞いで十分である場合、派生クラスのインスタンスを基底クラスとして扱いたい場合があります。たとえば、勇者型の勇者インスタンスと、魔法使い型の魔法使いインスタンスは、通常ゲームの中で戦闘する時は、攻撃したり防御したりといったことは、同じキャラクター型由来の機能であり、そのため勇者であっても魔法使いであっても、それらのインスタンスを基底クラスのキャラクター型として、共通インターフェースから利用できると便利です。実際のところ、勇者や魔法使いのクラスのインスタンスは、基底クラスのキャラクター型として扱うことが可能です。
このような時、同じキャラクタークラスの機能であっても、勇者クラスや魔法使いクラスでそれがオーバーライド(上書き)されていれば、それらの機能を動的バインディングで呼び出すようにすることができるとさらに便利です。これを実現するのが、仮想関数やポリモーフィズムです。仮想関数やポリモーフィズムを使うことで、勇者や魔法使いを同じキャラクターとして扱いながら、勇者や魔法使い固有の機能は場合場合によって動的に呼び出すことができます。
実際のところ、オブジェクトとは、データや状態を保管し、維持する、基地のようなものであると考えるといいでしょう。
たとえば、DbManagerクラスのdbオブジェクトがあったとして、このdbオブジェクトに対してdb.connect()のようにメソッドを実行した時、dbオブジェクトには、このDbManagerクラスのデータベース機能を成り立たせるための、すべての情報が状態として保管し、維持されています。
db.connect()のdbとは、すなわち「dbオブジェクトの中にすべての情報が維持されている」ということを意味し、これに対してconnect()というメソッドを実行することで、このオブジェクトに「命令としてデータベースに接続せよという指示を与える」ということができます。
また、db.setServer('server.domain.com')のように実行すれば、このdbオブジェクトの中の情報を、setServer()というメソッドで適切に変更し、設定することができます。ここでsetServer()の引数である'server.domain.com'は、その時だけ与えられる設定情報であり、オブジェクトとして維持されるデータとは異なります。'server.domain.com'はあくまでその時だけの情報であり、dbオブジェクトの中に保管された情報は常に「状態」として維持される、ステートフルな情報なのです。
そう、オブジェクトとは、データを保管し、維持するための「基地」なのです。データと状態の情報を保管する基地をまず作っておいて、その基地を中心とし、基地に対してさまざまな命令を与えていくという方法が、オブジェクト指向プログラミングであると言えます。
また、DbManagerクラスのように、実際のオブジェクト指向プログラミングでは、「カプセル化されたクラスの中に情報を包括させ、隠蔽する」ということをよく行います。
たとえば、タスクバーであれば、アイコンなどの情報をTaskbarクラスのメンバの中に持ち、task.show()メソッドなどでタスクバーを画面に表示できます。WindowSystemクラスの中にsendMessage()メソッドを用意しておいて、タスクバーがクリックされた時は、wsystem.sendMessage('active', windowId)のようにTaskbar.onClick()の中から実行すれば、ウィンドウシステムwsystemに「表示されている画面の中のウィンドウをアクティブにせよ」「タスクバーにおいてクリックされたのはこのウィンドウID」といった命令処理ができるでしょう。
また、ほかの場合であっても、クラスの中にメンバとして変数やオブジェクトを格納するということはよく行います。「クラスとしてどのようにデータを包括すればいいのか」ということを考えることが、オブジェクト指向プログラミングの最初のとっかかりになるかもしれません。
ポイントは、DbManagerやTaskbarやWindowSystemが「データ型」であり、dbやtaskやwsystemが「変数」であり、connect()やsetServer()やshow()やonClick()やsendMessage()が「演算子」あるいは「命令」であると言えることです。このように、オブジェクト指向とは、「自分のデータ型を定義することであらゆるプログラミングを行う」という方法です。オブジェクト指向においては、プログラムとはデータ型を作ってそれを変数として保持し、あらかじめ決められた演算子や命令(メソッド)を使って処理を行う、ということなのです。
オブジェクト指向を考える上で必要なのは、クラスという「抽象的な枠組み」と、インスタンスという「実体化」を組み合わせて考えることです。
クラスは、すべての場合に対応できる抽象的な枠組みを、ひとつ書けば十分です。すべての場合に対応できなくても、継承によって拡張することもできます。
インスタンスは、それぞれの個別のデータの値を持ちます。また、それぞれの個別の「実例」に合わせて操作します。クラスがひとつであっても、インスタンスはたくさん作ることができます。インスタンスを作成する実例はさまざまな状況や形態が個別に考えられます。また、メソッドは必要なものをひとつ作れば何度でも使えます。
クラスの中でインスタンスを使うこともあります。クラスのメンバの中にインスタンスを包有したり、インスタンスを引数に取るメソッドを実装することで、オブジェクトは互いに関連し、メッセージ通信しながらやり取りすることができます。単純なAPIしか持たないクラスを作っておいて、それを高度な操作形態や複雑なAPI体系を持つ、別のクラスから操作することも考えられます。
クラスは、なんらかのプログラムを成り立たせる「設計図」であり、インスタンスは、基本となるデータとやり取りの場所を保持するための「拠点」であるとも考えられます。クラスにおいてすべての機能を実装し、インスタンス化されたオブジェクトをどこかで「保持」することで、その保持されたインスタンスを拠点として、プログラムのさまざまな場所にあるデータとロジックにアクセスすることができるのです。
僕は、オブジェクト指向のクラスを使うことで、ポインタに対するデータ操作をスマートに記述できると思います。
通常、データを操作するプログラムでは、データを保持する配列や構造体がまずあって、この配列や構造体にアクセスするためにポインタを使います。それぞれの関数では同じ型のポインタに対してデータ操作を行い、この関数にポインタを渡します。
ですが、オブジェクト指向のクラスを使うことで、このような一連の操作をスマートに記述できます。
クラスには、メンバ変数とメソッドがあります。このうち、メンバ変数が、データの配列や構造体を保持するポインタに相当します。また、メソッドがデータを操作する関数に相当します。
クラスを用いることで、データをポインタで保持し、ポインタを使って関数から操作するという、複雑で分かりづらい方法を、メンバ変数とメソッドを使ってエレガントに記述できるのです。
Javaには、ポインタがありません。これは、構造化プログラミング(順次実行・選択・反復)によってgoto文が古くなり、使うのが非推奨になったように、オブジェクト指向によってポインタが古くなり、ポインタを使うことが非推奨になったのだ、ということであると言えると思います。
ポインタは、C言語などでは今でも使われます。データをポインタからアクセスするような関数は、ひとつのデータに対して複数の関数からさまざまな処理を行うというプログラム設計では必須の考え方となります。オブジェクト指向言語では、このような手法をポインタを用いずにクラスを用いて行うことで、スマートかつエレガントにデータ処理を記述できるのです。
また、オブジェクト指向のクラスにおいては、メンバ変数だけではなく、メソッドの引数に与える「引数オブジェクト」の場合も、ポインタと同様に参照型変数として扱います。メンバであろうとメンバでなかろうと、メソッドから扱う参照型変数はすべてポインタと同じです。メンバやメソッドの引数という違いに囚われず、同じ参照型の変数であるという考え方が、オブジェクトとオブジェクトの間での「メッセージング」という考え方を理解する上で重要となります。
僕は、ここまでごたくを述べておいて、実際はクラス設計のことがまったく分かっていません。
ですが、そのような僕が述べるとしたら、クラス設計とは「ふるまい」を考えることです。
オブジェクトがどのような「ふるまい」を持つのか、ということを考えて、その「ふるまい」が成立するように、クラスを設計し、プログラムを実装すること、これこそがオブジェクト指向における「クラス設計」だと言えると思います。
僕のように、クラス設計がまったく分かっていないオブジェクト指向の入門者は、「ふるまい」を考えるようにしましょう。自分の考えた設計の通りに「ふるまい」を実装できるようになったら、それをプログラミング中級者と言うことができるでしょう。
2023.02.16
オブジェクト指向について言えるのは、「データに対する処理をまとめてやると賢い」ということです。
通常、プログラムを書く上で、データに対する処理しか書きません。
なんらかのデータがあって、そのデータを参照したり代入したりしながら、データに対する処理のひとつひとつをメインルーチンに書くか、機能ごとに関数にして処理を別々の場所に書くか、ということしか、プログラミングでは基本的に記述しません。
ですが、そうであるならば、データという共通の「操作対象」を中心にして、そのデータに対する処理をひとつにまとめてしまえば、エレガントかつスマートにプログラムを部分ごとにモジュール化することができます。
そして、そこで機能をカプセル化し、外部から何も考えなくても使えるようにしてしまえば便利です。どんな機能であっても、データ(オブジェクト)に対する処理(メソッド)にして、いつでもその機能をオブジェクトのインスタンスに対して使えるようにするのです。
そのようにすれば、どこからでもプログラムを使えますし、そのプログラムを使う上で必要となる「実装のための詳細」を知らなくても、何も気にすることなくいつでも適切にプログラムの機能群を呼び出せるのです。
僕は、オブジェクト指向とはそういうことだと思います。データを中心に、データに対する処理と機能をまとめ、外部から使いやすいようにカプセル化して使えるようにするのです。これこそ、オブジェクト指向です。
そう、つまり、オブジェクト指向で設計するということが、モジュールを作って再利用するという上で、もっとも正しいのです。プログラムを機能ごとにモジュール化するのであれば、オブジェクト指向のクラスライブラリにするべきなのです。そうすることで、継承やカプセル化が活用できるというメリットもあります。すべてはモジュールの「再利用性」と「カプセル化」を目指した結果なのです。
2023.03.31
オブジェクト指向の利点として、「複雑性の軽減になる」ことが言える。
たとえば、放送大学「コンピュータとソフトウェア ('18)」では、単位と長さを保持するオブジェクトを作って、メートルやセンチメートルやキロメートルという単位で長さを設定・参照するようにし、複雑な単位の変換はオブジェクトの中に隠蔽されるようなプログラムを考える。
if~else文のような条件分岐で、さまざまな単位変換を実現するよりも、オブジェクトの内部でこうした雑多な変換処理を行うことで、処理がオブジェクトの内部に内包され、実際に使う時は単純にメソッドを呼び出すだけでシンプルにプログラムを操作できる。
このように、オブジェクトの内部に雑多な処理を内包することで、プログラムが非常に簡単かつ単純になる。オブジェクト指向を上手く使えば、プログラムの複雑さを軽減できるのである。
オブジェクト指向により、生産性と利便性が大きく高まる。ひとつのクラスにすべての処理が記述され、さまざまな場所に処理が点在せずクラス内に隠蔽されるため、条件分岐でさまざまな処理をさまざまな場所に書く必要がなくなり、プログラムがシンプルになる。また、クラスの内部の詳細が分からなくても、「面倒なことはクラスライブラリが勝手にやってくれる」ようになり、使う側は単純にインスタンスを作ってメソッドを実行するだけで、複雑な処理について何も気にしなくても目的の処理の実行が達成できる。
オブジェクト指向は、複雑で高度になった現在のプログラムを記述するプログラマにとって「救世主」のような存在であり、実際の処理内容がどんなに複雑なプログラムであっても、オブジェクト指向がそれを単純にしてくれるのである。
2023.04.15
オブジェクト指向とは、簡単に言えばデータ操作のカプセル化です。
たとえば、オブジェクト指向がない時、プログラムの記述は以下のようになります(C言語)。
#include <stdio.h> int main(int argc, char *argv[]) { /* hogeという変数を中心に、上から下に処理を記述する */ int hoge = 1; hoge = hoge + 2; hoge = hoge + 3; printf("%d\n", hoge); return 0; }
これに対して、オブジェクト指向では以下のようになります(C++)。
#include <iostream> using namespace std; // プログラムの基本となるクラスHoge class Hoge { private: // 共通に操作されるデータvをprivateメンバとして定義する int v; public: // 変数を初期化するためのコード部分 Hoge(int x) { v = x; } // 変数を加算するためのコード部分 void addHoge(int x) { v = v + x; } // 変数を画面に出力するためのコード部分 void printHoge() { cout << v << endl; } } int main(int argc, char *argv[]) { // Hogeのインスタンスhogeに対して、それぞれのメソッドを呼び出すことでプログラムを記述する Hoge hoge(1); hoge.addHoge(2); hoge.addHoge(3); hoge.printHoge(); return 0; }
ただし、本当のことを言えば、同様の記述はC言語でも関数とポインタを使うことで可能です。つまり、
#include <stdio.h> /* ポインタを使った変数への加算 */ void addHoge(int* v, int x) { *v = *v + x; } /* 画面への表示 */ void printHoge(int x) { printf("%d\n", x); } int main(int argc, char *argv[]) { /* hogeという変数を中心に、ポインタで処理を記述する */ int hoge = 1; addHoge(&hoge, 2); addHoge(&hoge, 3); printHoge(hoge); return 0; }
のようになります。
このように、オブジェクト指向を基にすることで、データを中心として処理をそれぞれの部分にして、それを実行時に適切に呼び出すことで処理が行えます。
ほとんどのプログラムは、「データに対する処理」によって記述されます。
これを、main()関数の中ですべて記述するのが、従来の手続き型のプログラミングスタイルであるとするなら、データを中心とした「クラス」というまとまりを作って、個別の処理を「メソッド」にし、クラスから「オブジェクト」を利用する際に作って、オブジェクトに対するメソッドを呼び出すようにします。
そう、このようにすれば、すべてを流れるように上から下に向かって書くのではなく、個別の処理を部分化して、それを「共通に操作されるデータ」を中心にまとめ、カプセル化をすることができるのです。
オブジェクト指向は、大規模なプログラミングを行いやすくします。たとえば、データベース操作をするのであれば、データベースを作ってそれを操作するのを上から下へと書くのではなく、データベースのハンドラをクラスのメンバとして持ち、その操作をメソッドにし、利用する際にこのクラスをオブジェクトとして作成し、そのオブジェクトに対してメソッドを実行するように書くことができるのです。
このようにすることが、プログラム全体の見通しと保守性を高めます。オブジェクト指向は最初は分かりづらいかもしれませんが、そのように、「プログラムを上から下に書くのではなく、データを中心としたメソッドのまとまりとして書く」ということができる、ということなのです。
2023.06.26
僕は、オブジェクト指向の優れた点は、文字列や数値のような単一データだけを返すのではなく、オブジェクトを返せるところだと思います。
関数を使う場合、処理をだらだらと実行しておいて、そのほとんどのデータは消え去ってしまい、返されるのは文字列や数値のような単一データだけです。
すなわち、たくさんの処理を行ったにもかかわらず、そのほとんどは失われて分からなくなってしまうのです。
そのような時、オブジェクトを返すようにすることで、オブジェクトの中に必要なさまざまなデータを詰め込んで返すことができます。
そう、オブジェクト指向においては、「さまざまなデータ処理の結果、オブジェクトを返す」ということが重要になるのです。
2023.08.07