Classes & PHP

Design patterns en PHP4

Les design patterns définissent des manières de procéder en développement, qui permettent de résoudre des problèmes récurrents. Seuls les design patterns les plus courants seront présentés.

Singleton

Le design pattern Singleton permet de s'assurer qu'il n'existe qu'une seule et unique instance d'un objet.

Voici le code correspondant :

Class Singleton
{
	var $name;

	function Singleton()
	{
		// Initialisation
		$this->name = 'toto';
	}

	function &getInstance()
	{
		static $singleton;

		if (!$singleton)
			$singleton = new Singleton();

		return $singleton;
	}
}

$var1 =& Singleton::getInstance()."\n";
$var2 =& Singleton::getInstance()."\n";

echo $var1->name."\n";
echo $var2->name."\n";
$var2->name = 'titi';
echo $var1->name."\n";

Cependant, il reste un inconvénient majeur, rien n'empêche de créer une nouvelle instance en se passant de la méthode getInstance(). Dans ce cas, la meilleure solution consiste à mettre l'initialisation dans la méthode getInstance(). Il n'y a donc aucun moyen de forcer le développeur à utiliser la méthode getInstance().

Télécharger le code source de l'exemple

Observer

Le design pattern Observer est aussi très répandu. Il permet de s'enregistrer auprès d'un objet afin d'être averti des éventuels changements.

Voici le code correspondant :

Class Observable
{
	var $name;

	var $_listeners = array();

	function Observable($name)
	{
		$this->name = $name;
	}

	function register($listener)
	{
		if (!is_object($listener) || !is_a($listener, 'Observer'))
			return;

		foreach ($this->_listeners as $el)
			if ($el === $listener)
				return;
		$this->_listeners[] =& $listener;
	}

	function _notify($event)
	{
		foreach ($this->_listeners as $el)
			$el->notify($event);
	}

	function setName($name)
	{
		$this->name = $name;
		$this->_notify('name');
	}
}

Class Observer
{
	var $name;

	function Observer($name)
	{
		$this->__uniqid = uniqid(rand(), true);
		$this->name = $name;
	}

	function notify($event)
	{
		echo "Je m'appelle ".$this->name." et le nom de l'objet observé a changé\n";
	}
}

$observable = new Observable('toto');
echo $observable->name."\n";

$obs1 =& new Observer('obs1');
$obs2 =& new Observer('obs2');
$observable->register($obs1);
$observable->register($obs1);
$observable->register($obs2);
$observable->register($observable);

$observable->setName('titi');
echo $observable->name."\n";

Ceci produira :

toto
Je m'appelle obs1 et le nom de l'objet observé a changé
Je m'appelle obs2 et le nom de l'objet observé a changé
titi

Dans le cas présent, j'ai utilisé la méthode is_a pour être sûr que l'objet peut être enregistré. En effet, tout objet de type Observer ou qui a Observer pour parent est accepté.

Une autre manière de procéder consiste à vérifier, grâce à la fonction method_exists, que la méthode appelée lors de la notification est définie dans l'objet. Cette technique, plus laxiste, est utile car la notion d'interface n'existe pas en PHP4.

On utilise le champ __uniqid, dans les objets Observer, qui permet de s'assurer qu'un objet n'est pas déjà enregistré. Ce champ n'est volontairement pas défini avec le mot-clé var afin que sa présence ne soit pas explicite au développeur, il ne sert qu'à la comparaison.

Télécharger le code source de l'exemple

Composite

Le design pattern Composite organise les objets sous forme hierarchique et les actions sont effectuées indifféremment pour un objet ou un arbre.

Voici le code correspondant :

Class Component
{
	var $name;
	var $parent;
	var $children;

	function Component($name)
	{
		$this->__uniqid = uniqid(rand(), true);
		$this->name = $name;
		$this->parent = null;
		$this->children = array();
	}

	function addChild(&$child)
	{
		if (!is_object($child) || !is_a($child, 'Component'))
			return false;

		if ($child->parent != null)
			$child->parent->removeChild($child);

		$this->children[] =& $child;

		return true;
	}

	function removeChild(&$child)
	{
		if (!is_object($child) || !is_a($child, 'Component'))
			return false;

		for ($i = 0; $i < count($this->children); $i++)
		{
			if (isset($this->children[$i]) && $this->children[i] === $child)
			{
				unset($this->children[$i]);
				$this->children[$i]->parent = null;
				return true;
			}
		}

		return false;
	}

	function display($depth = 0)
	{
		echo str_repeat('-', $depth).$this->name."\n";

		foreach($this->children as $el)
			$el->display($depth +2);
	}
}

