オブジェクト指向プログラミング入門
「OOP」という用語はオブジェクト指向プログラミングを指し、これはコードを整理し構造化する方法です。OOPでは、プログラムを一連のコマンドや関数の代わりに、互いに通信するオブジェクトの集合として見ることができます。
OOPにおいて、「オブジェクト」とは、データとそのデータを操作する関数を含む単位です。オブジェクトは「クラス」に基づいて作成され、クラスはオブジェクトの設計図やテンプレートと考えることができます。クラスがあれば、その「インスタンス」を作成できます。これは、そのクラスに基づいて作成された具体的なオブジェクトです。
PHPで簡単なクラスを作成する方法を見てみましょう。クラスを定義する際には、キーワード「class」を使用し、その後にクラス名、そしてクラスの関数(「メソッド」と呼ばれる)と変数(「プロパティ」と呼ばれる)を囲む波括弧が続きます:
この例では、Auto
という名前のクラスを作成し、honk
という名前の関数(または「メソッド」)を1つ含んでいます。
各クラスは、1つの主要なタスクのみを解決する必要があります。クラスが多くのことを行いすぎる場合は、より小さく、特化したクラスに分割するのが適切かもしれません。
コードを整理し、ナビゲートしやすくするために、通常、クラスは別々のファイルに保存します。ファイル名はクラス名に対応する必要があるため、Auto
クラスの場合、ファイル名はAuto.php
になります。
クラスに名前を付ける際には、「PascalCase」という規則に従うのが良いでしょう。これは、名前の各単語が大文字で始まり、アンダースコアや他の区切り文字がないことを意味します。メソッドとプロパティは「camelCase」規則を使用します。これは、小文字で始まることを意味します。
PHPの一部のメソッドには特別な役割があり、__
(2つのアンダースコア)のプレフィックスでマークされています。最も重要な特殊メソッドの1つは「コンストラクタ」であり、__construct
としてマークされています。コンストラクタは、クラスの新しいインスタンスを作成するときに自動的に呼び出されるメソッドです。
コンストラクタは、オブジェクトの初期状態を設定するためによく使用されます。例えば、人を表すオブジェクトを作成する場合、コンストラクタを使用してその年齢、名前、またはその他のプロパティを設定できます。
PHPでコンストラクタを使用する方法を見てみましょう:
この例では、Person
クラスにはプロパティ(変数)$age
があり、さらにこのプロパティを設定するコンストラクタがあります。メソッドgetAge()
は、人の年齢にアクセスすることを可能にします。
疑似変数$this
は、クラス内でオブジェクトのプロパティやメソッドにアクセスするために使用されます。
キーワードnew
は、クラスの新しいインスタンスを作成するために使用されます。上記の例では、年齢25歳の新しい人を作成しました。
オブジェクト作成時に指定されない場合、コンストラクタのパラメータにデフォルト値を設定することもできます。例えば:
この例では、Person
オブジェクトを作成する際に年齢を指定しない場合、デフォルト値20が使用されます。
嬉しいことに、プロパティの定義とそのコンストラクタによる初期化は、このように短縮および簡略化できます:
完全を期すために、コンストラクタに加えて、オブジェクトにはデストラクタ(メソッド
__destruct
)もあり、これはオブジェクトがメモリから解放される前に呼び出されます。
名前空間
名前空間(英語では「namespaces」)を使用すると、関連するクラス、関数、定数を整理してグループ化し、同時に名前の衝突を回避できます。これらはコンピュータのフォルダのようなものと考えることができ、各フォルダには特定のプロジェクトやテーマに属するファイルが含まれています。
名前空間は、大規模なプロジェクトや、クラス名の衝突が発生する可能性のあるサードパーティのライブラリを使用する場合に特に役立ちます。
プロジェクトにAuto
という名前のクラスがあり、それをTransport
という名前の名前空間に配置したいと想像してください。(Doprava
→ Transport) 次のようにします:
別のファイルでAuto
クラスを使用したい場合は、クラスがどの名前空間から来ているかを指定する必要があります:
簡略化のために、ファイルの先頭で使用したい特定の名前空間のクラスを指定できます。これにより、完全なパスを指定せずにインスタンスを作成できます:
継承
継承はオブジェクト指向プログラミングのツールであり、既存のクラスに基づいて新しいクラスを作成し、そのプロパティやメソッドを引き継ぎ、必要に応じて拡張または再定義することができます。継承により、コードの再利用性とクラス階層が保証されます。
簡単に言えば、1つのクラスがあり、それから派生した別のクラスをいくつかの変更を加えて作成したい場合、元のクラスから新しいクラスを「継承」できます。
PHPでは、キーワードextends
を使用して継承を実現します。
私たちのPerson
クラスは年齢に関する情報を保持しています。Person
を拡張し、研究分野に関する情報を追加する別のクラスStudent
を持つことができます。
例を見てみましょう:
このコードはどのように機能しますか?
- キーワード
extends
を使用してPerson
クラスを拡張しました。これは、Student
クラスがPerson
からすべてのメソッドとプロパティを継承することを意味します。 - キーワード
parent::
を使用すると、親クラスのメソッドを呼び出すことができます。この場合、Student
クラスに独自の機能を追加する前に、Person
クラスからコンストラクタを呼び出しました。そして同様に、学生に関する情報を表示する前に、祖先のメソッドdisplayInfo()
を呼び出しました。
継承は、クラス間に「is-a」関係が存在する状況を対象としています。例えば、Student
はPerson
です。猫は動物です。これにより、コードで1つのオブジェクト(例:「Person」)を期待する場合に、代わりに継承されたオブジェクト(例:「Student」)を使用する可能性が得られます。
継承の主な目的はコードの重複を防ぐことではないことを認識することが重要です。逆に、継承の不適切な使用は、複雑で保守が困難なコードにつながる可能性があります。クラス間に「is-a」関係が存在しない場合は、継承の代わりにコンポジションを検討する必要があります。
Person
クラスとStudent
クラスのdisplayInfo()
メソッドが少し異なる情報を表示することに注意してください。そして、このメソッドの他の実装を提供する追加のクラス(例えばEmployee
)を追加できます。(Zamestnanec
→ Employee)
異なるクラスのオブジェクトが同じメソッドに異なる方法で応答する能力は、ポリモーフィズムと呼ばれます:
コンポジション
コンポジションは、他のクラスのプロパティやメソッドを継承する代わりに、単にそのインスタンスを自分のクラス内で利用するテクニックです。これにより、複雑な継承構造を作成することなく、複数のクラスの機能とプロパティを組み合わせることができます。
例を見てみましょう。Engine
クラスとCar
クラスがあります。(Motor → Engine, Auto
→ Car)
「車はエンジンである」と言う代わりに、「車はエンジンを持つ」と言います。これは典型的なコンポジションの関係です。
ここでは、Car
はEngine
のすべてのプロパティとメソッドを持っているわけではありませんが、プロパティ$engine
を通じてそれにアクセスできます。
コンポジションの利点は、設計の柔軟性が高く、将来の変更の可能性が向上することです。
可視性
PHPでは、クラスのプロパティ、メソッド、定数に対して「可視性」を定義できます。可視性は、これらの要素にどこからアクセスできるかを決定します。
- Public:
要素が
public
としてマークされている場合、クラス外からでもどこからでもアクセスできることを意味します。 - Protected:
protected
とマークされた要素は、そのクラス内およびそのすべての子孫(このクラスから継承するクラス)からのみアクセス可能です。 - Private:
要素が
private
の場合、それが定義されたクラス内からのみアクセスできます。
可視性を指定しない場合、PHPは自動的にpublic
に設定します。
サンプルコードを見てみましょう:
クラスの継承を続けます:
この場合、ChildClass
クラスのdisplayProperties()
メソッドは、パブリックおよびプロテクテッドなプロパティにアクセスできますが、親クラスのプライベートなプロパティにはアクセスできません。
データとメソッドは可能な限り隠蔽し、定義されたインターフェースを通じてのみアクセス可能であるべきです。これにより、コードの残りの部分に影響を与えることなく、クラスの内部実装を変更できます。
final
キーワード
PHPでは、クラス、メソッド、または定数が継承またはオーバーライドされるのを防ぎたい場合に、final
キーワードを使用できます。クラスをfinal
としてマークすると、拡張できません。メソッドをfinal
としてマークすると、子クラスでオーバーライドできません。
特定のクラスやメソッドがさらに変更されないことを知っていると、潜在的な競合を心配することなく、変更をより簡単に行うことができます。例えば、どの子孫もすでに同じ名前のメソッドを持っていて衝突が発生するという心配なしに、新しいメソッドを追加できます。または、メソッドのパラメータを変更することもできます。なぜなら、子孫でオーバーライドされたメソッドとの不整合を引き起こすリスクがないからです。
この例では、finalクラスFinalClass
からの継承の試みはエラーをスローします。
静的プロパティとメソッド
PHPでクラスの「静的」要素について話すとき、それは特定のクラスインスタンスではなく、クラス自体に属するメソッドとプロパティを意味します。これは、それらにアクセスするためにクラスのインスタンスを作成する必要がないことを意味します。代わりに、クラス名を介して直接呼び出したりアクセスしたりします。
静的要素はクラスに属し、そのインスタンスには属さないため、静的メソッド内で疑似変数$this
を使用することはできないことに注意してください。
静的プロパティの使用は落とし穴だらけの不明瞭なコードにつながるため、決して使用すべきではなく、ここでは使用例も示しません。対照的に、静的メソッドは便利です。使用例:
この例では、2つの静的メソッドを持つCalculator
クラスを作成しました。これらのメソッドは、::
演算子を使用してクラスのインスタンスを作成せずに直接呼び出すことができます。静的メソッドは、特定のクラスインスタンスの状態に依存しない操作に特に役立ちます。
クラス定数
クラス内で定数を定義するオプションがあります。定数は、プログラムの実行中に決して変更されない値です。変数とは異なり、定数の値は常に同じままです。
この例では、定数NumberOfWheels
を持つCar
クラスがあります。クラス内で定数にアクセスしたい場合は、クラス名の代わりにキーワードself
を使用できます。
オブジェクトインターフェース
オブジェクトインターフェースは、クラスの「契約」として機能します。クラスがオブジェクトインターフェースを実装する場合、そのインターフェースが定義するすべてのメソッドを含まなければなりません。これは、特定のクラスが同じ「契約」または構造に従うことを保証するための優れた方法です。
PHPでは、インターフェースはキーワードinterface
で定義されます。インターフェースで定義されたすべてのメソッドはパブリック(public
)です。クラスがインターフェースを実装する場合、キーワードimplements
を使用します。
クラスがインターフェースを実装しても、期待されるすべてのメソッドが定義されていない場合、PHPはエラーをスローします。
クラスは一度に複数のインターフェースを実装できます。これは、クラスが1つのクラスからしか継承できない継承とは異なります:
抽象クラス
抽象クラスは他のクラスの基本テンプレートとして機能しますが、直接インスタンスを作成することはできません。これらは、完全なメソッドと、内容が定義されていない抽象メソッドの組み合わせを含んでいます。抽象クラスから継承するクラスは、祖先のすべての抽象メソッドの定義を提供する必要があります。
抽象クラスを定義するには、キーワードabstract
を使用します。
この例では、1つの通常メソッドと1つの抽象メソッドを持つ抽象クラスがあります。次に、AbstractClass
から継承し、抽象メソッドの実装を提供するChild
クラスがあります。
インターフェースと抽象クラスは実際にはどのように異なりますか?抽象クラスは抽象メソッドと具象メソッドの両方を含むことができますが、インターフェースはクラスが実装しなければならないメソッドを定義するだけで、実装は提供しません。クラスは1つの抽象クラスからしか継承できませんが、任意の数のインターフェースを実装できます。
型チェック
プログラミングでは、扱っているデータが正しい型であることを確認することが非常に重要です。PHPには、これを保証するためのツールがあります。データが正しい型を持っているかどうかを確認することは、「型チェック」と呼ばれます。
PHPで遭遇する可能性のある型:
- 基本型:
int
(整数)、float
(浮動小数点数)、bool
(真偽値)、string
(文字列)、array
(配列)、null
が含まれます。 - クラス: 値が特定のクラスのインスタンスであることを要求する場合。
- インターフェース: クラスが実装しなければならないメソッドのセットを定義します。インターフェースを満たす値は、これらのメソッドを持っている必要があります。
- 混合型: 変数が複数の許可された型を持つことができるように指定できます。
- Void: この特殊な型は、関数またはメソッドが値を返さないことを示します。
型を含むようにコードを修正する方法を見てみましょう:
このようにして、コードが正しい型のデータを期待し、それを使用して動作することを保証し、潜在的なエラーを防ぐのに役立ちます。
PHPでは直接記述できない型もあります。その場合、phpDocコメントで指定されます。これは、/**
で始まり*/
で終わるPHPコードを文書化するための標準形式です。これにより、クラス、メソッドなどの説明を追加できます。また、いわゆるアノテーション@var
、@param
、@return
を使用して複雑な型を指定することもできます。これらの型は、静的コード解析ツールによって使用されますが、PHP自体はそれらをチェックしません。
比較と同一性
PHPでは、2つの方法でオブジェクトを比較できます:
- 値の比較
==
: オブジェクトが同じクラスであり、プロパティに同じ値を持っているかどうかを確認します。 - 同一性
===
: 同じオブジェクトインスタンスであるかどうかを確認します。
instanceof
演算子
instanceof
演算子を使用すると、特定のオブジェクトが特定のクラスのインスタンスであるか、そのクラスの子孫であるか、または特定のインターフェースを実装しているかどうかを確認できます。
Person
クラスと、Person
クラスの子孫である別のクラスStudent
があると想像してみましょう:
出力から、$student
オブジェクトは同時に両方のクラス(Student
とPerson
)のインスタンスと見なされることが明らかです。
Fluent Interface
「Fluent Interface」(英語では「Fluent Interface」)は、OOPのテクニックであり、1回の呼び出しでメソッドを連鎖させることができます。これにより、コードがしばしば簡略化され、明確になります。
Fluent
Interfaceの重要な要素は、チェーン内の各メソッドが現在のオブジェクトへの参照を返すことです。これは、メソッドの最後にreturn $this;
を使用することで実現します。このプログラミングスタイルは、オブジェクトのプロパティ値を設定する「セッター」と呼ばれるメソッドとよく関連付けられます。
電子メール送信の例でFluent Interfaceがどのように見えるかを示します:
この例では、メソッドsetFrom()
、setRecipient()
、setMessage()
は、対応する値(送信者、受信者、メッセージの内容)を設定するために使用されます。これらの各値を設定した後、メソッドは現在のオブジェクト($email
)を返し、これにより次のメソッドを連鎖させることができます。最後に、実際に電子メールを送信するメソッドsend()
を呼び出します。
Fluent Interfaceのおかげで、直感的で読みやすいコードを書くことができます。
clone
を使用したコピー
PHPでは、clone
演算子を使用してオブジェクトのコピーを作成できます。この方法で、同一の内容を持つ新しいインスタンスを取得します。
オブジェクトをコピーする際に一部のプロパティを変更する必要がある場合は、クラス内で特殊なメソッド__clone()
を定義できます。このメソッドは、オブジェクトがクローンされるときに自動的に呼び出されます。
この例では、1つのプロパティ$name
を持つSheep
クラスがあります。このクラスのインスタンスをクローンすると、__clone()
メソッドは、クローンされた羊の名前が「クローン」プレフィックスを取得するようにします。
トレイト
PHPのトレイトは、クラス間でメソッド、プロパティ、定数を共有し、コードの重複を防ぐことを可能にするツールです。これらは「コピー&ペースト」(Ctrl-CおよびCtrl-V)メカニズムのようなものと考えることができ、トレイトの内容がクラスに「挿入」されます。これにより、複雑なクラス階層を作成することなくコードを再利用できます。
PHPでトレイトを使用する方法の簡単な例を示しましょう:
この例では、1つのメソッドhonk()
を含むHonking
という名前のトレイトがあります。次に、Car
とTruck
の2つのクラスがあり、どちらもHonking
トレイトを使用しています。これにより、両方のクラスがhonk()
メソッドを「持ち」、両方のクラスのオブジェクトでそれを呼び出すことができます。
トレイトを使用すると、クラス間でコードを簡単かつ効率的に共有できます。同時に、それらは継承階層には入りません。つまり、$car instanceof Honking
はfalse
を返します。
例外
OOPの例外を使用すると、コード内のエラーや予期しない状況をエレガントに処理できます。これらは、エラーまたは異常な状況に関する情報を持つオブジェクトです。
PHPには、すべての例外の基礎として機能する組み込みクラスException
があります。これには、エラーメッセージ、エラーが発生したファイルと行など、例外に関する詳細情報を取得できるいくつかのメソッドがあります。
コードでエラーが発生した場合、キーワードthrow
を使用して例外を「スロー」できます。
関数divide()
が2番目の引数としてゼロを受け取ると、エラーメッセージ'ゼロ除算!'
を持つ例外をスローします。例外がスローされたときにプログラムがクラッシュするのを防ぐために、try/catch
ブロックでそれをキャッチします:
例外をスローする可能性のあるコードは、try
ブロックにラップされます。例外がスローされると、コードの実行はcatch
ブロックに移動し、そこで例外を処理できます(例:エラーメッセージを表示)。
try
およびcatch
ブロックの後には、オプションのfinally
ブロックを追加できます。これは、例外がスローされたかどうかに関係なく常に実行されます(try
またはcatch
ブロックでreturn
、break
、またはcontinue
ステートメントを使用した場合でも):
Exceptionクラスから継承する独自の例外クラス(階層)を作成することもできます。例として、入金と引き出しを実行できる簡単な銀行アプリケーションを想像してみましょう:
1つのtry
ブロックに対して、異なるタイプの例外を予期する場合は、複数のcatch
ブロックを指定できます。
この例では、catch
ブロックの順序に注意することが重要です。すべての例外はBankException
から継承するため、このブロックを最初に配置した場合、後続のcatch
ブロックにコードが到達することなく、すべての例外がここでキャッチされてしまいます。したがって、より具体的な例外(つまり、他の例外から継承するもの)を、親の例外よりもcatch
ブロックの順序で上に配置することが重要です。
イテレーション
PHPでは、配列を反復処理するのと同様に、foreach
ループを使用してオブジェクトを反復処理できます。これが機能するためには、オブジェクトは特別なインターフェースを実装する必要があります。
最初のオプションは、Iterator
インターフェースを実装することです。これには、現在の値を返すcurrent()
、キーを返すkey()
、次の値に移動するnext()
、先頭に移動するrewind()
、そしてまだ終端に達していないかどうかを確認するvalid()
メソッドがあります。
2番目のオプションは、IteratorAggregate
インターフェースを実装することです。これにはgetIterator()
という1つのメソッドしかありません。これは、反復処理を保証するプレースホルダーオブジェクトを返すか、またはジェネレータを表すことができます。ジェネレータは、キーと値を段階的に返すためにyield
を使用する特別な関数です:
ベストプラクティス
オブジェクト指向プログラミングの基本原則を理解したら、OOPのベストプラクティスに焦点を当てることが重要です。これらは、機能的であるだけでなく、読みやすく、理解しやすく、保守しやすいコードを書くのに役立ちます。
- 関心の分離 (Separation of Concerns): 各クラスは明確に定義された責任を持ち、1つの主要なタスクのみを解決する必要があります。クラスが多くのことを行いすぎる場合は、より小さく、特化したクラスに分割するのが適切かもしれません。
- カプセル化 (Encapsulation): データとメソッドは可能な限り隠蔽し、定義されたインターフェースを通じてのみアクセス可能であるべきです。これにより、コードの残りの部分に影響を与えることなく、クラスの内部実装を変更できます。
- 依存性の注入 (Dependency Injection): クラス内で直接依存関係を作成する代わりに、外部から「注入」する必要があります。この原則をより深く理解するために、Dependency Injectionに関する章をお勧めします。