[php: OOP on PHP] Object Oriented Programming on PHP. #php
ソフトウェアを効率的にプログラミングするための仕組み・考え方・概念。また、その概念をプログラミング上で実装できるようにした言語の機能のことを指す。
一言にいうと、変更に強くなる。
大前提として、「オブジェクト指向で書くこと」は一つの手段であり、全てのケースでベストプラクティスとは言い切れない。しかし「オブジェクト指向で書かれた "なにか" ( FW とかライブラリとか ) を使って機能実装する」機会はとても多いので まずは書いてあることが解る 状態にしておくべき。書いてあることが分かるようになったなら、次はそれを利用して自身の問題を解決できる。
// クラスの定義
class Human {
// クラスのプロパティの宣言
private $name;
private $age = 15;
// クラスのコンストラクタ (初期化処理) の定義
public function __construct($name='John') {
$this->name = $name;
}
// Public なメソッドの定義
public function sayHello($destName){
echo 'Hello, '.$destName.'.'.PHP_EOL;
echo 'My name is '.$this->name.'.'.PHP_EOL;
echo 'I\'m '.$this->age.' years old.'.PHP_EOL;
}
// Private なメソッドの定義
public function setAge($age) {
return $this->age = $age;
}
}
// クラスからオブジェクト (インスタンス) 生成
$HumanBob = new Human('Bob');
// オブジェクトのメソッドを実行
$HumanBob->setAge(29);
$HumanBob->sayHello('Charlie'); // Hello, Charlie. My name is Bob. I'm 29 years old.
// 親クラスを継承した子クラスの定義
class Child extends Human {}
PHP のデータ構造体 ( = Object ) の型定義には以下のような種類がある。
class hoge { クラス定義 }
class fuga extends hoge { hoge クラス継承の fuga クラス定義 }
// クラスのほかに、似たようなオブジェクト定義が色々ある
abstract foo { 抽象クラス定義 }
interface bar { インターフェイス定義 }
trait hoge { トレイト定義 }
クラス内変数、メンバー変数とも。
private $foo = 'value';
クラス内の関数。
public function bar($str) {
echo $str;
}
プロパティ・メソッドの「外部クラスからのアクセス制御」について制御するやつ。
修飾子 | アクセス制御 |
---|---|
private | 自クラス内のみ許可 |
protected | 自クラスと継承先のみ許可 |
public | 外部クラスからのアクセス許可 |
class hoge {
public $bar // 外部アクセス可
private $foo // 外部アクセス不可
protected $baz // 継承先でのみアクセス可
static $val // インスタンス化せずともアクセス可 & $this-> では呼べない
/**
* 上記 static $val は PHP の関数内等で利用される static $val = 0; などの「静的変数」扱いらしい
*
* Ref) 静的変数?
* - 関数・ブロック内で宣言しブロック終了後も値を保持する変数
* - 宣言時リテラル( [ static $val = 0; ] 等=計算式でない記述 )で初期化処理可能
* - 初期化処理移行呼び出されても初期化処理をスルーする仕様
*/
}
インスタンス ( オブジェクト ) のプロパティやメソッドにアクセスするやつ。連想配列で扱うファットアロー =>
とは別物。
$obj->property;
$obj->method('arg');
クラス定義を参照して new
演算子や clone
演算子で生成したオブジェクトのこと。
$obj1 = new hoge(); // new演算子必須
$obj2 = new hoge(); // id別インスタンス扱い
// オブジェクトのコピーで扱う clone
$obj3 = clone $obj1;
クラス内で、自クラスを参照するための組込み変数。
class Human {
public $name = 'John';
public function sayHello() {
echo 'Hello, my name is '.$this->name.'!!'; // 自身の $name プロパティを参照
}
}
$Human = new Human();
$Human->sayHello(); // Hello, my name is John!!
「::の左辺にあるメソッドやプロパティ名が右辺のスコープに属するよ」と明示する演算子で、下記の機能を有する。
記法 | スコープ | 使える場所 |
---|---|---|
self:: | 記述された自クラスを参照 | クラス定義の中でのみ使える |
parent:: | 記述された自クラスの親クラスを参照 | クラス定義の中で使える |
クラス名:: | 記述で明示したクラス名を参照 | クラス定義の外or中で使える |
static:: | 直近の非転送(self等でないクラス名を明示した)コールがあるクラス | クラス定義の中 |
PHP5.3より、冗長な命名規則を使って名前衝突を避けなくても良いように【名前空間】が実装された。(オブジェクトと関係ないけどオブジェクト指向を採用するような規模の開発で用いられるので一応...)
/*
例えば以下のようなディレクトリ構成で開発するとき、同名クラス・関数・変数を使わないように...なんてチーム開発ではほぼ不可能である
/app
/src
/controller
/model
/view
/vender
/controller
/model
/view
そんな時に各衝突を避けてため、各ディレクトリに倣った名前空間を仮想的に作る
で、全ソースコードの先頭で「このファイルで扱うクラス名・関数名・変数名は~~名前空間でのみ有効だよ」というのを宣言する
*/
// バックスラッシュ区切りで空間を区切る
namespace App\Src\Controller;
/*
また、別の名前空間にアクセスしたいときは use 文を使う。これを使うことで自ファイル・自名前空間内に同一名称のクラスや変数がないとき、 use 先の名前空間を探索するようになる
*/
namespace App\Src\Controller;
use Vendor\HogeHogeLibrary;
use Vendor\PiyoPiyoUtility;
public / protected なメソッドを通して private プロパティに値を入れるような処理やそのメソッドのこと。setProperty(){}
public / protected なメソッドを通して private なプロパティを取得するような処理やそのメソッドのこと。getProperty(){}
オブジェクト指向の基本的な設計思想 自クラス外部クラス問わず全てのプロパティへのアクセスで上記アクセサーを経由することでバリデーションや入出力の処理を一元化でき、結果的にオブジェクト間のデータのやりとりを疎結合な状態にするもの。
インスタンス化された時自動的に走るクラスの初期化処理。エラー時はエラーをreturn出来ないのでtrycatchで例外を投げたりする。
public function __construct() {
echo "run constructor\n";
}
インスタンスされたオブジェクトにnullを入れたり、変数スコープが外れたり、プログラムが終了した時に自動的に走るクラスの終了処理。リソース解放とかに使う。
public function __destruct() {
echo "run destructor\n";
}
不変的オブジェクト。セッターを用意せず、コンストラクタのみ値の代入を許可する設計のクラス。インスタンス時に値が入り以降は代入不可能なため、中身が明示的。
// 継承先hogehogeには継承元hogeのプロパティ・メソッドが全て引き継がれる。
// 継承の重なりをレイヤー・レイヤリングなどと呼ぶ。
class hogehoge extends hoge {}
継承先で継承元と同名のプロパティ・メソッドを宣言した時、継承先が優先され継承元を上書きすることになる。
継承元プロパティ・変数が...
言語によっては、クラス継承先で継承元のコンストラクタとデストラクタは自動的に呼び出されるが、PHPでは自動的に呼び出されない。
また、継承元のコンストラクタと継承先のコンストラクタを2重で実行したい時などオーバーライドされ継承元のコンストラクタが呼ばれなくなるという問題がある。
上記のような理由から、継承先で継承元のコンストラクタ・デストラクタ処理を呼び出すために、以下の記法で親クラスメソッドの実行をかけてあげる必要がある。
// 親クラスメソッドの呼び出し
parent::メソッド名();
class hogehoge extends hoge{
public function __construct() {
parent::__construct(); // 継承元クラスのコンストラクターを呼ぶ・処理の最初に書くこと
echo "run hogehoge constructor\n";
}
public function __destruct() {
echo "run hogehoge destructor\n";
parent::__destruct(); // 継承元クラスのデストラクターを呼ぶ・処理の最後に書くこと
}
}
これで継承元クラスhogeに実装したコンストラクタ・デストラクタと、継承先のコンストラクタ・デストラクタの両方が実行される。この [ parent::method() ] はコンストラクタに限らずどのメソッドでも使える。
Fatalエラーしたり、致命的なバグを生みそうなクラスへのアクセスに対する防御(例外投げて処理をエラー停止させないようにとか)手段で用いる。PHPにはコンパイルエラーがなく、動的型付けなので予期せぬアクセスに弱い。それを補うための実装をする時に使う。
具体的には、コンストラクタ・デストラクタなどの__から始まる「PHPが用意した、特定タイミングで自動的に呼び出される」メソッドのことを指す。
__set(){ }
__get(){ }
↑はアクセス不能なプロパティの値を取る・値を入れるような動作をする時に暗黙的に実行されているメソッド。このマジックメソッドに(実行されたら即例外を投げる)みたいな処理を書けば、クラスのprivateプロパティを守ったり、存在しないプロパティへアクセスして(PHPの仕様通りに)新たに変数を生成したりせずに済む。
__call(){ }
__callStatic(){ }
__invoke() { }
↑はアクセス不能なクラスメソッドへのアクセスタイミングで呼ばれる。上記のメソッド版。インスタンスからの呼び出し( $obj->method(); )では _callが、外部からの静的呼び出し ( class::method(); )では _callStaticが呼ばれる。例外投げ( throw new Exception('エラー') )以外にも開発序盤のテスト用疑似アクセサーに使ったりもするらしい。
_invokeは無名関数を実行する際に暗黙的に呼ばれるマジックメソッド。インスタンス生成→インスタンスを無名関数的に実行しようとした時、エラーが出たら例外投げる的な使い方をする。
class hoge {
public function __invoke() {
echo "call method\n";
}
}
$obj = new hoge();
$obj();
この他にインスタンスを文字列のように扱おうとした時に呼ばれる __toString()
もある。
クラスより抽象度の高い「持つべきメソッドの宣言だけをする」構造体定義。
publicメソッドの宣言のみ記述でき、継承先のクラスに対して「このような機能を実装するべきである」ことを強く明示すると共に「継承先ではこれらメソッドが存在することを保証します」と明示する役割がある。
メソッドの実装はそのインターフェースの実装版(implement)として子クラスで定義する。PHPでは単一継承(子クラスは複数の親クラスを持てない)だが、インターフェースは多重継承可能(宣言だけをしているのでメソッドなどの名前衝突エラーが起きづらい為)
[ abstract class ~~ {} ]で定義する、クラス基本形の他に抽象メソッドを所有できる抽象度の高い上位レイヤクラス。継承は通常のクラスと同様extendsで行う。
上記抽象クラス内において、publicかprotectedで宣言し、「継承先ではこのようなメソッドを必ず実装せよ」という暗示と同時に、上位レイヤで実装できる箇所は実装し継承させられるメソッド。
PythonやCのように多重継承ができない言語(JavaやPHPやRuby)では、上記2つの上位レイヤクラスを使い、以下のようにクラスのレイヤー設計(レイヤリングアーキテクチャ)を行う。※C等の多重継承は便利な反面複雑になり易い、らしい。
// 参考
// 再生機能の基本コントローラー(インターフェース)をおさえ、抽象的な継承先でCDコンポ、mp3プレーヤーみたいなものをつくりたい。
interface cotrolPanel {
/*継承先のメソッドの所有を明示・保証*/
public function play();
public function stop();
public function next();
public function back();
}
interface display {
/*継承先のメソッドの所有を明示・保証*/
public function outDisplay();
}
abstract class player {
/*継承先で扱う抽象概念を明示*/
adstract protected function readFormat(){
/*実装はしないor途中まで記述*/
}
adstract protected function outSpeaker(){
/*実装はしないor途中まで記述*/
}
}
class mp3player extends player implements controlPanel, display {
protected function readFormat() { ・・・}
protected function outSpeaker() { ・・・}
public function play() {・・・}
public function stop() {・・・}
public function next() {・・・}
public function back() {・・・}
public function outDisplay() { ・・・}
}
PHP 5.4 から実装された機能。
処理する概念が [ A is a B ] であれば通常のクラス継承で十分だが [ A has a B ] の時は難しいケースがある。その際にBを子クラスではなくモジュール ( 汎用的かつ交換可能な部品 ) としてまとめられる構造体定義のこと。メソッド・プロパティを有する(再利用が前提なので殆どpublic)。継承(extends)はできず、使用する時はクラス内で [ use trait_name; ] を宣言する。
trait fuga {
public function seyHi() {
echo "Hi!!\n";
}
public $var;
}
class hoge{
use fuga; // trait 使用宣言
}
$obj = new hoge();
$obj->seiHi();
$obj->$var = 10;
生成したインスタンスは通常、プログラムの処理終了と同時にメモリ解放されて消えてしまう。しかし「敵キャラの残りHP」や「お買い物カゴ」など、ある特定のタイミングまで持ちまわりたいインスタンスがある。PHPではそういった場合に、インスタンスを一旦文字列化して他処理に渡したり、DB格納したりしている。
$obj = new hoge();
$str = serialize($obj); // serializeで文字列化
$obj2 = unserialize($str); // unserializeで戻す
通常はシリアライズできないもの(リソース)や、逆にシリアライズが不要なものをシリアライズの対象外としてあらかじめ指定するために_sleep()を定義することがある。(シリアライズ時に自動で呼ばれるマジックメソッド)
sleep()と対をなす、unserialize()が実行されるタイミングで呼ばれるマジックメソッドwakeup()もある。
配列などの実体データと違い、生成されたインスタンスが変数に格納されたとき、この変数の中身はインスタンスそのものではなく「インスタンスを参照する値」が入っていることに注意する。
// ex) 浅いコピー(インスタンス参照値をコピーしている)
class foo {
public $value;
}
$obj = new foo();
$obj->i = 1;
$obj2 = $obj;
$obj2->i = 999;
var_dump($obj);
var_dump($obj2);
既にインスタンスを生成し、中身をいじったりして初期状態ではない状態で、"インスタンスをコピーしたい"と考えたとき、変数の代入では参照の値が渡されてしまいこれを実現できない。この時使うのが clone 書式。
class hoge {
public $i;
}
$obj = new hoge();
$obj->i = 1;
$obj2 = clone $obj; // 代入ではなくcloneを使う
$obj2->i_ = 999; // 片方だけ値を変更
var_dump($obj); // 保持する $i は 1
var_dump($obj2); // 保持する $i は 999
インスタンス自身がインスタンスを保持しているとき、cloneしても保持しているインスタンスは「参照渡し」の状態になってしまう。これを浅いコピーと呼ぶ。 cloneをした時に自動的に呼ばれるマジックメソッド__clone()を使ってこれを解決する。
class hoge {
public function __clone() {
$this->obj = clone $this->obj;
// プロパティのうち「インスタンス型のもの」は全てcloneしておく
}
}
自身でクラス設計をするような場面では、オブジェクト指向だけでなく「その使用言語ならではの特性」を理解しておく必要がある。
基本はクラス継承だが、その根幹には機能やデータ概念の抽象化により、具体的な実装コードによるオブジェクト同士の汚染を防ぐという大前提があることを忘れない。
通常のプログラミングにも求められるが、オブジェクト指向プログラミングでは特に一つ一つの機能の「モジュール化」を意識している。「モジュール化」を行うときには、以下 3 点に気を付ける。
モジュール同士が結合していない。片方のモジュールの修正に、もう片方が引っ張られたりしない。
モジュールは、適切な文脈で機能が凝集され、また不適切な文脈では機能を凝集しない。具体的には、「似たような処理だから」という理由で似たような処理が同じクラスに同席させたりしない。「あるデータの取り扱い」といった文脈で機能を凝集すべき。
インスタンスの生成・探索・実行など様々な過程で、インプットとアウトプットが1つずつで副作用がなく、状態が明示的なモジュールが望ましい。
あるまとまったデータ・処理の塊をクラスとして分けるときに、そのクラスを「静的なクラス」として扱うか、実際にインスタンス化してオブジェクトクラスとして扱うか悩むときがある。
基本的に「状態を持つか」「システムの中で複数存在するエンティティか」が分かれ目になる。まず、大前提として「動的クラス・インスタンスメソッドを使う」ところを考え、場合によってはその他を検討する。
グローバル変数・関数のまとまりのようなクラスなら、おそらくほとんど変更されない初期プロパティのまま様々な箇所でStaticに呼び出される「静的クラス」になるだろうし、反対に「インスタンス化したオブジェクト毎に状態を持つ」ようなまとまりは、やはり動的なクラスにしたほうが良い。
さっぱり状態を持たない「メソッドの部品」ならおそらくトレイトになるだろうし、継承先クラスに制限をかけたりする場合はインターフェースや抽象クラスを使うべきである。
再利用の単位とリリースの単位は等価になる。
パッケージに含まれるクラスは、すべて一緒に再利用される。つまり、パッケージに含まれるいずれかのクラスを再利用するということは、その他のクラスもすべて再利用することを意味する。
パッケージに含まれるクラスは、みな同じ種類の変更に対して閉じているべきである。パッケージに影響する変更はパッケージ内のすべてのクラスに影響を及ぼすが、他のパッケージには影響しない。
パッケージ依存グラフに循環を持ち込んではならない。
安定する方向に依存せよ。
パッケージの抽象度と安定度は同程度でなければならない。
ソフトウェア開発におけるデザインパターン(型紙(かたがみ)または設計パターン、英: design pattern)とは、過去のソフトウェア設計者が発見し編み出した設計ノウハウを蓄積し、名前をつけ、再利用しやすいように特定の規約に従ってカタログ化したものである。 ( wikipedia )
一つの複雑なシステムを開発する時、システムをお互いに影響がない(出来るだけ少ない)ような独立した個別部品に分割して、それぞれ実装していくという手法。部品と部品の間のインタフェースを固め、各部品ができるだけ単一の機能を実現し、お互いに内部の実装を気にしない(関心しない)こと。
上記のような「モジュール間がお互いに分離されて」おり、いくつもの機能からなる複雑なシステムでは、モジュール間のやりとりを「規約」「暗黙的なルール」で縛る・統一するのが基本。「設定」は要素が増えるごとに増えていきやがて初期JavaのStrutsのように設定XMLのボリュームが人間に処理できる範囲を超えてしまう。規約であれば(しかもそれがモジュール間のやりとりにフォーカスしたものであれば)基本的に増えつづけるようなことはない。RubyOnRailsが提唱し、現在のシステム設計の基礎の考え方になっている。
( 依存性の注入 ) とはいったものの、依存性を注入ってなんやねん。簡単に言うと「ある機能を提供するオブジェクト(サービス)」を「その機能を利用するオブジェクト(クライアント)」に渡して、依存処理部分の実装をクライアント側(サーバとクライアントのクライアントじゃないよ、サービスの利用者・オブジェクトの意味)に注入することを指す。
// DIパターンなやつ
class Client{
private $service;
public function __construct(Service $service) {
$this->service = $service;
}
public function doSomething() {
$this->service->doSomething();
}
...
class Service{
public function doSomething(){
...
}
// -----------------------------
// DIぱたーんぢゃないやつ
class Client{
private $service;
public function __construct() {
$this->service = new service();
}
public function doSomething() {
$this->service->doSomething();
}
...
class Service{
public function doSomething(){
...
}
上記例では、オブジェクトの生成と使用が分離されている 。new時に引数はいるのか?とかそういった部分が全部隠匿されてる。クライアントが自主的にサービスを呼ぶのではなく、サービスがクライアントに(自動的に)注入される仕組み。つまり、制御が反転している。
MVCフレームワークが提供する「処理・制御の反転」の、より狭義な、より実装よりな、より実際的な部分の設計パターンのこと。これにより開発者は「呼ばれる処理を書く」ことに専念でき、オブジェクト間をより疎結合に保てる。
ちなみにこのDIパターンを楽に実装するために、オブジェクトの生成設定をまとめたオブジェクトを提供する「らくらくDIパターン設定ライブラリ」がある、これが俗にいうDIコンテナ。
これも補足で、DIコンテナの誤った利用方法でオブジェクトの依存先を「元オブジェクト」「コンテナオブジェクト」の2つにしてしまうサービスロケータというアンチパターンもある。
コンストラクタを外部からいじいじされたくないよね?ってことだよね。
class Robot {
private $name = '';
private $color;
public static function createRedRobot($name) {
return new self($name, 'red');
}
public static function createBlueRobot($name) {
return new self($name, 'blue');
}
private function __construct($name, $color) {
$this->setName($name);
$this->color = $color;
}
public function setName($name) {
$this->name = (string)filter_var($name);
}
public function getName() {
return $this->name;
}
public function getColor() {
return $this->color;
}
}
// 普通に呼ぶ
$a = Robot::createRedRobot('ロボ太郎');
echo $a->getColor(); // red
// コンストラクタをいじって private な $color いじってやるぜ...
$a->__construct('ロボ太郎', 'blue'); // エラーではじかれる
echo $a->getColor(); // red
// これでオブジェクトの不変性を保てる