$node = new Component('node');
$node1 = new Component('node1');
$node11 = new Component('node11');
$node12 = new Component('node12');
$node121 = new Component('node121');
$node122 = new Component('node122');
$node13 = new Component('node13');
$node2 = new Component('node2');
$node3 = new Component('node3');
$node31 = new Component('node31');
$node32 = new Component('node32');

$node->addChild($node1);
$node1->addChild($node11);
$node1->addChild($node12);
$node12->addChild($node121);
$node12->addChild($node122);
$node1->addChild($node13);
$node->addChild($node2);
$node->addChild($node3);
$node3->addChild($node31);
$node3->addChild($node32);

$node->display(1);

Ceci produit :

-node
---node1
-----node11
-----node12
-------node121
-------node122
-----node13
---node2
---node3
-----node31
-----node32

Il y a deux choses importantes dans ce code. D'une part, on utilise un identifiant unique tout comme dans le design pattern Observer. D'autre part, il n'existe aucune méthode pratique pour supprimer un élément d'un tableau. Donc une fois supprimé, l'indice n'est plus présent, c'est la raison pour laquelle lors du parcours des élements de la méthode removeChild, on vérifie tout d'abord la présence de l'indice.

Télécharger le code source de l'exemple

Bridge

Le design pattern Bridge permet de séparer la définition d'un objet de son implémentation. Il permet, par exemple, de communiquer avec une base de données quelle qu'elle soit en cachant son implémentation.

Voici un exemple :

Class DatabaseManager
{
	var $impl;

	function DatabaseManager()
	{
		$this->impl = null;
	}

	function setImplementor(&$impl)
	{
		if (!is_object($impl) || !is_a($impl, 'Database'))
			return false;

		$this->impl =& $impl;
		return true;
	}

	function connect($host, $user, $pass, $base)
	{
		if (!$this->impl)
			return false;
		return $this->impl->connect($host, $user, $pass, $base);
	}

	function query($sql)
	{
		if (!$this->impl)
			return array();
		return $this->impl->query($sql);
	}
}

Class Database
{
	function connect($host, $user, $pass, $base) { return false; }
	function query($sql) { return array(); }
}

Class MySQL extends Database
{
	var $_id;

	function connect($host, $user, $pass, $base)
	{
		$this->_id = @mysql_connect($host, $user, $pass);
		if (!$this->_id)
			return false;
		return @mysql_select_db($base, $this->_id);
	}

	function query($sql)
	{
		$tab = array();
		$res = @mysql_query($sql, $this->_id);
		if (!$res)
			return array();
		while ($row = @mysql_fetch_assoc($res))
			$tab[] = $row;
		@mysql_free_result($res);
		return $tab;
	}
}

Class PostgreSQL extends Database
{
	var $_id;

	function connect($host, $user, $pass, $base)
	{
		$this->_id = @pg_connect('host='.$host.' dbname='.$base.' user='.$user.' password='.$pass);
		if (!$this->_id)
			return false;
		return true;
	}

	function query($sql)
	{
		$tab = array();
		$res = @pg_query($this->_id, $sql);
		if (!$res)
			return array();
		while ($row = @pg_fetch_assoc($res))
			$tab[] = $row;
		@pg_free_result($res);
		return $tab;
	}
}

$mgr =& new DatabaseManager();
$impl =& new MySQL();
$mgr->setImplementor($impl);
echo $mgr->connect('localhost', 'root', '', 'mysql') ? 'Connecté !' : 'Non connecté !';

Cet exemple, bien qu'un peu long, a le mérite de présenter une solution réelle.

La première chose à noter dans cet exemple est l'utilisation de la classe Database qui fait office de classe abstraite. Ces dernières n'existant pas, on choisit d'utiliser une classe qui retourne la valeur des codes d'erreur.

Finalement, il y a l'utilisation du symbole @ qui inhibe l'affichage du message d'erreur en cas d'erreur. En effet, lorsqu'un fichier est inexistant ou tout autre erreur, un message disgracieux est affiché. Il indique la cause de l'erreur, le fichier et la ligne de l'erreur. Il suffit de préfixer les appels des méthodes par le préfixe @ pour supprimer l'affichage de l'erreur (exemple : @mysql_connect).

Télécharger le code source de l'exemple