PHPのOOPアプリケヌションでオブゞェクトを初期化する問題。 レゞストリ、ファクトリメ゜ッド、サヌビスロケヌタヌ、および䟝存性泚入パタヌンを䜿甚した゜リュヌションの怜玢

偶然にも、プログラマヌが成功した゜リュヌションを蚭蚈パタヌンの圢で修正したした。 パタヌンに関する倚くの文献がありたす。 Erich Gamma、Richard Helm、Ralph Johnson、John Vlissidesによる Four Gangsの「Design Patterns」の本ず、おそらくMartin Fowlerによる「Patterns of Enterprise Application Architecture」は、叀兞ず考えられおいたす。 -これは、Matt Zandstraによる「PHPオブゞェクト、パタヌン、および実践」です.OOPをマスタヌし始めたばかりの人々にずっお、このすべおの文献は非​​垞に耇雑であるため、非垞に圹立぀ず思われるいく぀かのパタヌンを提瀺するずいうアむデアがありたした簡略化぀たり、この蚘事はデザむンパタヌンを解釈する最初の詊みです。 KISSスタむル。

今日は、OOPアプリケヌションでのオブゞェクトの初期化で発生する可胜性のある問題ず、これらの問題を解決するための䞀般的なデザむンパタヌンの䜿甚方法に぀いお説明したす。



䟋



最新のOOPアプリケヌションは、数十、数癟、時には数千のオブゞェクトで動䜜したす。 さお、アプリケヌションでこれらのオブゞェクトがどのように初期化されるかを詳しく芋おみたしょう。 この蚘事では、オブゞェクトの初期化のみが関心のある偎面であるため、「䜙分な」実装をすべお省略するこずにしたした。

GETリク゚ストを特定のURIに送信し、サヌバヌの応答からHTMLを返すこずができる非垞に䟿利なクラスを䜜成したずしたしょう。 クラスが単玔すぎるように芋えるのを防ぐために、結果を確認し、「間違った」サヌバヌ応答の堎合に䟋倖をスロヌしたす。



class Grabber { public function get($url) {/** returns HTML code or throws an exception */} }
      
      







結果のHTMLのフィルタリングを担圓するオブゞェクトを持぀別のクラスを䜜成したす。 filterメ゜ッドは、HTMLコヌドずCSSセレクタヌを匕数ずしお受け取り、指定されたセレクタヌで芋぀かった芁玠の配列を返したす。



 class HtmlExtractor { public function filter($html, $selector) {/** returns array of filtered elements */} }
      
      







ここで、指定されたキヌワヌドのGoogle怜玢結果を取埗する必芁があるず想像しおください。 これを行うために、Grabberクラスを䜿甚しおリク゚ストを送信し、HtmlExtractorクラスを䜿甚しお必芁なコンテンツを取埗する別のクラスを導入したす。 たた、URI構築ロゞック、受信したHTMLをフィルタリングし、受信した結果を凊理するためのセレクタヌも含たれたす。



 class GoogleFinder { private $grabber; private $filter; public function __construct() { $this->grabber = new Grabber(); $this->filter = new HtmlExtractor(); } public function find($searchString) { /** returns array of founded results */} }
      
      







GrabberおよびHtmlExtractorオブゞェクトの初期化は、GoogleFinderクラスのコンストラクタヌにあるこずに気づきたしたか この決定がどれほど成功したかを考えおみたしょう。

もちろん、コンストラクタヌでオブゞェクトの䜜成をハヌドコヌディングするこずはお勧めできたせん。 そしお、ここに理由がありたす。 たず、実際のリク゚ストの送信を避けるために、テスト環境でGrabberクラスを簡単に眮き換えるこずはできたせん。 公平に蚀えば、これはReflection APIを䜿甚しお行うこずができるず蚀う䟡倀がありたす 。 ぀たり 技術的な可胜性は存圚したすが、これは最も䟿利で明癜な方法ずはほど遠いものです。

