Home English

Kuina-chan

くいなちゃんAug 22, 2017


6さいからのプログラミング」第12話は、プログラムが大規模になったときや多人数開発のときに役に立つ「オブジェクト指向」について解説します。

オブジェクト指向

プログラムが数万行を超えてくると、全体像を把握するのが困難になるため、後で内容を変更したくなったときや他の人と一緒に開発するときに大変になります。 そのような場合、多くのプログラミング言語で採用されている「オブジェクト指向」を使うと便利です。
「オブジェクト指向しこう」とは、プログラムを「オブジェクト」というまとまりで扱う手法で、全体像を把握したり、後で内容を変更したり、プログラムを別の箇所で再利用したり、他の人と開発するのを容易にします(オブジェクト指向)。
オブジェクト指向
オブジェクト指向
これまでのプログラムは全体がひとまとめになっていたため、どの部分がどの部分に影響しているかが解りにくく、プログラムを変更するときにはすべてを把握する必要がありましたが、オブジェクト単位でプログラムを切り分けると、変更したいオブジェクトとオブジェクト間の関係を把握するだけでプログラムが変更できるようになります。

オブジェクトの機能

さて、切り分けた各オブジェクトが互いにやりとりできるようにするために、オブジェクトは「3つの仕組み」と「3つの要素」という機能を持つことが決められています(オブジェクトの3つの仕組みと3つの要素)。
3つの仕組み
  1. オブジェクトは、データを持つ。
  2. オブジェクトは、他のオブジェクトにメッセージを送る。
  3. オブジェクトは、送られたメッセージに応じて処理を行う。

3つの要素
  1. 継承
  2. カプセル化
  3. ポリモーフィズム
オブジェクトの3つの仕組みと3つの要素
それぞれがどのようなものかを順番に説明します。 単にプログラムを切り分けるだけでなく、これらの機能を持たせることで、各オブジェクトは扱いやすい部品になります。

3つの仕組み

まずは「3つの仕組み」から見ていきましょう(オブジェクトの3つの仕組み)。
オブジェクトの3つの仕組み
オブジェクトの3つの仕組み
これは先ほどの「データを持つ」「他のオブジェクトにメッセージを送る」「送られたメッセージに応じて処理を行う」という3つの仕組みを図示したものです。
まずオブジェクトはデータを保持することができ、これを「データメンバ」といいます。 データメンバは言語によって呼び方が異なり、C++では「メンバ変数へんすう」、C#では「プロパティ」と呼ばれます。
オブジェクトは他のオブジェクトを操作するために、「メッセージ」を送ることができます。 メッセージとは、いくつかの情報をまとめたものです。
そして送られたメッセージに応じてオブジェクトは処理を行うことができ、これを「メソッド」といいます。 C#でも「メソッド」ですが、C++では「メンバ関数かんすう」と呼ばれます。 「データメンバ」と「メソッド」は、合わせて「メンバ」といいます。

3つの要素

次に3つの要素、「継承」「カプセル化」「ポリモーフィズム」を見ていきましょう。
1つ目の「継承けいしょう」とは、オブジェクトのメンバを引き継いで別のオブジェクトを作成できる機能です。 継承のように、引き継ぐときに新しいメンバを追加したり、引き継いだメンバを上書きすることもできます。
継承
継承
2つ目の「カプセル」とは、オブジェクトのメンバの一部を隠すことができる機能です。 カプセル化のように、外部に公開する部分を必要最小限にすることで、オブジェクトに対する想定外の操作を防ぐことができます。
カプセル化
カプセル化
3つ目の「ポリモーフィズム」とは、継承によって複製されたオブジェクトたちを、全て継承元と同じとみなして扱える機能です。 ポリモーフィズムのように、多くの種類のオブジェクトが生まれたとしても、その違いを意識することなくまとめて扱うことができます。
ポリモーフィズム
ポリモーフィズム
まとめますと「オブジェクト指向」とは、プログラムを「オブジェクト」という単位で扱い、そのオブジェクトに「3つの仕組み」と「3つの要素」を持たせることで、プログラムの開発を容易にする手法です。

クラスとインスタンス

それでは、プログラム上でオブジェクト指向がどう表現されるかを、具体的に見ていきましょう。 多くの言語はC++に影響を受けていますのでC++を例に解説します。 C++は基本的にはC言語にいくつかの文法と機能を足しただけの言語ですので、今までに説明したC言語の知識が流用できます。
C++のオブジェクトは、C言語における構造体を発展させて実現しました。 この発展した構造体を「クラス」と呼びます。 クラスとC言語の構造体との違いは、クラスには中に変数だけでなく、関数も持たせられることです(struct_and_class.cpp)。
struct S
{
  int a;
  char b;
};
  
class C
{
  int a;
  char b;
  
  void f(void)
  {
    this->a = 5;
  }
};
  
