lundi, octobre 20 2008, 21:15
Le pattern Adapter : Quezako par l'exemple
Par Olivier Hoareau - Architecture - Lien permanent
Imaginons une classe qui est censé fournir une liste de villes et de départements qui seront utilisés dans une balise HTML select pour permettre à un internaute de sélectionner sa ville de naissance à partir d'une liste.
Nous commençons simplement par récupérer la liste des villes disponibles dans un fichier texte CSV ayant la structure suivante : ...
Imaginons une classe qui est censé fournir une liste de villes et de départements qui seront utilisés dans une balise HTML select pour permettre à un internaute de sélectionner sa ville de naissance à partir d'une liste.
Nous commençons simplement par récupérer la liste des villes disponibles dans un fichier texte CSV ayant la structure suivante :
code_ville;nom_ville PAR;Paris NYK;New York SVL;Seville BXL;Bruxelles ...
Puis nous récupérons la liste des villes comme suit en PHP :
class VilleService {
public function getVilles() {
$handle = fopen("villes.csv", "r");
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
$villes[$data[0]] = $data[1];
}
fclose($handle);
return $villes;
}
}
$service = new VilleService;
$villes = $service->getVilles();
// $villes contient la liste des villes comme suit :
// $villes = array('PAR'=>'Paris', 'NYK'=>'New York','SVL'=>'Seville','BXL'=>'Bruxelles');
Notre script est alors fonctionnel et nous pouvons utiliser la variable $villes pour construire notre balise HTML select (liste déroulante). Notre application évolue et nous voulons utiliser une base de données pour stocker la liste des villes, ce qui nous donneras plus de souplesse pour mettre à jour cette liste, par exemple depuis un formulaire. Nous devons alors recoder la récupération de la liste de villes depuis zéro depuis la base de données, notre script est impacté.
Imaginons que nous avions eu le code suivant :
$service = new VilleService;
$adapter = new CsvAdapter;
$adapter->setFile('villes.csv');
$service->setAdapter($adapter);
$villes = $service->getVilles();
La classe CsvAdapter correspond à une implémentation de la récupération des villes depuis un fichier CSV. Voici le contenu des classes :
class VilleService {
protected $adapter;
public function setAdapter(AdapterInterface $adapter) {
$this->adapter = $adapter;
}
public function getVilles() {
return $this->adapter->listRows();
}
}
interface AdapterInterface {
public function listRows();
}
class CsvAdapter implements AdapterInterface {
public function listRows() {
$handle = fopen("villes.csv", "r");
while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
$rows[$data[0]] = $data[1];
}
fclose($handle);
return $rows;
}
}
A quoi cela nous aurait-il servit ? Revenons à notre besoin d'évolution : nous voulions faire évoluer notre script pour récupérer les données en base de données et non plus à partir d'un fichier. En utilisant les concepts de service et d'adapter, nous pourrions créer un nouvel adapter, DatabaseAdapter, qui implémenterait l'interface AdapterInterface et fournirait donc la méthode listRows() comme suit :
class DatabaseAdapter implements AdapterInterface {
protected $dsn;
protected $user;
protected $password;
public function setConnectionParameters($dsn,$user,$password) {
$this->dsn = $dsn;
$this->user = $user;
$this->password = $password;
}
public function listRows() {
try {
$dbh = new PDO($this->dsn, $this->user, $this->password);
foreach($dbh->query('SELECT * from villes') as $row) {
$rows[$row[0]] = $row[1];
}
$dbh = null;
} catch (PDOException $e) {
$rows = array();
}
return $rows;
}
}
Nous pourrions ainsi utiliser notre nouvel adapter dans notre script comme suit :
$service = new VilleService;
$adapter = new DatabaseAdapter;
$adapter->setConnectionParameters('mysql:host=localhost;dbname=mydb','myuser','mypassword');
$service->setAdapter($adapter);
$villes = $service->getVilles();
Soit 2 lignes de changées dans notre script (c'est peu !).
Nous pourrions faire encore évoluer par la suite notre script pour récupérer la liste des villes depuis un webservice SOAP, simplement en ajoutant un adapter, par exemple SoapAdapter :
$service = new VilleService; $adapter = new SoapAdapter; $service->setAdapter($adapter); $villes = $service->getVilles();
Notre classe SoapAdapter devrait implémenter AdapterInterface et notamment la méthode listRows() :
class SoapAdapter implements AdapterInterface {
...
public function listRows() {
$soapClient = new SoapClient(...);
...
$villesList = $soapClient->getVilles();
...
return $viles;
}
}
Le pattern Adapter vous permet d'externaliser des opérations (récupération d'une liste de villes par exemple) dans une classe d'implémentation spécifique (récupération depuis un fichier csv, récupération depuis une base de données...) vous permettant ainsi de changer l'implémentation (passer d'un fichier à une base) sans impacter votre script (vous créez un nouvel adapter, le code d'appel reste le même).
Nous verrons plus tard (autre article) que le concept d'Adapter est souvent utilisé dans les tests unitaires en conjonction avec le pattern Mock (fausse implémentation) pour réaliser des tests sans déclencher certains opérations couteuses (par exemple tester la logique de construction d'un email sans envoyer réellement l'email).
un commentaire
Tres bien presente! :)
Juste une remarque :
"$adapter = new CsvAdapter;
$adapter->setFile('villes.csv');"
-> Visiblement la methode setFile a ete oubliee dans l'implementation de l'adapter.
De la meme maniere j'ajouterais une methode setTable() a la classe DatabaseAdapter, ca rendrait ces 2 adapters encore plus versatiles!
En tout cas merci pour le post!