くまくまの業務日誌

Markdown記法で徒然に書いてみましょう。

オブジェクト指向プログラミングの再考

最近ふと、「オブジェクト指向プログラミング」を正しく人に説明できるだろうか、と疑問が湧いてきたのでここらで脳内整理してみようと思った訳であります。

検索結果からみる、「オブジェクト指向

オブジェクト指向」を検索してみる。

by Bing 「オブジェクト指向とは
by Googleオブジェクト指向とは

以下のキーワードが列挙されています。

オブジェクト指向の「オブジェクト」とは

世の中「オブジェ」の方が浸透性が高いこの「オブジェクト」という言葉は、本当に「物」に過ぎないと思います。そして、この「オブジェクト」と対になる考え方が「型」になると思います。

型を作れば、その型からオブジェクトをいくらでも作る事ができる。でも、そんなに型を作ってまでいっぱいオブジェクトを作る必要もないと思われるかもしれません。

例えばログファイルを解析するプログラムを作ろうとしたとき、解析ロジックさえきっちり作れば、ファイル操作部は関数だのグローバル変数だので構成して早々に作り上げてもよいとは思います。しかし、初期の段階で将来を見越した設計をしておくことは、この先思いもよらぬヒット商品を生み出すことになるかもしれません。

そのためにも、オブジェクトを自分だけではなく、賛同する誰かが使ってくれるかもしれないという思いで設計しておくことは、大切な事ではないかと思います。

「型」に求められるもの

プログラムがおかしなことになるのは、ロジックがおかしいために起きることに加え、以下のファクタも加わってきます。

  • 意図しない値を設定された。
  • 意図しない機能の使われ方をした。

1つ目の「意図しない値を設定された」に関しては、値の設定値を検証すればいい訳ですが、そもそも値が丸見えになっているのでは、どんだけ頑張っても裏技的に使われたりして「意図しない機能の使われ方」に繋がってしまいます。だから、設計者の意図するアクセス権限(直接見れる、見れない)を設定できなくてはなりません。これが、オブジェクト指向の1つ目のキーワード「カプセル化」になります。

カプセル化

直接操作して欲しくない変数や、関数は「カプセル化」で操作できないようにします。

class Sample
{
public:
    // 外部から操作可能です。
protected:
    // このクラスと継承先だけが操作可能です。
private:
    // このクラス内でのみ操作可能です。
};

外部からアクセス可能・不可で分けるなら、以下の通りです。

publicprotectedprivate
アクセス可能
アクセス不可

さて、protectedとprivateはどう使い分けましょうか。経験則からですと、privateに設定した変数は、継承が発生したり、深くなってきますと、どうしても不便となって、protectedに変えることが多かったです。わざわざprivate変数のためにprotectedな関数を作ってアクセスするのもなんですからね。

継承(派生)

機能を受け継ぎ、さらに発展させるという解釈で「継承」を考えるとちょっと違うかもしれません。ちなみにですが、私は最初にC++を勉強していた時、この「継承」というものは、Version1、Version2というような進化をさせるためにあるものと勘違いしていました。

実際は、機能のグルーピングという観点で見た方がよいと思います。以下はそのグルーピングの例となります。

  • ファイルの読み書きを行うクラスを作りたい。
  • ファイルが読み書きできるなら、CSVや固定長ファイルも扱いたい。
  • ファイルを操作するようにメモリを操作できると早いかも。
  • ファイルを操作するように外部との通信ができると便利かも。

実は、WindowsはCreateFileでファイルだけでなく外部との通信を可能にする「名前付きパイプ」を操作する事ができます。また、「共有メモリ」という機能を使えば、Read/Writeでメモリを操作することができます。流石にCSV、固定長ファイルに関しては自力で「機能」として考えなくてはなりませんが。

すべての機能をRead()/Write()で実現したいので、親クラスにはこの関数を装備します。Windowsは基本的にHANDLEという変数を使ってファイル、名前付きパイプ、共有メモリの操作が行えます。HANDLE変数も親クラスに実装しましょう。

CSVや固定長ファイルの機能に関しては、親のファイル操作機能を「継承」して実装すると、同じコードを書かなくて済みそうです。共有メモリに関しては追加の作業が必要になりますので、これも継承先のクラスで実装しましょう。

上記はあくまで一例ではあります。継承という考え方は確かに「機能を受け継いで」いる訳ですが、継承設計を行うために考えることは、機能全体の配置方法についてとなります。つまり、登場するファクタが多ければ多いほど、設計に柔軟性が必要となりますが、結果的にはバランスの良い構造が完成します。

