OOP Workshop PHP5:言語でミックスインをエミュレートする

ある晩、私の自転車フレームワークでORMの動作を実装するために、Rubyのmixinのように、またはC#の拡張メソッドとして(またはPHPの将来のバージョンではtrait / graftとして)動作するものが必要でしたPHPで不純物を実装する方法を楽しみに楽しみました。 不純物が何であるかわからない場合は、問題ではありません。すべてを説明します。

PHPでの不純物の実装と、それらを実装できる小さなライブラリのプログラミングについての議論で私をフォローしてください。 この記事は、PHPの初心者および中級の開発者を対象としています(主なことは、OOPに精通していることです)。 その過程で、クラスを使用してPHP 5.3を操作する際の複雑さについても少し間違えますが、しばらくしてから指摘し、修正することを提案します。 また、あなたの批判に対する私の解決策を提供します。 良い読書をしてください。



不純物とは何ですか?


混合物は、メソッドとプロパティを他のクラスに単に公開するクラスです。 クラスに他のクラスを混在させることは、多重継承をエミュレートするための単なるオプションであり、PHPでは実装されていないと想定できます。 わかりやすくするために、構文がPHPに似た擬似コードを使用した小さな例を示します。
 <?php
 mixin Timeable {
     private $ timeStarted;
    プライベート$ timeStopped;
     public function start(){$ timeStarted = time();  }
     public function stop(){$ timeStopped = time();  }
     public function getElapsed(){return $ timeStopped-$ timeStarted}
 }

 mixin Dumpable {
     public function dump(){var_dump($ this);  }
 }

 Timeable、Dumpableを混合したMyClassクラス{
    パブリック関数hello(){}
 }

 $ a = new MyClass();
 $ a-> start();
睡眠(250);
 $ a-> stop();
 echo $ a-> getElapsed();
 $ a-> dump();
 ?>


アイデアは明確ですか? 不純物は、クラスがそれらすべてからすぐに継承されたかのように、単に機能をクラスに追加します。 ただし、それらは混合されているクラスのメンバーを操作できます。 これは、PHPで実装する機能の一種です。
自分でタスクを設定しましょう。


レジストリを設計しています。


