Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/00322.jsonl.gz/23

くいなちゃん2020年05月27日
「６さいからのプログラミング」第12話は、プログラムが大規模になったときや多人数開発のときに役に立つ「オブジェクト指向」について解説します。
プログラムが数万行を超えてくると、全体像を把握するのが困難になるため、後で内容を変更したくなったときや他の人と一緒に開発するときに大変になります。 そのような場合、多くのプログラミング言語で採用されている「オブジェクト指向」を使うと便利です。
「オブジェクト指向」とは、プログラムを「オブジェクト」というまとまりで扱う手法で、全体像を把握したり、後で内容を変更したり、プログラムを別の箇所で再利用したり、他の人と開発するのを容易にします(図1-1)。
これまでのプログラムは全体がひとまとめになっていたため、どの部分がどの部分に影響しているかが解りにくく、プログラムを変更するときにはすべてを把握する必要がありましたが、オブジェクト単位でプログラムを切り分けると、変更したいオブジェクトとオブジェクト間の関係を把握するだけでプログラムが変更できるようになります。
さて、切り分けた各オブジェクトが互いにやりとりできるようにするために、オブジェクトは「3つの仕組み」と「3つの要素」という機能を持つことが決められています(図2-1)。
それぞれがどのようなものかを順番に説明します。 単にプログラムを切り分けるだけでなく、これらの機能を持たせることで、各オブジェクトは扱いやすい部品になります。
2.13つの仕組み
まずは「3つの仕組み」から見ていきましょう(図2-2)。
オブジェクトには、「データを持つ」「他のオブジェクトにメッセージを送る」「送られたメッセージに応じて処理を行う」という3つの仕組みがあります。
1つ目として、オブジェクトはデータを保持することができ、これを「データメンバ」といいます。 データメンバは言語によって呼び方が異なり、C++では「メンバ変数」、C#では「プロパティ」と呼ばれます。
2つ目として、オブジェクトは他のオブジェクトを操作するために、「メッセージ」を送ることができます。 メッセージとは、いくつかの情報をまとめたものです。
そして3つ目として、送られたメッセージに応じてオブジェクトは処理を行うことができ、これを「メソッド」といいます。 C#でも「メソッド」ですが、C++では「メンバ関数」と呼ばれます。 「データメンバ」と「メソッド」は、合わせて「メンバ」といいます。
この3つの仕組みにより、データを持ったオブジェクトは互いにメッセージを送りあって処理を行うことができます。
2.23つの要素
次に3つの要素、「継承」「カプセル化」「ポリモーフィズム」を見ていきましょう。
1つ目の「継承」とは、オブジェクトのメンバを引き継いで別のオブジェクトを作成できる機能です。 図2-3のように、引き継ぐときに新しいメンバを追加したり、引き継いだメンバの内容を上書きすることもできます。
2つ目の「カプセル化」とは、オブジェクトのメンバの一部を隠すことができる機能です。 図2-4のように、外部に公開する部分を必要最小限にすることで、オブジェクトに対する想定外の操作を防ぐことができます。
3つ目の「ポリモーフィズム」とは、継承によって複製されたオブジェクトたちを、全て継承元と同等とみなして扱える機能です。 図2-5のように、多くの種類のオブジェクトが生まれたとしても、その違いを意識することなくまとめて扱うことができます。
まとめますと「オブジェクト指向」とは、プログラムを「オブジェクト」という単位で扱い、そのオブジェクトに「3つの仕組み」と「3つの要素」を持たせることで、プログラムの開発を容易にする手法です。
それでは、プログラム上でオブジェクト指向がどう表現されるかを、具体的に見ていきましょう。 多くの言語はC++に影響を受けていますのでC++を例に解説します。 C++は基本的にはC言語にいくつかの文法と機能を足しただけの言語ですので、今までに説明したC言語の知識が流用できます。
C++のオブジェクトは、C言語における構造体を発展させて実現しました。 この発展した構造体を「クラス」と呼びます。 クラスとC言語の構造体との違いは、クラスには中に変数だけでなく、関数も持たせられることです(図3-1)。
このプログラムでは「構造体S」と「クラスC」を定義し、メイン関数の20～21行目で、Sの変数sと、Cの変数cを作成しています。
このとき、構造体sのメンバ変数aが「s.a」と表せることは以前説明しましたが、クラスcのメンバ変数aも同様に「c.a」と書いて扱えます。 またクラスcのメンバ関数fも同様に「c.f()」と書いて呼び出せます。
このcのような、クラス型の変数のことを「インスタンス」といいます。 つまりC++におけるオブジェクトは、ひな形であるクラスと、それを実体化したインスタンスによって作られます。
メンバ関数の内部では、そのクラスが持つメンバ変数を読み書きできます。 例えばこのプログラムでは「c.f()」を呼び出すと、14行目の「this->a=5;」でc.aに5を代入することができます。 メンバ関数内で自身のインスタンスを表すときは「this」と記述し、ポインタのように扱います。
ちなみに「this->」は省略可能となっており、「this->a=5;」は単に「a=5;」と書かれることが多いです。
3.13つの仕組み
ここでオブジェクトの「3つの仕組み」とC++のクラスとを比べると、表3-1のようになっていることが解ります。
|3つの仕組み||C++のクラス|
|オブジェクトは、データを持つ。||メンバ変数|
|オブジェクトは、他のオブジェクトにメッセージを送る。||他のオブジェクトのメンバ関数の呼び出し|
|オブジェクトは、送られてきたメッセージに応じて処理を行う。||メンバ関数内の処理|
C++における「メッセージ」のやりとりは、このようにメンバ関数の呼び出しで代用されています。
ここからはC++における「3つの仕組み」を見ていきましょう。
3.2継承
1つ目は「継承」です。 C++での継承はクラス名の右側に「:継承元」と書くことで行えます(図3-2)。
この例では、7行目でクラス「Parent」を継承して新たなクラス「Child」を作成しています。 継承元のメンバが継承先に引き継がれますので、Childは変数zだけでなく、xやyも持ちます。
3.3カプセル化
2つ目は「カプセル化」です。 C++でカプセル化を行うには、「private:」「protected:」「public:」という「アクセス修飾子」を用います。
「private:」と書くと、メンバは外部や継承したクラスから参照できないようになります。 「protected:」は継承したクラスからは参照できるようになり、「public:」は外部からも参照できるようになります(図3-3)。
この例では、メンバSetにはpublic:が設定されているため、16行目のn.Setのように参照することができます。 しかしメンバvalueにはprivate:が設定されているため、n.valueのように外部から値を読み書きすることはできません。 ただし9行目のようにクラス内部からはthis->valueとして参照可能です。
private:などのアクセス修飾子は、一度設定すると再設定されるまで設定が持続します(図3-4)。
この例の場合、メンバb、c、dは全てpublic:になります。 また、3行目のメンバaのようにアクセス修飾子が設定されていないメンバはprivate:となります。
継承関係にもアクセス修飾子を設定することができます(図3-5)。
継承のときに「:protected 継承元」と書くと、継承元のメンバでpublic:だったものは全てprotected:になって引き継がれます。 また「:private 継承元」と書くと、継承元のメンバは全てprivate:になって引き継がれます。 「:public 継承元」は継承元のアクセス修飾子の設定がそのまま引き継がれます。
このように継承元メンバの公開範囲は制限することができます。 ちなみに、省略して単に「:継承元」と書くと「:private 継承元」と解釈されます。
3.4ポリモーフィズム
3つ目はポリモーフィズムです。 C++では継承先クラスを継承元クラスと同じように扱うことができます。 またメンバ関数の先頭に「virtual」と書くと、継承先クラスで上書きすることができます(図3-6)。
7～17行目では、クラスPersonを継承してクラスKuinaとクラスAliceを作成し、それぞれでメンバ関数GetAgeを上書きしています。 メンバ関数を上書きすることを「オーバーライド」といいます。
main関数の中では、それぞれのクラスのインスタンスを作成し、24行目でPerson*型の配列「people」にそれぞれのアドレスを代入しています。 KuinaやAliceは、Personとは型が異なりますが、このように継承元クラスであるPersonと同じ型として扱えます。
28行目ではPersonクラスのメンバ関数GetAgeを呼んでいますが、インスタンスはそれぞれPerson、Kuina、Aliceのものですので、それぞれで上書きしたGetAgeが呼ばれます。 よって画面には「0」「6」「7」が表示されます。
ここまでオブジェクト指向の考え方とC++での実装方法について解説しました。 さてここからは、プログラムのどこの部分をオブジェクトにすればいいのかについて、オブジェクトの設計方法を説明します。
基本的には、「開発が効率的になるように」オブジェクトを作成すると大丈夫です。 言い換えると、それ以外の観点でオブジェクトを作成すると本末転倒になりますので注意が必要です。
例えば、アクションゲームのプログラムで、主人公のクラス「Hero」と敵のクラス「Enemy」を作成し、どちらもキャラクターでなので、さらにクラス「Character」を作って継承関係にさせるべきかどうかを検討したとしましょう。 このとき、衝突判定や移動などの処理が再利用できそうならば、クラスCharacterを作って継承関係にすることは有意義です。 しかし、細かな処理が異なるために再利用できる部分が少なければ、無理に継承関係にしないほうが有益になることもあります。
特によくある失敗として、プログラムのあらゆる概念をオブジェクトにして大きなピラミッド構造を作りたくなることがあります。 プログラムの最終目的は期待通り動くことであり、秩序正しく美しい階層を構築することではないはずです。 何をオブジェクトにしてどのような継承関係にするかは、開発が効率的になるかどうかに基づいて判断すると良いと思います。
さて、C++ではクラスを使ってオブジェクト指向を実現しましたが、他の言語ではこれとは異なる方法でオブジェクト指向を実現していることがあります。
例えばJavaScriptには、クラスというものが存在しませんでした。 JavaScriptでは図5-1のように、空の状態のインスタンスにメンバを追加していく流れでオブジェクトを構築していきます。
また、Java(JavaScriptではない)におけるオブジェクトは、C++の仕組みに近いですが、よりオブジェクト指向を前提とした言語設計になっています。 クラスのメンバでない変数や関数を作成することができず、あらゆるデータや処理は基本的にオブジェクトで管理され、メイン関数も何らかのクラスに含める必要があります(図5-2)。
今回は、プログラムが大規模になったときに役立つ仕組み「オブジェクト指向」について説明しました。 次回はより速く計算させるために、同時に複数のプログラムを実行する方法について説明します。