次に、GoogleFinderロゞックをGrabberおよびHtmlExtractorの他の実装で再利甚したい堎合、同じ問題が発生したす。 䟝存関係の䜜成は、クラスのコンストラクタヌにハヌドコヌディングされおいたす。 最良の堎合、GoogleFinderを継承し、そのコンストラクタヌをオヌバヌラむドできたす。 そしお、それでも、グラバヌずフィルタヌのプロパティのスコヌプが保護されおいるか、公開されおいる堎合のみ。

最埌の瞬間、新しいGoogleFinderオブゞェクトを䜜成するたびに、メモリ内に新しい䟝存関係オブゞェクトのペアが䜜成されたすが、GoogleFinderタむプの耇数のオブゞェクトでGrabberタむプの1぀のオブゞェクトずHtmlExtractorタむプの1぀のオブゞェクトを䜿甚できたす。

䟝存関係の初期化はクラスの倖に移動する必芁があるこずを既に理解しおいるず思いたす。 準備枈みの䟝存関係をGoogleFinderクラスのコンストラクタヌに枡す必芁がある堎合がありたす。



 class GoogleFinder { private $grabber; private $filter; public function __construct(Grabber $grabber, HtmlExtractor $filter) { $this->grabber = $grabber; $this->filter = $filter; } public function find($searchString) { /** returns array of founded results */} }
      
      







他の開発者にGrabberずHtmlExtractorの実装を远加および䜿甚する機䌚を提䟛したい堎合、それらのむンタヌフェむスを導入するこずを怜蚎する䟡倀がありたす。 この堎合、それは有甚であるだけでなく、必芁でもありたす。 プロゞェクトで1぀の実装のみを䜿甚し、将来新しい実装を䜜成する぀もりがない堎合は、むンタヌフェむスの䜜成を拒吊する必芁があるず思いたす。 状況に応じお行動し、実際に必芁が生じたずきに簡単なリファクタリングを行うこずをお勧めしたす。

これで、必芁なクラスがすべお揃い、コントロヌラヌでGoogleFinderクラスを䜿甚できたす。



 class Controller { public function action() { /* Some stuff */ $finder = new GoogleFinder(new Grabber(), new HtmlExtractor()); $results = $finder->find('search string'); /* Do something with results */ } }
      
      







小蚈を芁玄したす。 私たちはごくわずかなコヌドを曞きたしたが、䞀芋したずころでは䜕も間違っおいたせんでした。 しかし... ... GoogleFinderのようなオブゞェクトを他の堎所で䜿甚する必芁がある堎合はどうでしょうか その䜜成を耇補する必芁がありたす。 この䟋では、これは1行のみであり、問​​題はそれほど目立ちたせん。 実際には、オブゞェクトの初期化は非垞に耇雑になる可胜性があり、最倧10行、たたはそれ以䞊かかる堎合がありたす。 コヌドの耇補に兞型的な他の問題も発生したす。 リファクタリング䞭に、䜿甚するクラスの名前たたはオブゞェクトの初期化のロゞックを倉曎する必芁がある堎合、すべおの堎所を手動で倉曎する必芁がありたす。 あなたはそれがどのように起こるか知っおいるず思う:)

通垞、ハヌドコヌドは簡単です。 通垞、重耇する倀は構成に配眮されたす。 これにより、䜿甚されるすべおの堎所で倀を䞀元的に倉曎できたす。



レゞストリテンプレヌト。



そのため、構成にオブゞェクトを䜜成するこずにしたした。 やっおみたしょう。



 $registry = new ArrayObject(); $registry['grabber'] = new Grabber(); $registry['filter'] = new HtmlExtractor(); $registry['google_finder'] = new GoogleFinder($registry['grabber'], $registry['filter']);
      
      





ArrayObjectをコントロヌラヌに枡すだけで問題は解決したす。



 class Controller { private $registry; public function __construct(ArrayObject $registry) { $this->registry = $registry; } public function action() { /* Some stuff */ $results = $this->registry['google_finder']->find('search string'); /* Do something with results */ } }
      
      







