Избавление от повторного вызова методов

в 13:28, , рубрики: php, модели, метки: ,

Описание

В топике описан один из вариантов избавления от повторного вызова методов объекта. Но данный нельзя считать правильным, так как хороший разработчик в принципе не должен допустить возникновения ситуаций, когда это может понадобиться, но, как показывает практика, такое встречается часто, особенно в каких либо паблик проектах.

Пример

К примеру нужно вывести какую либо информацию, а перед этим проверить её наличие. Что то типо:

if ($oObject->getData())
{
       foreach ($oObject->getData() as $oData)
      {
             ...
      }
}

Метод getData() вызывается дважды. То есть может быть два одинаковых запроса к БД, файлам, каким либо сервисам, да куда угодно. Обходится это элементарно:

$aPhotos = $oObject->getData();

if ($aPhotos)
{
       foreach ($aPhotos as $oPhoto)
      {
            ...
      }
}

Но бывают ситуацию, когда код уже написан, и проще исправить все в одном файле, нежели во всем проекте.

Решение

На помощь приходит магия PHP:

<?php

 abstract class Model 
 {
 	/**
 	 *	Methods return data
 	 */
 	protected $_aMethodData = array(); 

 	public function __call($sMethod, $aArguments)
 	{	
 		$sMethod 	= '_'.$sMethod;
 		$sHash 		=  md5( serialize($aArguments) );

 		if (!isset($this->_aMethodData[$sMethod][$sHash]))
 		{
 			$this->_aMethodData[$sMethod][$sHash]  = call_user_func_array( array($this, $sMethod), $aArguments);
 		} 		

 		return $this->_aMethodData[$sMethod][$sHash];
 	}
 }

Названия методов, которые должны вызываться один раз, должны начинаться с нижнего подчеркивания.

...

public fucntion _getData()
{

}

...

Теперь при вызове метода getData(), будет вызываться метод _getData(), а его возвращаемая информация будет храниться в массиве, и при повторном вызове, с учетом передаваемых аргументов, будет возвращен сохраненный результат с предыдущего вызова, либо новый результат, если передаваемые аргументы будут отличаться.

class Test extends Model 
{
	public function _getData($iNumber = 1)
	{	
		return $iNumber;
	}
}

$oTest = new Test();

$oTest ->getData();  // Произойдет вызов метода _getData()
$oTest ->getData();  // Будет возвращен сохранный результат

$oTest ->getData(2); // Новый выхов функции так как передаваемые аргументы отличаются

$oTest ->_getData(); // Принудительный вызов метода

Осталось одна проблема — если будет создано два объекта, то сохраненные результаты будут в области видимости каждого объекта отдельно.

$oTest = new Test();
$oTest->getData();  // Произойдет вызов функции

$oSecondTest = new Test();
$oSecondTest->getData(); // Произойдет вызов функции

Для того, что бы область видимости была для всех созданных объектов, можно воспользоваться статикой. Но это так же может привести ко многим проблемам. Например если результаты выполнения методов зависят от параметров, передаваемых в конструктор объекта, или от результатов выполнения ранее вызванных методов:

$oObject = new Test(1,2);
$oObject->getData();

$oObject = new Test(3,4);
$oObject->getData();

$oObject = new Test();
$oObject->setLimit(1)->getData();

Решение может быть такое — совмещение метода со статикой и обычного.

 <?php

abstract class Model 
{
 	/**
 	 *	Methods return data
 	 */
 	protected $_aMethodData 		= array(); 
 	protected static $__aMethodData = array();

 	public function __call($sCalledMethod, $aArguments)
 	{	
 		$sGeneralMethod = '__'.$sCalledMethod;
 		$sMethod 		= '_'.$sCalledMethod; 		

 		$sClassName =  get_class($this);
 		$sHash 		=  md5( serialize($aArguments) );

 		$mReturnData = null;

 		if (method_exists($this, $sGeneralMethod))
 		{	 			
			if (!isset( self::$__aMethodData[$sClassName][$sGeneralMethod][$sHash]))
	 		{
	 			 self::$__aMethodData[$sClassName][$sGeneralMethod][$sHash]  = call_user_func_array( array($this, $sGeneralMethod), $aArguments);	 			
	 		} 

	 		$mReturnData =  self::$__aMethodData[$sClassName][$sGeneralMethod][$sHash] ;
 		}
 		elseif (method_exists($this, $sMethod))
 		{
 			if (!isset($this->_aMethodData[$sMethod][$sHash]))
	 		{
	 			$this->_aMethodData[$sMethod][$sHash]  = call_user_func_array( array($this, $sMethod), $aArguments);	 		
	 		} 	

	 		$mReturnData = $this->_aMethodData[$sMethod][$sHash];
 		}
 		else
 		{
 			throw new Exception("There are no methods (<b>".$sMethod."</b> or <b>".$sGeneralMethod."</b>) for called method ".$sCalledMethod, 1); 			
 		}	

 		return $mReturnData;
 	}
}

Теперь, если существует метод с двойным подчеркиванием в названии, результат его выполнении будет в области видимости всех созданных объектов данного класса, если с одним подчеркиванием — то результат виден только для объекта, у которого метод был вызван.

class Test extends Model 
{
	public function __getData($iNumber = 1)
	{
		return $iNumber;
	}
	
	public function _getSelfData($iNumber = 1)
	{
		return $iNumber;
	}

}

$oTest = new Test();

$oTest->getData(); 		// Произойдет вызов метода
$oTest->getData(); 		// Будет возвращен сохранный результат
$oTest->getSelfData(); 	// Произойдет вызов метода

$oSecondTest = new Test();

$oSecondTest->getData(); 		// Будет возвращен сохранный результат
$oSecondTest->getSelfData(); 	// Произойдет вызов метода
Заключение

Как я говорил в начале, подобные проблемы встречаются только в плохо спроектированных проектах. Тем более, при использование различных фреймворков (Yii ActiveRecord, Propel, Doctrine etc.), эта проблема отпадает сама собой, и то не всегда, зависит от настроек. Но бывают разные ситуации, например не правильное использование DDD. По этому, желаю Вам побольше качественно спроектированных проектов и что бы данный топик пригодился Вам в редких случаях :)

Автор: hlx


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js