考え直してみましょう。 システムの異なるクラスに異なる不純物を追加したい場合があります。 つまり、どこに、どのクラスの不純物が混入しているかに関する情報を保存する必要があります。 プロジェクトのこのような情報はグローバルであり、どこからでもアクセスできる必要があります。 したがって、そのようなストレージを実装するために、静的クラスを選択しました(PHPにはC#のように静的クラスはありません。静的クラスとは、インスタンス化する必要のないクラスを意味します。すべての機能は、クラス)。 小さな作業として、シングルトンを使用する必要がないように、レジストリを再設計することをお勧めします(興味がある場合は、記事を最後まで読んだ後)。

上記から、レジストリはアグリゲータークラスの不純物を登録できるはずです。 そしてもう少し高く、特定のクラスに不純を登録する場合、この不純の機能はすべての子孫クラスに混合する必要があると言いました。 登録時にすぐに祖先クラスのリストを取得することはできません(クラスのロードを避ける必要があり、クラス階層の検査にはこれが必要です)。 このことから、後で必要なときに対応関係のリスト(クラス=>不純物のリスト)を作成します。 さらに、アグリゲータークラスの新しいインスタンスを作成するときにリストが再構築されないように、このようなリストをキャッシュする必要があります。

class Registry { private static $registeredMixins = array(); public static function register($className, $mixinClassName) { $mixinClassNames = func_get_args(); unset($mixinClassNames[0]); foreach ($mixinClassNames as $mixinClassName) { self::$registeredMixins[$className][] = $mixinClassName; } self::$classNameToMixinCache = array(); } } 

登録機能は非常にシンプルであることが判明しました。 アグリゲータークラスの名前と不純物のリストを彼女に与えます。 便宜上、不純物のリストはコンマで示すことができます。 Func_get_args()がこれを処理します(必要に応じて、不純物のリストを配列として指定するための適切なサポートを追加します)。 次に、このクラスの不純物のリストに各不純物を追加します。 また、関数の最後の最後の呼び出しはキャッシュをクリアします。これは、特定のクラスの不純性を登録すると、そのすべての子孫にもキャッシュが追加され、キャッシュの再構築が必要になるためです。

キャッシング関数を書きましょう。 クラスとそれらに登録された不純物のリストを調べ、同じ不純物のリストで指定されたすべての子孫クラスを追加する必要があります。 結果はキャッシュです。
キャッシング関数には、このクラスの祖先のリストを受け取る関数が必要です。
  private static $classNameToMixinCache = array(); private static function getAncestors($className) { $classes = array($className); while (($className = get_parent_class($className)) !== false) { $classes[] = $className; } return $classes; } private static function precacheMixinListForClass($className) { if (isset(self::$classNameToMixinCache[$className])) { return; } $ancestors = self::getAncestors($className); $result = array(); foreach ($ancestors as $ancestor) { if (isset(self::$registeredMixins[$ancestor])) { $result = array_merge($result, self::$registeredMixins[$ancestor]); } } self::$classNameToMixinCache[$className] = array_unique($result); } 


この関数は、指定されたクラスの不純物のリストのみをキャッシュに追加することに注意してください。 キャッシュの内容のほとんどが必要になることはないため、キャッシュ全体をすぐには作成しません。 キャッシュを構築する前に、すでにこれを行っているかどうかを確認しました。

ここで、特定のクラスの不純物のリストを取得する必要がある場合、この関数を使用できます。
  public static function getMixinsFor($className) { self::precacheMixinListForClass($className); return self::$classNameToMixinCache[$className]; } 

次のステップに進みます。 私たちがアグリゲータークラスメソッドを呼び出し、そのアグリゲータークラスメソッドではなく、いくつかの不純物で定義されていると想像してください。 何をする必要がありますか? このクラスの不純物のリストを取得し、それらを調べて、必要なメソッドがそれらのいずれかで定義されているかどうかを確認する必要があります。
混合はクラスの本質であるため、次のようにします。

  private static $methodLookupCache = array(); public static function getMixinNameByMethodName($className, $methodName) { if (isset(self::$methodLookupCache[$className][$methodName])) { return self::$methodLookupCache[$className][$methodName]; } self::precacheMixinListForClass($className); foreach (self::$classNameToMixinCache[$className] as $mixin) { if (method_exists($mixin, $methodName)) { self::$methodLookupCache[$className][$methodName] = $mixin; return $mixin; } } throw new MemberNotFoundException("$className has no mixed method $methodName()!"); } 


つまり、キャッシュにこのクラスとメソッド名のエントリが既にある場合、単純にそれらを返します。 そうでない場合は、キャッシュからこのクラスの不純物のリストを取得し、それらをバイパスして、実装が必要なメソッドがあるかどうかを確認します。 その場合、キャッシュに追加し、不純物の名前を返します。 何も見つからない場合、例外をスローします。
プロパティに対してまったく同じオプションが取得されます。 自分で書くことをお勧めします。
以上です。 レジストリを実装しました。 不純物クラスのプログラミングに進みます。

混合物をプログラムします。


だから、混合物。 どんな不純物? 混合物は普通のクラスです。 彼は別のクラスのフィールドを操作する方法を知っています。 そして、この他のクラスのインスタンスは、コンストラクターで渡すことが論理的です。
 class Base { protected $_owningClassInstance; protected $_owningClassName; public function __construct($owningClassInstance) { $this->_owningClassInstance = $owningClassInstance; $this->_owningClassName = get_class($owningClassInstance); } } 


私のプロジェクトでは、Mixins名前空間に属しているため、より具体的に名前を付ける必要はないため、ベースの不純クラスをBaseと名付けました。 ただし、好きな名前を付けることができます。

変数owningClassInstanceを介して、パブリックフィールドとメソッドを直接操作できます。 しかし、隠され保護された状態では、リフレクションを介して作業する必要があります。 複雑なことは何もありません。 関数のすべての定義を示します。
  protected $_owningPropertyReflectionCache; protected $_owningMethodReflectionCache; protected function getProtected($name) { if (! isset($this->_owningPropertyReflectionCache[$name])) { $property = new \ReflectionProperty($this->_owningClassName, $name); $property->setAccessible(true); $this->_owningPropertyReflectionCache[$name] = $property; } return $this->_owningPropertyReflectionCache[$name]->getValue($this->_owningClassInstance); } protected function setProtected($name, $value) { if (! isset($this->_owningPropertyReflectionCache[$name])) { $property = new \ReflectionProperty($this->_owningClassName, $name); $property->setAccessible(true); $this->_owningPropertyReflectionCache[$name] = $property; } $this->_owningPropertyReflectionCache[$name]->setValue($this->_owningClassInstance, $value); } protected function invokeProtected($name, $parameters) { $method = new \ReflectionMethod($this->_owningClassName, $name); $method->setAccessible(true); $parameters = func_get_args(); unset($parameters[0]); $method->invokeArgs($this->_owningClassInstance, $parameters); } 


ここでは、リフレクションのためにシステムクラスのインスタンスを作成して常に構成しないように、キャッシュを再び有効にしていることに注意してください。 メモリ消費を削減するために、必要に応じてキャッシュを破棄できます。
レジストリクラスで使用したmethod_exists()およびproperty_exists()関数は、パブリック名とともに指定された名前の隠され保護された関数を持っている不純物をチェックすることに既に気づいているかもしれません。 これにより、アグリゲータークラスは、非表示または保護として定義されている場合、同じ名前の関数を「試行」できるようになります。 その結果、まだエラーが発生しますが、明示的にそれを行うことを好みます。

  public function __call($name, array $arguments) { throw new MemberNotFoundException( "Method $name is not defined or is not accessible in mixin \"" . get_class() . "\""); } public function __get($name) { throw new MemberNotFoundException( "Property $name is not defined or is not accessible in mixin \"" . get_class() . "\""); } public function __set($name, $value) { throw new MemberNotFoundException( "Property $name is not defined or is not accessible in mixin \"" . get_class() . "\""); } 


小さなタスクとして、レジストリクラスのこのような不適切な動作を修正してみてください。 さらに、以前は別の名前で非表示または保護されていた名前でパブリックな不純物メソッドを呼び出すことができなくなります。

あの 以上です。 混合物はすぐに食べられます。 最後のステップ-不純物を混合するためのプラットフォームの実装-アグリゲータークラス。 これが私たちが今やることです。

アグリゲータークラスを作成します。


アグリゲータークラスは何ができますか? 彼は、不純物のクラスのインスタンスを保存し、それらのメソッドを呼び出す方法を知っています。 さて、プロパティを参照してください。 この動作は、PHPの「マジック」メソッドを使用して実装します。

 class Aggregator { protected $_mixins; protected $_className; public function __construct($aggregatorClassInstance = false) { $this->_className = $aggregatorClassInstance ? get_class($aggregatorClassInstance) : get_class($this); $mixinNames = Registry::getMixinsFor($this->_className); foreach ($mixinNames as $mixinName) { $this->_mixins[$mixinName] = new $mixinName($aggregatorClassInstance ? $aggregatorClassInstance : $this); } } } 


コンストラクターコードでは、クラスの不純物のリストを取得し、それらをループで処理してインスタンスを作成します。
変数$ aggregatorClassInstanceは、Aggregatorクラスからクラスを継承する必要がないことを保証するのに役立ちます。 Aggregatorクラスを別のクラスに含め、この他のクラスのインスタンスに等しい$ aggregatorClassInstanceパラメーターを使用してコンストラクターを呼び出すことができます。 したがって、そうすることで、この所有者クラスの不純物のリストを取得し、アグリゲータークラスの対応するインスタンスを不純物に渡します。

上記の説明が複雑すぎると思われる場合は、問題ではありません。 少し低く滑って、例があります。 継承の例がコンポジションの例とどのように異なり、どのように機能するかをご覧ください。

「魔法の方法」を実現します。

  public function __call($name, array $arguments) { return call_user_func_array(array($this->_mixins[Registry::getMixinNameByMethodName($this->_className, $name)], $name), $arguments); } public function __get($name) { return $this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name; } public function __set($name, $value) { $this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name = $value; } public function __isset($name) { return isset($this->_mixins[Registry::getMixinNameByPropertyName($this->_className, $name)]->$name); } 


各マジックメソッドは、情報を取得するためにレジストリにアクセスします。 すべてがシンプルです。

使用した例外クラスは次のようになります。

 class MemberNotFoundException extends \Exception {} 


いくつかの例を見てみましょう。



最初に、継承を使用した従来のスキームの場合:

 class MixinAggregatorSample extends Mixins\Aggregator { } class MixinHello extends Mixins\Base { protected $inaccessible; public $text = "I am a text!\r\n"; public function hello() { echo ("Hello from mixin!\r\n"); } } Mixins\Registry::register("MixinAggregatorSample", "MixinHello"); $a = new MixinAggregatorSample(); $a->hello(); //Accesing mixed methid echo ($a->text); //Accessing mixed property $a->text = "I am also a text!\r\n"; //Setting mixed property //$a->inaccessible = 'Error here'; //Throws exception //$a->inaccessible2 = 'Error here'; //Throws yet another exception (Homework: explain, why) echo ($a->text); var_dump(isset($a->text)); 


包含回路を見てみましょう。

 class MixinAggregatorSample { protected $_aggregator; public function __construct() { $this->_aggregator = new Mixins\Aggregator($this); } public function __call($name, $arguments) { return $this->_aggregator->__call($name, $arguments); } } class MixinHello extends Mixins\Base { public function hello() { echo ("Hellp from mixin!"); } } Mixins\Registry::register("MixinAggregatorSample", "MixinHello"); $a = new MixinAggregatorSample(); $a->hello(); 


違いがわかりますか? 含める場合、機能を失うことなく、アグリゲータークラスを他のクラスから自由に継承できます。 もちろん、通常の使用では、__ call()だけでなく、すべてのマジックメソッドを実装する必要があります。

性能



結果のライブラリの速度を測定しました。 測定は非常に近似しており、オープンIDE、Winamp、および想定されるすべての機能を備えたホームコンピューターで実行されます。

ネイティブ時間:0.57831501960754
時間による名前:1.5227220058441
混合時間:7.5425450801849
時間の反映:12.221807956696




このようなアプローチを大規模なプロジェクトで使用できるように、上記の数値は十分に受け入れられると思います。 原則として、不純なメソッドはスクリプトで何千回も呼び出されず、メソッドを呼び出すのに0.7マイクロ秒に対して10マイクロ秒はネイティブメソッドには非常に受け入れられるオプションです。 特に、大量のテキストやデータベースへのクエリの実行など、htmlspecialchars()に費やされる時間が非常に長いと考えると特にそうです。

ほとんどすべての場所で、ハッシュされたPHP配列に基づいたキャッシュを使用しているため、不純物やアグリゲータークラスの数が増えても、パフォーマンスはそれほど低下しません。 しかし、誰かが必要なテストを行う場合、私は非常に幸せになります。

エピローグ


この記事に関するあなたの批判を聞いてうれしいです。 私はこの資料をすべての読者が理解できるようにしたかどうかに特に興味があります。

もちろん、この記事は完全、正確、またはエラーのないことを目的とするものではありません。 あなたが私を修正してくれたら本当に感謝しています。 ここで結果のライブラリのコードを広げました。 もちろん、プロジェクトはトレーニングプロジェクトであり、実際のプロジェクトで使用する前に、慎重に考えてすべてをテストする価値があります。 同じクラス内の同じ名前のパブリックメソッドを持ついくつかの不純物など、いくつかの問題のある問題が存在します。

PHPの不純物のトピックに興味がある場合は、グーグルもお勧めします。

Source: https://habr.com/ru/post/J107682/


All Articles