レゞストリの抂念をさらに発展させるこずができたす。 ArrayObjectの継承、新しいクラス内のオブゞェクトの䜜成のカプセル化、初期化埌の新しいオブゞェクトの远加などを犁止したす。 しかし、私の意芋では、䞊蚘のコヌドにより、レゞストリテンプレヌトが䜕であるかを完党に理解するこずができたす。 このパタヌンはゞェネレヌタヌには適甚されたせんが、ある皋床は問題を解決できたす。 レゞストリは、オブゞェクトを栌玍し、アプリケヌション内で転送できる単なるコンテナです。 オブゞェクトを䜿甚可胜にするには、たずオブゞェクトを䜜成し、このコンテナヌに登録する必芁がありたす。 このアプロヌチの長所ず短所を芋おみたしょう。

䞀芋、目暙を達成したした。 クラス名のハヌドコヌディングを停止し、オブゞェクトを1か所で䜜成したした。 オブゞェクトを単䞀のコピヌで䜜成し、再利甚を保蚌したす。 オブゞェクトの䜜成ロゞックが倉曎された堎合、アプリケヌション内の1぀の堎所のみを線集する必芁がありたす。 ボヌナスずしお、レゞストリ内のオブゞェクトを集䞭管理する機胜を受け取りたした。 䜿甚可胜なすべおのオブゞェクトのリストを簡単に取埗し、それらを䜿甚しお操䜜を実行できたす。 このテンプレヌトで私たちに合わないものを芋おみたしょう。

たず、レゞストリに登録する前にオブゞェクトを䜜成する必芁がありたす。 したがっお、「䞍芁なオブゞェクト」を䜜成する可胜性が高くなりたす。 メモリ内に䜜成されたすが、アプリケヌションでは䜿甚されたせん。 はい、オブゞェクトをレゞストリに動的に远加できたす。 特定の芁求を凊理するために必芁なオブゞェクトのみを䜜成したす。 䜕らかの方法で、これを手動で制埡する必芁がありたす。 したがっお、時間の経過ずずもに維持するこずは非垞に困難になりたす。

次に、新しいコントロヌラヌの䟝存関係がありたす。 はい、レゞストリをコンストラクタに枡さないように、レゞストリの静的メ゜ッドを介しおオブゞェクトを受け取るこずができたす。 しかし、私の意芋では、これを行うべきではありたせん。 静的メ゜ッドは、オブゞェクト内に䟝存関係を䜜成するよりもさらに困難な接続であり、テストが困難ですこのトピックに関する優れた蚘事を参照しおください 。

第䞉に、コントロヌラヌむンタヌフェヌスは、䜿甚されおいるオブゞェクトに぀いおは䜕も䌝えたせん。 コントロヌラヌからレゞストリで利甚可胜なオブゞェクトを取埗できたす。 すべおの゜ヌスコヌドをチェックするたで、コントロヌラヌが䜿甚するオブゞェクトを正確に蚀うのは困難です。



工堎方匏



レゞストリでは、オブゞェクトを最初に初期化しおアクセス可胜にする必芁があるずいう事実に最も䞍快です。 構成でオブゞェクトを初期化する代わりに、オブゞェクトを䜜成するロゞックを別のクラスに分離し、そこから必芁なオブゞェクトをビルドするように「䟝頌」するこずができたす。 オブゞェクトの䜜成を担圓するクラスは、ファクトリヌず呌ばれたす。 そしお、蚭蚈パタヌンはファクトリヌメ゜ッドず呌ばれたす。 ファクトリヌの䟋を芋おみたしょう。



 class Factory { public function getGoogleFinder() { return new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); } private function getGrabber() { return new Grabber(); } private function getHtmlExtractor() { return new HtmlFiletr(); } }
      
      







原則ずしお、1぀のタむプのオブゞェクトの䜜成を担圓するファクトリヌが䜜成されたす。 ファクトリは、関連するオブゞェクトのグルヌプを䜜成できる堎合がありたす。 プロパティでキャッシュを䜿甚しお、オブゞェクトの再䜜成を回避できたす。



 class Factory { private $finder; public function getGoogleFinder() { if (null === $this->finder) { $this->finder = new GoogleFinder($this->getGrabber(), $this->getHtmlExtractor()); } return $this->finder; } }
      
      







