オブジェクト指向プログラミングとは何か
オブジェクト指向プログラミングとは何か?
ある人は「多態性」、ある人は「多重定義(オーバーロード)」、ある人は「依存性の注入」として、オブジェクト指向プログラミングを説明するでしょう。
継承、仮想・抽象メンバー、オーバーライド、カプセル化など、オブジェクト指向プログラミングとは様々なものである、とも言えます。
「オブジェクトをモノに例える」説明は、オブジェクト指向プログラミングのクラスとメンバー、そして時には継承を説明したものです。
この記事ではオブジェクト指向プログラミングの用語とその意味の知識を共有します。
そして、「では、お前はオブジェクト指向プログラミングを何だと思っているのか?」を書かせて頂きます。
オブジェクト
大雑把に言えば、オブジェクト指向プログラミングで用いる様々な機能の総称です。
しかし、正確には総称とするのは誤りです。
データと機能の集合としての実体であり、個としての実体であり、プログラム言語によってもニュアンスが異なる場合がありますが、これらをオブジェクトとして捉えることも大切な考え方であるためです。
そして、アプリケーションや文書においてもオブジェクトとは、それぞれの環境下で意味するものが異なります。
目的や環境に応じてオブジェクトが指す機能や事柄、あるいは意味が異なる点に注意しましょう。
クラス
クラスとはインスタンス化されていない状態を意識する場合の用語です。
設計図とも言われますが、テンプレートであるとも言えます。
または、データと機能の集合としての実体とも言えます。
スーパークラス
継承元となる親のクラスです。
基底クラス、親クラスとも呼ばれます。
サブクラス
継承先となる子のクラスです。
派生クラス、子クラスとも呼ばれます。
インスタンス
インスタンスは、クラスをインスタンス化した状態を意識する場合の用語です。
実体とも言われますが、「クラスを元に生成された、実際に動作させることが可能な状態」とも言えます。
データと機能の集合としての実体に基づいて生成された、個としての実体とも言えます。
メンバー
クラスやインスタンスが持つメソッド、プロパティー、フィールド等の要素の総称です。
仮想メンバー
仮想メソッドと仮想プロパティーを指します。
プログラム言語によって異なりますが、オーバーライドすることを前提としています。
言い換えると、規定の実装とも表現できます。
抽象メンバー
抽象メソッドと抽象プロパティーを指します。
この抽象メンバーを含むクラスを「抽象クラス」と呼びます。
この抽象クラスを継承した子クラスでは、必ず抽象メンバーを定義しなくてはなりません。
インターフェース
インターフェースは単一継承の言語で良く使用されます。
インターフェースは多重継承が許されているためです。
抽象クラスは内部にコードの存在を許しますが、インターフェースは許しません。
つまり、インターフェースと抽象クラスとは、パソコンを例にするなら、キーボードやマウスの存在を示すものと説明できます。
インターフェースは、パソコン本体とディスプレイを別途用意して、マウスやキーボードを接続します。
抽象クラスは、パソコン本体とディスプレイは予め用意しておいて、マウスやキーボードを接続するイメージです。
【注記】
上記の説明も十分ではありません。
インターフェースは機能定義、抽象クラスは機能実装定義であり、故にインターフェースの設計は非常にシビアです。
この絶妙なニュアンスは実際に言語を学ぶ中で感覚を掴んでいけるでしょう。
ちなみにJavaは単一継承言語のため、インターフェースを利用する機会が多くあります。
このため用語として抽象クラスをインターフェースと呼んでいる場合があるので注意しましょう。
メソッド
機能に対するアクセス制御です。
これは、関数と言うこともできますが、クラスメソッド、インスタンスメソッド、仮想メソッド、抽象メソッドである点を考えると、単純な関数とは表現できません。
つまり、メソッドはメソッドなのです。
クラスメソッド
クラスからインスタンスを生成せずに、例えば Class.Method() のように直接呼び出すメソッドです。
プロパティー
データに対するアクセス制御です。
これは、変数と言うこともできますが、仮想プロパティー、抽象プロパティー、ミューテーター、アクセサー、非対称アクセス、自動実装、あるいは、メソッドと似た実装でもあります。
つまり、プロパティーはプロパティーなのです。
フィールド
メンバー変数とも呼ばれます。
オブジェクトが持つデータであり、インスタンスからアクセスします。
プロパティーは「アクセス制御の機能」であり、フィールドは「アクセスされるデータ」です。
オブジェクト内の変数と表現できますが、メンバー変数(インスタンスが持つプライベート変数)と説明した方が、より正確でしょう。
スコープ
オブジェクト指向プログラミングに限った話ではありませんが、参照範囲の名前空間に対する振る舞いです。
関数や変数はどこから見える?と考えてしまっても(少し語弊はありますが)構わないでしょう。
Pythonではグローバルスコープと、ローカルスコープの2種類があります。
グローバルスコープはすべての関数やクラスから参照できますが、使用するにはglobal文でグローバル変数を定義しなくてはいけません。
ローカルスコープは関数やクラス、またはそのメンバー内でのみ参照できます。
ただし、クラスで定義したメンバー変数は、クラス内の他のメンバーからself.__varとして参照することができます。
イテレーター
データ集合(コンテナーやコレクション)の繰り返し処理において、各要素にアクセスします。
ジェネレーター
イテレーターを生成します。
インデクサー
インスタンスに対するメソッドの明示がなくとも実行されるメソッドです。
依存性の注入
DI(Dependency injection)と呼びます。
過去にはInversion of Control: IoC(制御の逆転)と呼ばれ、これは、「必要な制御を自分で揃える」と対である「必要な制御を外部から揃える」のようなイメージです。
開発者は依存関係がはっきりしたコードを読むことができるので、コンベンジョン(規則)に従うだけで他のクラスを意識する必要がなくなります。
しかし、気を付けないと、サービスロケーターと呼ばれるアンチパターンを書き込んでしまう場合もあります。
ファクトリー
オブジェクトを生成するメンバーです。
日本語では「工場」の意味です。
オブジェクトの生成を動的に行うことを「ファクトリーパターン」と呼び、オブジェクトの生成を行うメソッドを個別に用意することを「ファクトリーメソッドパターン」と呼びます。
これらの手法によって、「クラスからオブジェクト生成のメソッドを追い出す」ことが可能となり、クラスの再利用性を高めます。
コンテナー
オブジェクトを格納できるオブジェクトです。
コンテナーはコレクションと呼ばれるものと同じです。
コンテナーは「入れ物」の意味です。
コレクションとは「集めた物」の意味であり、実体を意識しているか、その内容を意識しているかがよく分かりますね。
データの入れ物として意識する場合はコンテナー、処理や機能などで用いるデータ集合として意識する場合はコレクションと呼び分けるわけです。
コンテナーを具体的に挙げると、Pythonではbytes, bytearray, dict, frozenset, list, range, set, str, tupleを指します。
けれども、DIで用いる場合のコンテナーとはオブジェクトを格納できる必要があるため、dict, list, set, tupleになります。
カプセル化
外部からデータを隠蔽し、データを保護するアクセス制御です。
隠蔽されたデータは外部から直接アクセスすることができませんが、操作したり閲覧が必要な場合はミューテーター(セッター)とアクセサー(ゲッター)と呼ばれるプロパティーを実装します。
カプセル化は積極的に行い、オブジェクトの設計情報や内部情報が漏洩しないように注意しましょう。
継承
同じ事を書かないように機能を整理し、オブジェクトに階層構造を持たせます。
継承元を「親」、継承した側を「子」と呼び、親は複数持つことができ、子も複数持つことができます。
ひし形継承問題
「Aを継承したBとCがある。BとCを多重継承したDは、BとC、どちらのメソッドを継承すべきか?」と言う問題です。
AからのBとCの分岐、それがDへ集約する形が「ひし形」と言うわけです。
これは、メソッド順序解決(Method Resolution Order: MRO)とも呼ばれます。
この解決法はプログラム言語毎に異なりますので注意しましょう。
Pythonでは、ひし形継承問題をC3線形化によって解決しています。
class A(object): def __init__(self): print('A') super().__init__() class B(A): def __init__(self): print('B') super().__init__() class C(A): def __init__(self): print('C') super().__init__() class D(C, B): def __init__(self): print('D') super().__init__() d = D()
出力は次のようになります。
D C B A
まず、クラスDは、クラスC, Bの順に継承しています。
CとBは、クラスAを継承しています。
C3線形化はD→C→B→Aの順に解決していきます。
下記は、Pythonでのより実践的な例です。
class A(object): def __init__(self): print('start A') print('exit A') class B(A): def __init__(self, b): print('start B') super().__init__() print('exit B') class C(A): def __init__(self, c, **kwargs): print('start C') super().__init__(**kwargs) print('exit C') class D(C, B): def __init__(self): print('start D') super().__init__(b=1, c=1) print('exit D') d = D()
出力は次のようになります。
start D start B start C start A exit A exit C exit B exit D
この時、BとCのコンストラクターの引数が異なる点に注目してください。
解決順序を追ってみましょう。
①Dはsuper()でCのコンストラクターにアクセスします。
この時、キーワード引数で、b = 1とc = 1を明示しています。
②Cのコンストラクターは引数cを受け取ります。
この時、**kwargsで可変キーワード引数{'b': 1}も受け取ります。
③Cはsuper()で、Bのコンストラクターにアクセスします。
この時、Bのコンストラクターへ**kwargsで可変キーワード引数{'b': 1}を渡します。
④Bのコンストラクターは引数bを受け取ります。
⑤Bはsuper()で、Aのコンストラクターにアクセスします。
⑥Aのコンストラクターは引数を何も受け取りません。
多態性
ポリモーフィズムや、多相性とも呼ばれます。
同じ名前のメソッドでも、引数の型や個数に応じて、個別の機能にアクセスできることです。
多態性と多重定義(オーバーロード)をごちゃまぜにしないように注意しましょう。
これらは広い意味では関係性がありますが、狭い意味では別のものです。
多態性には次のような種類があります。
多重定義
オーバーロードとも呼ばれます。
引数の型や個数が異なる場合、同一名のメソッドを複数個持つことができます。
多態性と似ていますが、多態性は「使う側」の話で、多重定義(オーバーロード)は「動作する側」の話です。
ちなみに、PHPでのオーバーロードは「動的プロパティーやメソッドの生成」を意味しているので、まったくの別物です。
オーバーライド
継承した親クラスのメソッドと、子クラスのメソッドが名称、引数の型と数が同じである場合、子クラスのメソッドが呼び出されることです。
多重定義(オーバーロード)や多態性とは広い意味では関係性がありますが、別のものとして考えましょう。
単純な上書きとは異なるので誤解しないように注意しましょう。
【おまけ】ダック・タイピング
同一のメンバーを持つオブジェクトであれば、入れ替えて使用できるプログラミング手法です。
動的型付けが可能な柔軟なプログラム言語で利用できるテクニックです。
以下は、自分自身にアヒルと犬を憑依させ、自己紹介の上で鳴き声を出す例です。
class Duck(object): def __init__(self): print('I\'m a duck') def sound(self): print('quack quack !') class Dog(object): def __init__(self): print('I\'m a dog.') def sound(self): print('bow wow !') def Me(soul): soul.sound() Me(Duck()) Me(Dog())
出力は次のようになります。
I'm a duck quack quack ! I'm a dog. bow wow !
オブジェクト指向プログラミングとは何か
私はオブジェクト指向プログラミングとは「目的を意識する姿勢」であると考えています。
特定の機能や手法を指すのではなく、メンバーとクラス、変数や関数の正確な命名を基礎として、DIによって依存するオブジェクトの目的を鮮明にして、正しい名前を付けた上で追い出すのです。
その上で、オブジェクトが持つ機能の目的を明瞭にして再利用性を高めます。
私の認識では、オブジェクト指向プログラミングとは一種の哲学であり、「これはナイス!」と言うテクニックや機能、運用性や知識と言った英知の蓄積です。
プログラマーがコーディングし、それが動くことは当然できることですが、「その先」の目的を意識させるのが、オブジェクト指向プログラミングなのではないでしょうか。
これらを、より平たく言うなら「思いやりを大切にしようプログラミング」になるかも知れません。