Conception objets par composition de classes
-
Principe de la composition de classes
En programmation objets, on va séparer les responsabilités en développant des classes à la responsabilité, en respectant le principe de programmation SOLID.
Pour développer un logiciel modulaire, aux comportements différents suivant les usages qui en sont faits, on va enrichir des classes métier de base pour leur adjoindre des fonctionnalités supplémentaires.
Le principe est qu’on remplace une classe par sa descendante, enrichie des fonctionnalités supplémentaires qu’on veut lui adjoindre. Tous les objets, même dans le logiciel d’origine qui n’a pas connaissance des enrichissements apportés, sont instanciés avec la classe descendante enrichie à la place de la classe d’origine, garantissant ainsi que les nouvelles fonctionnalités sont disponibles à tout moment.
Composer vos classes
Dans it.rocks, vous allez composer vos classes :- en créant des traits qui vont ajouter / remplacer des fonctionnalités à vos classes existantes,
- en déclarant une classe fille, descendant de la classe que vous souhaitez enrichir,
- en enrichissant les comportements de vos classes en utilisant plugins et AOP.
Exemple métier
Dans les exemples qui suivent, nous partirons d’une classe métier simple : un article dans votre logiciel de gestion.
Une classe article de base serait :
namespace Bappli\Bappli; /** * @business * @feature * @representative code, name */ class Item { /** * @var string */ public $code; /** * @mandatory * @max_length 2000 * @var string */ public $name; /** * @return string */ public function __toString() { return $this->code . (strlen($this->code) ? (':' . SP) : '') . $this->name; } }
Dans le logiciel de gestion dont il est question, un article contient au minimum un code et un nom.
Suivant les utilisations qui en sont fait, chez différents utilisateurs, cet article pourra se voir adjoindre des traits supplémentaires, suivant si on a besoin d’en gérer les achats, les ventes, les stocks, etc.Création de traits
Les traits permettent d’ajouter des propriétés et des méthodes à une classe. Ils peuvent entrer dans la composition d’une classe descendant, ou servir d’ajouts à des classes existantes directement dans le fichier de configuration de votre application.
Par exemple on peut prévoir un trait qui entrera dans la composition d’un article pour permettre de gérer son prix de vente :
<?php namespace Bappli\Bappli\Sales\Item; /** * @business * @group Pricing sales_price */ trait Has_Sales_Price { /** * @var float */ public $sales_price; }
Composition dynamique dans votre fichier de configuration
Les traits sont conçus pour entrer dans la composition de vos classes. Si on pensait non-modulaire, c’est à dire si notre logiciel de gestion gérait les ventes pour tous ses utilisateurs, on déclarerait directement le trait dans la classe article de base par un
use Has_Sales_Price
. Toutefois on cherche ici à n’ajouter à notre fiche article que les données qui lui sont nécessaires, suivant l’utilisation qui va être faite du logiciel de gestion.C’est donc dans le fichier de configuration du projet final, par le biais de la classe plugin core Builder, qu’on va indiquer qu’on ajoute à notre classe
Item
ce traitHas_Sales_Price
:Extrait du fichier de configuration du projet final :
// (...) $config['Author/Software'] = [ Configuration::APP => Application::class, Configuration::EXTENDS_APP => 'Bappli/Bappli', Priority::CORE => [ Framework\Builder::class => [ Bappli\Item::class => [ Bappli\Sales\Item\Has_Sales_Price::class ] ] ] ];
Avec cette déclaration, partout où vous utilisez un objet de classe
Bappli\Item
, le framework instanciera à la place une classe construite , c’est à dire une classe fille qu’il a généré lui-même, qui hérite deBappli\Item
et utilise le traitHas_Sales_Price
.Pour que les mécanismes de composition dynamique fonctionnent, vos instanciations doivent toujours être réalisées via la fabrique Builder::create(), et pas avec la fonction
new
. La fabrique se chargera de vérifier si la classe doit être remplacée par une classe de composition fille.Fonctionnement interne : une classe construite est générée dans le cache.
La convention de nommage veut qu’elle s’appelle en fonction du nom de la classe d’origine : ici la classeBappli\Bappli\Item
donnera lieu à une classe de composition nomméeAuthor\Software\Built\Bappli\Item
.
Si vous êtes curieux, vous pouvez consulter le code source de cette classe de composition dans le fichier cache/compiled/author-software-built-bappli-item :
<?php namespace Author\Software\Built\Bappli; /** Built Item class */ class Item extends \Bappli\Bappli\Item { use \Bappli\Bappli\Sales\Item\Has_Sales_Price; }
Dans votre fichier de configuration, vous pouvez déclarer la composition des classes en indiquant :- soit une classe fille de remplacement (voir plus bas),
- soit une liste de traits et/ou interfaces qui vont enrichir votre classe d’origine.
Composition via une classe héritée
Vous pouvez également déclarer dans votre logiciel une classe héritée de la classe que vous souhaitez enrichir. Dans cette classe, vous pouvez ajouter du comportement, de la composition par traits et interfaces. Pour reprendre notre cas d’école, si on crée une classe fille dans le projet final :
<?php namespace Author\Software; use Bappli\Bappli; use Bappli\Bappli\Sales\Item\Has_Sales_Price; /** * My item is rich */ class Item extends Bappli\Item { use Has_Sales_Price; // do whatever you want to make it rich }
Il faut alors déclarer la classe finale que vous allez utiliser dans votre fichier de configuration du projet final :
// (...) $config['Author/Software'] = [ Configuration::APP => Application::class, Configuration::EXTENDS_APP => 'Bappli/Bappli', Priority::CORE => [ Framework\Builder::class => [ Bappli\Item::class => Software\Item::class ] ] ];
Avec cette déclaration, partout où vous utilisez un objet de classe
Bappli\Item
, le framework instanciera à la place votre classe filleAuthor\Software\Item
.Comme précédemment, pour que les mécanismes de composition dynamique fonctionnent, vos instanciations doivent toujours être réalisées via la fabrique Builder::create(), et pas avec la fonction
new
. La fabrique se chargera de vérifier si la classe doit être remplacée par une classe de composition fille.Enrichir les comportements de vos objets via plugins et AOP
Une autre façon d’enrichir les comportements de classes existantes, sans les surcharger, est d’utiliser leurs propriétés et méthodes existantes comme des points de coupe pour le moteur AOP du framework.
Voyez les chapitres AOP et plugins concernant ces possibilités.
Voir aussi