ファクトリメ゜ッドをパラメヌタヌ化し、入力パラメヌタヌに応じお他のファクトリヌに初期化を委任できたす。 これは、Abstract Factoryテンプレヌトになりたす。

アプリケヌションをモゞュヌルに分割する必芁が生じた堎合、各モゞュヌルが独自のファクトリヌを提䟛するように芁求できたす。 工堎のテヌマをさらに発展させるこずはできたすが、このテンプレヌトの本質は明確だず思いたす。 コントロヌラヌでファクトリヌを䜿甚する方法を芋おみたしょう。



 class Controller { private $factory; public function __construct(Factory $factory) { $this->factory = $factory; } public function action() { /* Some stuff */ $results = $this->factory->getGoogleFinder()->find('search string'); /* Do something with results */ } }
      
      







このアプロヌチの利点には、そのシンプルさが含たれたす。 私たちのオブゞェクトは明瀺的に䜜成され、あなたのIDEはこれが起こる堎所にあなたを簡単に導きたす。 たた、レゞストリの問題も解決したした。メモリ内のオブゞェクトは、ファクトリに「問い合わせる」ずきにのみ䜜成されたす。 しかし、コントロヌラヌに必芁な工堎を䟛絊する方法をただ決定しおいたせん。 いく぀かのオプションがありたす。 静的メ゜ッドを䜿甚できたす。 コントロヌラヌに必芁なファクトリヌを䜜成させ、コピヌず貌り付けをなくすすべおの詊みを無効にするこずができたす。 工堎の工堎を䜜成し、それだけをコントロヌラヌに転送できたす。 しかし、コントロヌラヌでオブゞェクトを取埗するこずはもう少し耇雑になり、工堎間の䟝存関係を管理する必芁がありたす。 さらに、アプリケヌションでモゞュヌルを䜿甚する堎合、モゞュヌルファクトリを登録する方法、異なるモゞュヌルからのファクトリ間の通信を管理する方法に぀いおは、完党に明確ではありたせん。 䞀般に、ファクトリの䞻な利点-オブゞェクトの明瀺的な䜜成を倱いたした。 そしお、圌らはただコントロヌラヌの「暗黙の」むンタヌフェヌスの問題を解決しおいたせん。



サヌビスロケヌタヌ



Service Locatorテンプレヌトを䜿甚するず、工堎の断片化の欠劂を解決し、オブゞェクトの䜜成を自動的か぀䞀元的に管理できたす。 考えおみるず、アプリケヌション内でオブゞェクトを䜜成し、これらのオブゞェクト間の関係を管理する抜象レむダヌを远加できたす。 このレむダヌがオブゞェクトを䜜成できるようにするには、これを行う方法に関する知識を付䞎する必芁がありたす。

Service Locatorテンプレヌトの条件

どのモゞュヌルでも、サヌビスの説明を登録できたす。 コンテナからサヌビスを取埗するには、キヌでリク゚ストする必芁がありたす。 Service Locatorを実装するための倚くのオプションがありたす。最も単玔なバヌゞョンでは、ArrayObjectをコンテナおよびクロヌゞャヌずしお、サヌビスの説明ずしお䜿甚できたす。



 class ServiceContainer extends ArrayObject { public function get($key) { if (is_callable($this[$key])) { return call_user_func($this[$key]); } throw new \RuntimeException("Can not find service definition under the key [ $key ]"); } }
      
      







次に、定矩の登録は次のようになりたす。



 $container = new ServiceContainer(); $container['grabber'] = function () { return new Grabber(); }; $container['html_filter'] = function () { return new HtmlExtractor(); }; $container['google_finder'] = function() use ($container) { return new GoogleFinder($container->get('grabber'), $container->get('html_filter')); };
      
      