int main(void)
{
  S s;
  C c;
  return 0;
}
struct_and_class.cpp
このプログラムでは「構造体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つの仕組み

ここでオブジェクトの「3つの仕組み」とC++のクラスとを比べると、3つの仕組みとC++のクラスのようになっていることが解ります。
3つの仕組みとC++のクラス
3つの仕組み C++のクラス
オブジェクトは、データを持つ。 メンバ変数
オブジェクトは、他のオブジェクトにメッセージを送る。 他のオブジェクトのメンバ関数の呼び出し
オブジェクトは、送られてきたメッセージに応じて処理を行う。 メンバ関数内の処理
C++における「メッセージ」のやりとりは、このようにメンバ関数の呼び出しで代用されています。
ここからはC++における「3つの仕組み」を見ていきましょう。

継承

1つ目は「継承」です。 C++での継承はクラス名の右側に「:継承元」と書くことで行えます(inheritance.cpp)。
class Parent
{
  double x;
  double y;
};
  
class Child : Parent
{
  double z;
};
inheritance.cpp
この例では、7行目でクラス「Parent」を継承して新たなクラス「Child」を作成しています。 継承元のメンバが継承先に引き継がれますので、Childは変数zだけでなく、xやyも持ちます。

カプセル化

2つ目は「カプセル化」です。 C++でカプセル化を行うには、「private:」「protected:」「public:」という「アクセス修飾子しゅうしょくし」を用います。
「private:」と書くと、メンバは外部や継承先クラスから参照できないようになります。 「protected:」は継承先クラスからは参照できるようになり、「public:」は外部からも参照できるようになります(encapsulation.cpp)。
class Number
{
private:
  int value;
  
public:
  void Set(int v)
  {
    this->value = v;
  }
};
  
int main(void)
{
  Number n;
  n.Set(5);
  return 0;
}
encapsulation.cpp
この例では、メンバSetにはpublic:が設定されているため、16行目のn.Setのように参照することができます。 しかしメンバvalueにはprivate:が設定されているため、n.valueのように外部から値を読み書きすることはできません。 ただし9行目のようにクラス内部からはthis->valueとして参照可能です。
private:などのアクセス修飾子は、一度設定すると再設定されるまで設定が持続します(access_modifiers.cpp)。
class Test
{
  int a;
public:
  int b;
  int c;
  int d;
};
access_modifiers.cpp
この例の場合、メンバb、c、dは全てpublic:になります。 また、3行目のメンバaのようにアクセス修飾子が設定されていないメンバはprivate:となります。
継承関係にもアクセス修飾子を設定することができます(access_modifiers2.cpp)。
class Parent
{
public:
  int a;
};
  
class Child : protected Parent
{
public:
  int b;
};
access_modifiers2.cpp
継承のときに「:protected 継承元」と書くと、継承元のメンバでpublic:だったものは全てprotected:になって引き継がれます。 また「:private 継承元」と書くと、継承元のメンバは全てprivate:になって引き継がれます。 「:public 継承元」は継承元のアクセス修飾子の設定がそのまま引き継がれます。
このように継承元メンバの公開範囲は制限することができます。 ちなみに、省略して単に「:継承元」と書くと「:private 継承元」と解釈されます。

ポリモーフィズム

3つ目はポリモーフィズムです。 C++では継承先クラスを継承元クラスと同じように扱うことができます。 またメンバ関数の先頭に「virtual」と書くと、継承先クラスで上書きすることができます(polymorphism.cpp)。
class Person
{
public:
  virtual int GetAge() { return 0; }
};
  
class Kuina : public Person
{
public:
  int GetAge() { return 6; }
};
  
class Alice : public Person
{
public:
  int GetAge() { return 7; }
};
  
int main(void)
{
  Person person;
  Kuina kuina;
  Alice alice;
  Person* people[3] = { &person, &kuina, &alice };
  int i;
  for (i = 0; i < 3; i++)
  {
    printf("%d\n", people[i]->GetAge());
  }
  return 0;
}
polymorphism.cpp
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ではJavaScriptのオブジェクトのように、空の状態のインスタンスにメンバを追加していく流れでオブジェクトを構築していきます。
JavaScriptのオブジェクト
JavaScriptのオブジェクト
また、Java(JavaScriptではない)におけるオブジェクトは、C++の仕組みに近いですが、よりオブジェクト指向を前提とした言語設計になっています。 クラスのメンバでない変数や関数を作成することができず、あらゆるデータや処理は基本的にオブジェクトで管理され、メイン関数も何らかのクラスに含める必要があります(HelloWorld.java)。
public class HelloWorld
{
  public static void main(String[] args)
  {
    System.out.println("Hello, world!");
  }
}
HelloWorld.java
今回は、プログラムが大規模になったときに役立つ仕組み「オブジェクト指向」について説明しました。 次回はより速く計算させるために、同時に複数のプログラムを実行する方法について説明します。
1503412905ja