どうせなら、小舟を作るより大型船を作る方が船の構造やバランスのとり方が理解しやすい訳であります。

多態性、多様性(ポリモーフィズム

昨今のカタカナ氾濫文化になっても、なじまない言葉ではあります。久しぶりに「オブジェクト指向」を振り返ってみようと思ったのもふと、この言葉を思い出したからであります。

取り敢えず、サンプルです。

class Car
{
public:
    virtual void Drive();
};

class TOYOTA_2000_GT : public Car
{
public:
    virtual void Drive();
};

class NISSANN_GTR : public Car
{
public:
    virtual void Drive();
};

Car* myCar = new TOYOTA_2000_GT();
Car* yourCar = new NISSAN_GTR();

myCar->Drive();
yourCar->Drive();

多様性の重要なポイントは、

  • 型から生成したオブジェクトは、なにもその型で受ける必要はない。
  • 親の型で子のオブジェクトを受けて、親の型から関数を呼び出しても、子のオブジェクトの関数が呼び出される。

というところにあります。上記のサンプルで言えば、TOYOTA_2000_GTクラスから生成したオブジェクトは、その親であるCarクラスのmyCarで受けている部分になります。またmyCar->Drive()は、Carクラスの変数からアクセスしていますが、実際はTOYOTA_2000_GTクラスのDrive()が呼び出されます。

つまり、Carクラスから派生したオブジェクトは、Carクラスの変数から呼び出すことで、「TOYOTA2000GTを運転する」のではなく、「(なんか知らんけど)車を運転する」という構文で、TOYOTAのかつてのスーパーカーを運転できるのです。GT-Rに関しても同じですね。「車を運転する」のですが、世界の最高峰の一つ、GT-Rを操作できるわけです。

さて、サンプルは「車」でしたので、どの車も運転して目的地に着くまでの操作は(MT,ATの違いはありますが)それほど変わりません。つまり、Carクラスから派生した2つのクラスはそれほど違いはないでしょう。

では、荷台の操作が可能なダンプトラックがCarクラスから継承された場合、後ろの荷台の操作はCarクラスからどうやって操作するのでしょうか。

class Dump : public Car
{
public:
    void Drive();
    void Dump(bool direction);
};

それは無理です。Carクラスは継承先のDumpクラスのDump()関数を知りません。よって呼び出すことができないのです。困りましたね。

  • TOYOTA 2000 GT で買い物に出かける。
  • NISSAN GT-R でドライブに出かける。
  • ダンプで土砂を捨てる。

全く違う事を行うのに、それをCarクラスでやるためにはどうすればよいでしょう。そのためには、親クラスとなるCarが知り得る事、つまり「なにかする」を用意することで実現できます。

もう少し先程のクラスを見直してみましょう。

class Car
{
public:
    virtual void Drive();
    virtual void DoAction();
};

class TOYOTA_2000_GT : public Car
{
public:
    virtual void Drive();
    virtual void DoAction();
};

class NISSANN_GTR : public Car
{
public:
    virtual void Drive();
    virtual void DoAction();
};

class Dump : public Car
{
public:
    virtual void Drive();
    virtual void DoAction();
private:
    void Dump(direction);
};

Car* myCar = new TOYOTA_2000_GT();
Car* yourCar = new NISSAN_GTR();
Car* workCar = new Dump();

myCar->Drive();
myCar->DoAction();

yourCar->Drive();
yourCar->DoAction();

workCar->Drive();
workCar->DoAction();

yourCar->DoAction()は、ドライブするだけなので、何もしないでしょうね。それでもいいんです。こう作れば、継承先がどんな特殊機能を持っていてもCarクラスでDoAction()すれば、実現できます。

今時点では、myCar, yourCar, workCarと3つの変数で動作を行っていますが、3つのオブジェクトを配列に入れて、for文でループさせれば、このコードはもっとコンパクトになります。ポリモーフィズムの最大効果です。「車を運転する」という抽象的な作業に対して、それぞれのオブジェクトがそれぞれの役目をキッチリ果たします。

もちろん、この方法以外にも「取り敢えず、現地まで私が運転するけど、荷台の操作はわからんので、誰かにやってもらう。」という考え方もありだと思います。その場合は、「もし、来たのがダンプなら」というif文が必要になるので、ロジックが複雑になります。そして、分岐点はテストの対象でもありますので、テスト工数がかかります。9台が車で、1台だけ特殊車両の構成なら、全体のバランスとしては、その1台のために分岐するのも悪いとは思えません。

いかがでしょうか。もう少しコードサンプルを提示できれば、もっとしっくりくるのかもしれませんが、時々このページは更新してわかりやすくしていきたいと思います。