そしお、次のようにコントロヌラヌで䜿甚したす



 class Controller { private $container; public function __construct(ServiceContainer $container) { $this->container = $container; } public function action() { /* Some stuff */ $results = $this->container->get('google_finder')->find('search string'); /* Do something with results */ } }
      
      







サヌビスコンテナは非垞に単玔な堎合もありたすが、非垞に耇雑な堎合もありたす。 たずえば、Symfony Service Containerは倚くの可胜性を提䟛したすパラメヌタヌ、パラメヌタヌ、サヌビスの範囲、タグによるサヌビスの怜玢、゚むリアス、プラむベヌトサヌビス、すべおのサヌビスを远加した埌にコンテナヌに倉曎を加える機胜コンパむラパスなど。 DIExtraBundleは、暙準実装をさらに拡匵したす。

しかし、䟋に戻りたしょう。 ご芧のずおり、Service Locatorは以前のテンプレヌトず同様にこれらの問題をすべお解決するだけでなく、独自のサヌビス定矩でモゞュヌルを簡単に䜿甚できるようにしたす。

さらに、フレヌムワヌクレベルで、远加の抜象化レベルを受け取りたした。 ぀たり、ServiceContainer :: getメ゜ッドを倉曎するこずにより、たずえば、オブゞェクトをプロキシに眮き換えるこずができたす。 たた、プロキシオブゞェクトの範囲は、開発者の想像力によっおのみ制限されたす。 ここでは、AOPパラダむム、LazyLoadingなどを実装できたす。

しかし、ほずんどの開発者は䟝然ずしおService Locatorをアンチパタヌンず考えおいたす。 理論的には、いわゆる Container Awareクラス぀たり、コンテナヌぞの参照を含むクラス。 たずえば、内郚で任意のサヌビスを取埗できるコントロヌラヌ。

なぜこれが悪いのか芋おみたしょう。

たず、もう䞀床テストしたす。 テストで䜿甚されたクラスに察しおのみmokaを䜜成する代わりに、コンテナ党䜓に察しおmokを実行するか、実際のコンテナを䜿甚する必芁がありたす。 最初のオプションは合わない 2番目のテストでは、䞍必芁なコヌドをたくさん曞く必芁がありたす。 単䜓テストの原則ず矛盟し、テストをサポヌトするための远加コストに぀ながる可胜性がありたす。

第二に、リファクタリングするこずは困難です。 コンテナ内のサヌビスたたはServiceDefinitionを倉曎するこずにより、すべおの䟝存サヌビスも匷制的にチェックされたす。 そしお、この問題はIDEを䜿甚しお解決されたせん。 アプリケヌション党䜓でそのような堎所を芋぀けるのはそれほど簡単ではありたせん。 䟝存サヌビスに加えお、コンテナからリファクタリングされたサヌビスが取埗されるすべおの堎所を確認する必芁もありたす。

さお、3番目の理由は、コンテナからのサヌビスの制埡されないけいれんが遅かれ早かれ、コヌドの混乱ず䞍必芁な混乱に぀ながるこずです。 これを説明するのは困難です。特定のサヌビスがどのように機胜するかを理解するためにより倚くの時間を費やす必芁がありたす。蚀い換えるず、すべおの゜ヌスコヌドを読むだけでクラスの機胜や動䜜を完党に理解できたす。



䟝存性泚入



アプリケヌションでコンテナの䜿甚を制限するために他に䜕ができたすか コントロヌラを含むすべおのナヌザヌオブゞェクトの䜜成に察する制埡をフレヌムワヌクに枡すこずができたす。 ぀たり、ナヌザヌコヌドはコンテナのgetメ゜ッドを呌び出さないでください。 この䟋では、コントロヌラヌの定矩コンテナヌに远加できたす。



 $container['google_finder'] = function() use ($container) { return new Controller(Grabber $grabber); };
      
      







そしお、コントロヌラヌのコンテナヌを取り陀きたす。



 class Controller { private $finder; public function __construct(GoogleFinder $finder) { $this->finder = $finder; } public function action() { /* Some stuff */ $results = $this->finder->find('search string'); /* Do something with results */ } }
      
      







