PHPでオブジェクト指向プログラミングを守備範囲に パート1:概念と構文
オブジェクト指向プログラミングを学んでコードをきれいにする
PHPは、Javaと違って手続き型のコーディング・スタイルとオブジェクト指向型(OO)コーディング・スタイルの両方を利用できます。これは、PHPは初めてというRPGプログラマーにとっては良い知らせです。というのも、PHPでは馴染みのある手続き型コーディングを使用してウェブ・アプリケーションを構築して稼動することができ、しかもまったく異なるプログラミング・パラダイムを取り入れる必要もなく、ウェブ・プログラミングに関わる多くの新しい概念や言語を扱う必要がないのです。ただし、商用のPHPアプリケーション・パッケージのほとんどはオブジェクト指向技術を使用しています。それはオブジェクト指向を取り入れることでコードをよりきれいにすることができ、保守や拡張が容易でバグが少なくなるからです。オブジェクト指向プログラミングは最初はとっつきにくいように見えるかもしれませんが、その基本を理解することはそれほど難しくはなく、また少しの練習で実際に適用することが可能です。本稿の目的とするところは、いくつかの簡単な例を使用してオブジェクト指向の基本的な概念と構文を紹介することです。今後の記事ではもっと深く、高度なオブジェクト指向のテクニックを探り、簡単な例にとどまらずに紹介し、オブジェクト指向のテクニックをIBM iなどといったデータベース中心のアプリケーション環境にも適用していきます。
オブジェクト指向プログラミングの基本的な目標は、再利用可能で信頼性の高いソフトウェア・コンポーネントを作成し、さまざまな状況下でより大規模なシステム構造の中に組み込まれるようにすることです。この目的を達成するために最も重要なオブジェクト指向の概念がカプセル化です。カプセル化とはデータとそれに関連するコードを1つにまとめて機能に対する簡単なインタフェースを提供することで、ソフトウェアの開発者がその機能がどのような仕組みになっているのかを理解する必要がないようにすることです。うまく設計されている製品でよく見られるように、オブジェクト指向のコンポーネントもその設計を理解していなくても簡単に使用できるようになっているべきです。たとえば自動車を考えてみましょう。自動車を設計したり製造したりするより自動車を運転するほうがずっと簡単です。オブジェクト指向プログラミングはソフトウェアの開発にそのような簡素化をもたらすものです。データ構造と関数で構成される(クラスと呼ばれる)テンプレートを設計することでオブジェクトをカプセル化します。クラス中のデータへのアクセスはクラス関数によって提供されるのが一般的です。オブジェクトを理解するには関数をしっかり理解しておくことが重要です。PHPの関数に通じておらず、アプリケーション中にカスタマイズあるいは事前に定義された関数をどのように組み入れるのかに通じていないのであれば、本稿のオンライン版の「PHPでユーザー定義関数を記述する」を参照してください。
なぜオブジェクトなのか
PHPの関数は基本的で再利用可能でしかも抽象的な機能を提供します。関数を記述すればテスト済みの信頼性の高いコード・モジュールのライブラリを構築してあらゆるアプリケーションに組み入れることができます。ではオブジェクト指向のコーディングはこの関数ライブラリでは得られないどのようなものを提供してくれるのでしょうか。オブジェクト指向のクラスは関数ライブラリと似ていますが、共通のデータ要素集合に対して動作する関連した関数のグループを定義することによって、関数ライブラリよりもさらにコード・ベースをきれいにしてくれます。まずは例を見てから構文要素について確認していきましょう。簡単な「Person」クラスからはじめます。
クラスはオブジェクトのテンプレートであることを覚えておいてください。つまり、クラスは各オブジェクトが持つことになるデータ要素とそのデータ要素に対して動作する関数を定義します。
図1はPersonという抽象的なオブジェクトに関連したデータと関数を記述したクラス定義です。この例では姓と名を扱います(コードの4行目と5行目の変数宣言を見てください)。また、__construct()、getFullName()、sayHello()という3つの関数をこのクラスで定義しています。(構文要素についてはのちほど説明します。)
さらに詳しく見て行く前にオブジェクト指向の用語について説明しましょう。クラス内(しかも関数の外)で定義されている変数はプロパティと呼ばれ、クラス内で定義されている関数はメソッドと呼ばれます。プロパティもメソッドもそれが定義されているクラスのメンバーです。クラスのプロパティはPHP変数で、クラスのメソッドを定義するときはfunctionというキーワードを使用しなければなりません(PHPにはmethodというキーワードはありません)。メソッドはPHPの関数と同様にコーディングされ、ローカルのデータのスコープやパラメータの渡し方、戻り値等に関して同じルールに従います。ただし、プロパティとメソッドはクラス定義から作成されたオブジェクトとの関係においてのみアクセスすることが可能です(これについては後述します)。
クラス定義はclassというキーワードに続いてクラスの名前、すべてのプロパティとメソッドを中括弧で囲んだもので構成されます。
クラス名はそのオブジェクトを表す名詞でなければならず、標準的には大文字で始まって大文字と小文字を混ぜたり下線を使ったりして単語を区切ります(たとえば、Customer、Order、Product、HTML_InputForm、DB2_Connection、ErrorLogといったように、です)。プロパティの集合(変数)とそのプロパティに対して遂行できるアクション(関数)に集約できるものは、オブジェクト・クラスとしてモデル化できます。プロパティはクラスの始めの部分に(メソッドの前に)コーディングし、各クラスは別々のPHPファイルに格納するのが一般的な習慣で、このファイルはrequire_once()関数を経由してアプリケーション中に含まれます。(もっと複雑なシステムでは、関連するクラスを1つのPHPファイルにグループ化してアプリケーションのスクリプトで必要となるファイルの数を減らすのが一般的です。) クラス定義自体では何も起こらずコードも実行しません。クラス定義は単にオブジェクトを作成するためのテンプレートに過ぎません。クラス中のプロパティやメソッドにアクセスできるようにするには、クラスのインスタンス(すなわちオブジェクト)を作成しなければなりません。
オブジェクトのインスタンス化
「クラスをインスタンス化する」という表現は「オブジェクトを作成する」ということを意味します。オブジェクトを作成する時それはクラスの「インスタンス」を作成しているので、つまりクラスをインスタンス化しているということになります。クラスからオブジェクトを作成するにはnewというキーワードに続いてクラス名を記述します。これによりそのクラスのインスタンスが返され、変数に割り当てることができます。Personという名前のクラスを使ってPersonというオブジェクトを作成しているアプリケーションを図2に示します。図2に示したスクリプトをブラウザで実行すると次のような出力が得られます。
図2の2行目のコードはPersonクラスの定義を引用しています。4行目と5行目ではPersonオブジェクトのインスタンスを作成し、作成したオブジェクトへの参照を変数$bobと$tomに格納しています。オブジェクトのインスタンス化を大雑把に例えるためにデータベースと比較すると、クラスはファイルのレイアウトに似ていて各レコード内にどのようなフィールドがあるのかを定義するものです。そしてオブジェクトはそのファイル中のレコードに似ています。(例えの話はここまでにしておきます。オブジェクトのインスタンス化にはデータベースは関係しないからです。データはPHPスクリプトの実行中はメモリ上に保持されています。) 各オブジェクト(つまりクラスのインスタンス)にはクラスと同じプロパティ(変数)の集合が含まれていますが、それらのフィールド(データベースのレコードのようなもの)には異なるデータが入っています。オブジェクトへの参照を変数に割り当てることでデータ構造全体を1つの実体として扱うことができます。
オブジェクト内のプロパティ(データ)やメソッド(関数)を取得するにはオブジェクト・メンバー・アクセス演算子を使用しなければなりません。この演算子のことを別名矢印演算子と呼び、「->」と表します。たとえば、図2の7行目と10行目ではオブジェクト$bobと$tomのPersonクラスのメソッドを呼び出しています。
「オブジェクトとデータベースのレコードとの類似は理解できたけどメソッドはどうなのですか」とおっしゃる方がいるかもしれません。関数のコピーがオブジェクトごとに作られるのでしょうか。答えはノーです。もうひとつのIBM iの例えを使うとすると、オブジェクトはRPGプログラムのようなもので、発注プログラムを実行している各ユーザーはそれぞれ自分専用のデータ集合を持っていますが、実行可能なコードは皆で共有しています。オブジェクト$bobおよび$tomに対してgetFullName()メソッドを呼び出す場合、同じコードを呼び出すことになりますが、呼び出されたコードは異なるデータの集合にアクセスします。
コンストラクタ
newというキーワードを使用してオブジェクトを作成するとき、クラスの中のコンストラクタという特別なメソッドを呼び出します。このメソッドは常に__construct()という名前です(constructの前に下線が2つある点に注意してください)。PHPでは2つの下線で始まるメソッドのことを「魔法の」メソッドと呼びますが、何が魔法かというとオブジェクトのライフサイクルの間のある特定のときに自動的に呼び出されるということだけです。たとえば、__construct()メソッドはオブジェクトが作成されたときに呼び出されます。また__destruct()メソッドをコードに記述すると(たとえばPHPのunset()関数を使用したりスクリプトが終了したりしたときに)オブジェクトが破壊される際に呼び出されます。オブジェクトのコンストラクタはRPGでいうと*INZSRのようなものです。つまり各オブジェクトに必要な初期化のタスクを__construct()と一緒に記述します。図1で説明したPersonコンストラクタを図3に示します。
このメソッドは名と姓という2つのパラメータを受け取ってオブジェクトのfirstNameプロパティおよびlastNameプロパティに割り当てます。newというキーワードを使用してオブジェクトのインスタンスを作成したときに__construct()が呼び出されるということを覚えておいてください。図2の4行目と5行目に記述した通り、__construct()にパラメータを指定するとnewでクラスをインスタンス化するときにそのパラメータを渡すことができます。
クラスのメンバー
__construct()メソッドについて最初にお話しておかなければならないのは、このメソッドはオブジェクト・アクセス演算子を使用するのですが(図3の8行目および9行目)、アクセスしているオブジェクトの名前が$thisであるということです。$thisは特別なオブジェクトで、クラスの現在のインスタンスを参照しています。図2で$tomオブジェクト上のメソッドを呼び出す場合、このメソッドが図3にあるように$this->firstNameを参照していると、これは$tomのfirstNameにアクセスしていることになります。$thisはクラスのメソッド中でのみ使用することができ、クラス定義の外にあるアプリケーション・コードからは使用できませんし、$thisは自分が使用されているクラスのインスタンスのプロパティとメソッドを参照しています。クラスのメソッド内では$thisを使用して同じクラスのインスタンスのプロパティやメソッドにアクセスしなければなりません。$this->firstNameではなく$firstNameを参照すると、図1の3行目のコードでクラス・レベルで宣言された$firstNameプロパティではなく、そのメソッドにローカルな新しい変数を作成することになります。
また$thisを使用してクラスのメソッドを同じクラス定義の中から呼び出します。図4の18行目はPersonクラスを示しています。sayHello()メソッドはgetFullName()メソッドを呼び出しており、このgetFullName()もまたPersonクラスで定義されています。同じクラス内であるメソッドが別のメソッドを呼び出すときは、この$this->を使用しなければなりません。
メンバーの可視性
図4のプロパティやメソッドの定義の前になぜpublicという単語があるのか不思議に思うかもしれません。各メンバーの定義にはそのメンバーの「可視性」(すなわち、そのプロパティやメソッドへはどこからアクセス可能か)を指定しなければなりません。publicというキーワードはそのメンバーがクラス・メソッドの内部からだけでなく、クラス定義の外側から(たとえばアプリケーション・コード中から)も直接アクセス可能であることを示しています。この他に指定できる可視性のエリアとしてprivateとprotectedの2つがあります。privateとはメンバーがクラス自身のメソッド内からアクセスが可能ですが、そのクラスを使用しているアプリケーションからはアクセスできないという意味です。protectedもprivateと似ていますが、メンバーの可視性をそれが定義されているクラスの親クラスと子クラスまでに拡張したものです。
本項で紹介している例では私はPersonクラスをpublicとして定義したので、Personクラスの定義の外側からそのメソッドを呼び出すことができます(図2の7行目と10行目のコード)。getFullName()メソッドとsayHello()メソッドをpublicではなくprivateとして定義していたら、Personクラスのメソッド内からのみアクセスが可能で、図2にあるスクリプトを実行しようとすると以下に示したものと同様な実行時エラーとなります。
firstNameプロパティもlastNameプロパティもpublicとして定義したので、図2のスクリプトから直接これらのプロパティにアクセスすることができます。たとえば次のように記述する代わりに、
次のように記述します(結果は同じになります)。
クラス内のprivateの可視性を見ていて関数内のローカル変数のことを思い浮かべたのであればポイントを掴んでいるかもしれません。ローカル関数の変数が関数の内部作業とその関数を呼び出している外部コードとを区別するように、privateの定義によりオブジェクトの内部データや機能を保護することができ、クラスのインタフェースを必要最小限に簡素化することができます。
GetterとSetter
クラスが内部で使用するだけで外部世界からは興味のないプロパティやメソッドのみをprivateとし、それ以外のすべてのプロパティやメソッドはpublicとすべきであるという考えがあります。これは理にかなっているように見えますが、ベスト・プラクティスの開発者やオブジェクトの純粋主義者と呼ばれる人たちの大多数は、すべてとは言わないまでもほとんどのプロパティはprivateと宣言すべきで、「getter」とか「setter」とか呼ばれるプロパティの値を取得または設定するためのpublicのメソッド経由でのみアクセス可能にすべきであると勧めています。(プロパティに対する読み取り専用の可視性があれば良いのにと常々思っていますが残念ながらそのような可視性はありません。プロパティにアクセスできればその値を取り出すだけでなく値を変更することもできます。プロパティを読み取り専用にしたい場合はprivateプロパティとして宣言してpublicのgetterメソッドを提供しpublicのsetterメソッドを提供しないようにする必要があります。) getterおよびsetterの別の呼び方としてaccessorメソッドというものがあります。これはクラスのデータにアクセスを提供するからということです。
クラスのプロパティをprivateにしてaccessorメソッドで包み隠すことで、データ要素をセットする前に値に対するフィルタリングの追加やエラーのチェックなどといったアクションを遂行し、さまざまなアプリケーションがその値を取り出す前にデータ要素をフォーマッティングすることができます。たとえば、setFirstName()というメソッドを作って最大長をチェックして、firstNameに対して提供された値がデータベースのフィールドに適合しない場合にエラーをスローする等といったことができます。
図5では、firstNameプロパティとlastNameプロパティをprivateの可視性に変えてsetFirstName()とget FirstName()というpublicのメソッドを追加し、firstNameプロパティへのアクセスを制御しています。setFirstName()メソッドは$firstというパラメータ中の値がオブジェクト内部のfirstNameの値として使用される前に、checkFirstName()というprivateなメソッドを呼び出してその値を検証しています。getFirstName()メソッドはfirstName中の文字列値をPHP組み込みの関数htmlentities()で実行することでエスケープして、ウェブ頁上で正しく表示されるようにしています。(htmlentities()関数の詳細についてはbit.ly/cnudtm.をご覧ください。) また、PersonクラスにsetLastName()とgetLastName()という同様のメソッドのペアを追加することもできます。
図5のcheckFirstName()メソッドは$first中の値を検証し、検証に失敗した場合は例外をスローし、成功した場合はtrueの値を返します。checkFirstName()メソッドはprivateとして宣言されており__construct()とsetFirstName()の両メソッドから呼び出されています。checkFirstName()メソッドは内部処理のためだけに必要なメソッドなのでprivateにすることができます。つまり、このクラスのメソッドの外側のコードからはこれを呼び出すことはできません(たとえば、図2のスクリプトから以下のようなコマンドを呼び出すことはできません)。
もちろんアプリケーションがcheckFirstName()を直接呼び出す必要があるのであれば、これをpublicとして宣言することができます。
accessorメソッドを使用することは、クラス・インタフェース(ユーザーがそれをどう見るか)とクラスの実装(それがどのように設計されているか)との間の抽象階層を提供することでもあります。クラスの実装を変更する必要が生じた場合でも、そのクラスを使用しているコードを変更する必要はありません。たとえば将来的に、名や姓を表すプロパティの代わりにPersonの名前を表すのにNameオブジェクトと接頭辞、接尾辞、肩書き、名、姓、ミドルネームのイニシャルなどを表すフィールドが必要になるかもしれません(そうです、他のクラスのオブジェクトとなるクラスプロパティ、すなわち入れ子のオブジェクトを持つこともできます)。accessorメソッドを使用すると名と姓用のgetterおよびsetterメソッドは変えずにPersonクラスのコードを変更し、Personクラスを使用しているアプリケーションは変更する必要がありません。名と姓用のaccessorメソッドは新しいNameオブジェクトにアクセスするためにそのコードを変更しなければなりませんが、Personクラスの使用者はそのことを知る必要がないのです。
例外処理
前述の例でオブジェクト指向のコードでエラーがどのように処理されているかに注意してください。checkFirstName()関数を見てみましょう。
$first中の文字列の長さが40文字以上の場合にエラーを発してほしいのです。そうであればecho文を使ってエラーをブラウザ上に表示させればよいだけではないでしょうか。このメソッドがどのような状況で呼び出されたのかわからないからです。ブラウザ・アプリケーションに呼び出されたかもしれませんし、ウェブ・サービスあるいはPersonオブジェクトを作成した無数のアプリケーションかもしれません。PHPのバージョン5.1.0には例外処理用の組み込みサポートがあり、catchというキーワードを使用してExceptionクラスのオブジェクトをコース・スタックから上がってきてキャッチされるまでオブジェクトがスローできるようにしています。このようにすれば、エラーが発生したときにオブジェクトがどのように使用されていたのかを知る必要がなくなります。問題発生を知らせてそのオブジェクトを使用していたアプリケーションに例外処理をさせればよいのです。
ExceptionはPHPのコア部分に組み込まれているので、そこからオブジェクトを作成することができる点に注意してください。checkFirstName()の中でキーワードnewを使用してExceptionオブジェクト(Exceptionクラスのインスタンス)をスローしています。
次のコードは参照されていないExceptionオブジェクトをインスタンス化してそれをスローしています。
このオブジェクトがcatchキーワードを使用する方法などでキャッチされると変数$eに割り当てられます。
throw文の中をご覧になるとおわかりの通り、Exceptionクラスのコンストラクタは文字列パラメータを受け取り、内部プロパティ$messageにセットします。このExceptionをキャッチするアプリケーション中のExceptionクラスのpublicのgetMessage()プロパティ(getterメソッド)を使用して、メッセージを取り出してそれを適切な相手に伝えることができます。
さらに進んで図2のスクリプトは図6のようなコードになります。図6のアプリケーションはtry-catchブロックを使用してオブジェクトがスローしてきた例外を処理しています。これには、エラーをスローする可能性があるとわかっているオブジェクトを使用するtryしたいコードの塊を中括弧で囲み、その前にキーワードtryを記述します。
図6の8行目ではtryブロックに続いてcatchキーワードを書き、そして変数$eに割り当てられるExceptionオブジェクトをキャッチするように指定します。これに続いて、例外をキャッチした時に取る適切なアクションを実行するコードの塊(catchブロックと呼ぶ)を記述します。これはRPGのMONITOR/ON-ERROR機能と似ています。catchブロック内ではExceptionオブジェクト中のpublicメソッドやプロパティにアクセスすることができます。図6の9行目および10行目のコードで2つのgetterメソッドgetMessage()とgetTraceAsString()を呼び出しています。(Exceptionクラスのドキュメントはphp.net:bit.ly/XMTOXmにあります。)
クラス・インタフェースの定義を図7に示します。Exceptionクラスにはメッセージ文字列、例外コード、例外が生成されたPHPファイルの名前、例外を生成したPHPファイル中の行番号の4つのプロパティがあります。この4つのプロパティはすべてprotectedのプロパティですから直接アクセスすることはできません。このデータにアクセスするにはpublicのgetterメソッドのいずれか(「get」で始まるもの)を使用します。たとえば以下のように使用して例外のメッセージデータを取得します。
このメソッドの定義をどのように読めばよいのか迷われるかもしれません。たとえば
は以下のように読みます。
- final - Exceptionクラスから継承した任意のクラスはこのメソッドをオーバーライドできない
- public - このメソッドの可視性
- string - メソッドが返す値のデータ・タイプ
- getMessage - メソッドの名前
- void - このメソッドに渡すパラメータはない
Exceptionクラス中ではコンストラクタ・メソッド(__construct)がパラメータを3つまで受け取り、四角括弧で囲まれていることからお分かりの通り、この3つのパラメータはいずれもオプションであることに注意してください(図7)。
魔法のメソッド
__constructメソッドに加えて、Exceptionクラスにはこの他に__toStringと__cloneという2つの「魔法の」メソッドが定義されています。下線2つで始まる魔法のメソッドはオブジェクトに対してあることが発生したときに自動的に呼び出されるということを思い出してください。たとえば__toStringはクラスのオブジェクトが文字列変数のように扱われたときに常に呼び出されます。たとえば$eという名前のExceptionオブジェクトがあって次のような文があったとします。
ここでecho関数は表示すべき文字列を期待しています。オブジェクトをechoしようとする場合、PHPはそのオブジェクトのクラス定義中の__toString()というメソッドを探しそれを自動的に呼び出します。__toString()メソッドを使用して独自のオブジェクトを作成してこの状況に対応することもできます。__toString()メソッド中では、オブジェクトのデータの値を適切なラベルや改行をつけてフォーマットし、フォーマット済みの文字列を返すことができます。
__cloneメソッドはオブジェクトのコピーを作成しようとしたときに自動的に呼び出されます。これにはキーワードcloneを使用します。
cloneを使用しないと、元のオブジェクトに対する参照を別の変数に作成するだけになってしまいます。
この例では、$bobと$bob2はいずれも同じオブジェクトを指し示しています。__clone()メソッドおよびその他の全ての魔法のメソッドの詳細についてはphp.net:bit.ly/172anbをご覧ください。
時間をかけてオブジェクト指向プログラミングを学んでください
本稿では、データのカプセル化とそれに関連した機能がオブジェクト指向プログラミングの概念の中心であることについて説明しました。オブジェクト指向がスタンドアロン関数によって提供されるカプセル化以上のものであるかをおわかりいただけ始めたかと思います。クラス定義の基本についても説明しましたが、これはオブジェクトのインスタンス化のためのテンプレートを作成し、それぞれにそのクラスに対して定義されたデータ要素のコピーが存在します。また、オブジェクトのインスタンス化のためのnewキーワード、オブジェクトのメンバーへのアクセス演算子(->)、$thisを使った内部クラス・メンバーへのアクセスなどといったオブジェクト指向に関連した用語や構文についても説明しました。
require_once()使用してクラス定義をアプリケーション・コードにどのように取り入れるかについての例もご紹介しました。メンバーの可視性(publicとprivate)やgetterメソッドやsetterメソッドを一般的にどのように使用して内部クラス・データへのアクセスを制御しているのかについても触れました。魔法のメソッドについても説明し、その中で最もよく使われるのがオブジェクト・コンストラクタ(__construct)であることも説明しました。また、throw、try、catchキーワードなど例外の処理方法についても紹介し、PHPの組み込みクラスの1つであるExceptionのインタフェース定義についても少しだけ触れました。
オブジェクト指向プログラミングは手続き型のコーディングとは異なるスタイルのプログラミングです。データ構造の定義およびそれに関連した機能やアクセス制御の定義を用いると、アプリケーション領域の実体の属性やふるまいをモデル化した抽象化システム・コンポーネントを構築することができます。RPGや手続き型PHPコーディングのバックグラウンドをお持ちであれば、オブジェクト指向プログラミングがどのように役に立つのかに慣れるには練習が必要かもしれません。習得するオブジェクト指向の概念は多数ありますし、構文にも馴染みがないかもしれません。
本稿に記載した例は可能な限り簡素なものにしました。本シリーズの次回の記事ではより詳細な例や継承について説明します。これらは既に定義されたクラスを出発点として使用してカスタマイズしたデータや機能で拡張する強力なテクニックです。また、RPGプログラマーにはお馴染みのリレーショナル・データベース・プログラミングの世界がオブジェクト指向の世界とどのように関係しているのかについて、リレーショナル・データベースのテーブルやフィールドをオブジェクト指向のクラスやプロパティにマッピングする戦略等について説明します。では次回までPHPのクラスやオブジェクトのマニュアルについてbit.ly/kdOf0をご覧ください。
オブジェクト指向プログラミングは最初はとっつきにくいように見えるかもしれませんが、その基本を理解することはそれほど難しくはなく、また少しの練習で実際に適用することが可能です。