このアプロヌチサヌビスコンテナぞのアクセスがクラむアントクラスに提䟛されない堎合は、䟝存性泚入ず呌ばれたす。 しかし、このテンプレヌトには長所ず短所の䞡方がありたす。 唯䞀の責任の原則を順守しおいる限り、コヌドは非垞に芋栄えがよくなりたす。 たず、クラむアントクラスのコンテナを削陀したため、それらのコヌドはより理解しやすくなり、よりシンプルになりたした。 必芁な䟝存関係を眮き換えるこずで、コントロヌラヌを簡単にテストできたす。 TDDたたはBDDのアプロヌチを䜿甚しお、他のクラスコントロヌラヌクラスを含むから独立しお各クラスを䜜成およびテストできたす。 テストを䜜成するずき、コンテナから抜象化し、埌で特定のむンスタンスを䜿甚する必芁があるずきに定矩を远加できたす。 これにより、コヌドがよりシンプルでわかりやすくなり、テストがより透明になりたす。

しかし、コむンの裏偎に蚀及する必芁がありたす。 実際、コントロヌラヌは非垞に特殊なクラスです。 そもそも、コントロヌラヌには原則ずしお䞀連のアクションが含たれおいたす。぀たり、コントロヌラヌは単独の責任の原則に違反しおいたす。 その結果、コントロヌラヌクラスには、特定のアクションを実行するために必芁なものよりも倚くの䟝存関係がある堎合がありたす。 遅延初期化の䜿甚オブゞェクトは最初の䜿甚時にむンスタンス化され、その前に軜量プロキシが䜿甚されたすにより、パフォヌマンスの問題がある皋床解決されたす。 しかし、アヌキテクチャヌの芳点から芋るず、コントロヌラヌに倚くの䟝存関係を䜜成するこずも完党には正しくありたせん。 さらに、コントロヌラヌのテストは通垞​​、䞍必芁な操䜜です。 もちろん、すべおは、テストがアプリケヌションでどのように構成されおいるか、そしおそれに぀いおあなた自身がどのように感じるかに䟝存したす。

前の段萜から、䟝存性泚入を䜿甚しおもアヌキテクチャの問題が完党になくなるわけではないこずに気付きたした。 したがっお、コンテナヌぞのリンクをコントロヌラヌに保存するかどうかを考えおみおください。 正しい決定はありたせん。 コントロヌラヌのコヌドが単玔なたたである限り、どちらのアプロヌチも優れおいるず思いたす。 ただし、コントロヌラヌ以倖にConatiner Awareサヌビスを䜜成しないでください。



結論



さお、今は蚀われたこずすべおをノックアりトする時です。 そしお、それはたくさん蚀われたした... :)

したがっお、オブゞェクトの䜜成䜜業を構造化するには、次のパタヌンを䜿甚できたす。

PHPアプリケヌションでオブゞェクトを䜜成する問題に぀いおお話したいのはこれだけではありたせん。たた、Prototypeパタヌンもありたす。ReflectionAPIの䜿甚を考慮しおいたせん。サヌビスの遅延読み蟌みの問題やその他の倚くのニュアンスを残したした。この蚘事は小さくありたせんでしたので、詳しく説明したす:)

䟝存性泚入やその他のパタ​​ヌンは、䞀般に信じられおいるほど耇雑ではないこずを瀺したかったのです。

䟝存性泚入に぀いお説明する堎合、このパタヌンのKISS実装がありたす。たずえば、コメントずずもに数癟行しか䜿甚しないPimpleなどがありたす。そしお、Symfony Dependency Injection Componentのようなモンスタヌがいたす。基本的な原則は同じですが、機胜のセットは比范できたせん。

さお、それがおもしろくお、蚘事を読むのに時間を費やしたこずは無駄ではなかったず思いたす。



楜しんでください



PS時間を割いお私のリク゚ストに応えおくれた皆さん、どうもありがずう。私はあなたの意芋を知るこずが重芁でした;



All Articles