9 règles pour écrire le code différement
Comme beaucoup de développeurs, j'aime apprendre de nouvelles choses. Et une chose qui m'intéresse beaucoup est la qualité de code et la manière d'écrire le code.
Afin de m'exercer dans différents domaines, je fais assez souvent des Katas auxquels je me rajoute des contraintes. Et il y a quelque temps, j'ai découvert les Object Calisthenics.
Dernière ce nom un peu barbare se cachent 9 règles à suivre lorsque l'on écrit du code. Et autant dire que certaines de ces règles sont bien contraires à tout ce qu'on apprend à l'école ou dans les différents tutoriels et formations en ligne.
L'objectif est d'avoir un code plus maintenable, plus lisible et plus facilement testable.
Le code est lu beaucoup plus souvent qu'il n'est écrit
Les règles suivantes ne sont pas à prendre au pied de la lettre. Le principe est de les expérimenter sur des petits projets perso ou lors de code Kata afin de voir ce qu'il serait intéressant d'appliquer en situation réelle.
Cela permet de faciliter la lecture et la maintenabilité du code.
// 🚫 Cette méthode contient deux niveaux d'indentation
public function notify(array $contacts)
{
foreach ($contacts as $contact) {
if ($contact->isEnabled()) {
$this->mailer->send($contact);
}
}
}
Plusieurs solutions possibles :
// ✅ Extraction du code indenté dans une autre méthode
public function notify(array $contacts)
{
foreach ($contacts as $contact) {
$this->notifyContact($contact)
}
}
private function notifyContact(Contact $contact)
{
if ($contact->isEnabled()) {
$this->mailer->send($contact);
}
}
// ---------------------------------------------------
// ✅ On filtre en amont de la boucle la liste des contacts
public function notify(array $contacts)
{
$enabledContacts = array_filter(
$contacts,
fn($contact) => $contact->isEnabled()
);
foreach ($enabledContacts as $contact) {
$this->mailer->send($contact);
}
}
// ---------------------------------------------------
// ✅ On demande à l'appelant de nous envoyer directement
// la liste des contacts activés
public function notify(array $enabledContacts)
{
foreach ($enabledContacts as $contact) {
$this->mailer->send($contact);
}
}
L'utilisation du else
nous oblige à lire du code imbriqué (avec donc plus de niveaux d'indentation) alors qu'on peut la plupart des cas s'en passer.
class ItemManager
{
private $cache;
private $repository;
public function __construct($cache, $repository)
{
$this->cache = $cache;
$this->repository = $repository;
}
public function get($id)
{
if (!$this->cache->has($id)) {
$item = $this->repository->get($id);
$this->cache->set($id, $item);
} else {
$item = $this->cache->get($id);
}
return $item;
}
}
class ItemManager
{
private $cache;
private $repository;
public function __construct($cache, $repository)
{
$this->cache = $cache;
$this->repository = $repository;
}
// ✅ Utilisation d'early returns
public function get($id)
{
if ($this->cache->has($id)) {
return $this->cache->get($id);
}
$item = $this->repository->get($id);
$this->cache->set($id, $item);
return $item;
}
}
public function redirect(User $user)
{
if ($user->isAuthenticate()) {
$urlToRedirect = '/dashboard';
} else {
$urlToRedirect = '/login';
}
return $urlToRedirect;
}
// ✅ Initialisation en amont de la valeur par défaut
// valide si l'initialisation est peu coûteuse
public function redirect(User $user)
{
$urlToRedirect = '/login';
if ($user->isAuthenticate()) {
$urlToRedirect = '/dashboard';
}
return $urlToRedirect;
}
class MyListener
{
public function onDelete(Event $event)
{
if ($event->getType() === 'OBJECT_DELETE'
&& $event->getObject instanceOf MyEntity) {
$this->cache->invalidate($event->getObject());
} else {
if ($event->getType() !== 'OBJECT_DELETE') {
throw new \Exception('Invalid event type');
} else {
throw new \Exception('Invalid object instance');
}
}
}
}
// ✅ Utilisation du principe Fail Fast : on teste tout
// de suite les cas d'erreurs
class MyListener
{
public function onDelete(Event $event)
{
if ($event->getType() !== 'OBJECT_DELETE') {
throw new \Exception('Invalid event type');
}
$myEntity = $event->getObject();
if (!$myEntity instanceOf MyEntity) {
throw new \Exception('Invalid object instance');
}
$this->cache->invalidate(myEntity);
}
}
(surtout ceux qui ont des comportements particuliers)
Avantages :
public function fizzBuzz(int $integer)
{
if ($integer <= 0) {
throw new \Exception('Only positive integer is handled');
}
if ($integer%15 === 0) {
return 'FizzBuzz';
}
//...
}
// Remplacement du int par un objet PositiveInteger
public function fizzBuzz(PositiveInteger $integer)
{
// ✅ Plus de test de validation du paramètre en entrée
if ($integer->isMultipleOf(15)) {
return 'FizzBuzz';
}
// ...
}
// Utilisation d'un Value Object
class PositiveInteger
{
private $value;
public function __construct(int $integer)
{
// ✅ Le test de validation de l'entier se fait directement ici
if ($integer <= 0) {
throw new \Exception('Only positive integer is handled');
}
$this->value = $integer;
}
// ✅ On peut même ajouter des fonctions liés à cet objet
public function isMultipleOf(int $multiple)
{
return $this->valueinteger%$multiple === 0;
}
}
Autre exemple :
// 🚫 Le fait de passer un tableau ne nous permet pas d'être sur
// du contenu et nous oblige à faire des tests supplémentaires
public function notify(array $enabledContacts)
{
foreach ($contacts as $contact) {
if ($contact->isEnabled()) {
$this->mailer->send($contact);
}
}
}
// ✅ On passe ici directement un objet contenant uniquement
// des contacts activés.
// On est donc assuré de n'avoir que des contacts actifs
public function notify(EnabledContacts $enabledContacts)
{
foreach ($enabledContacts as $contact) {
$this->mailer->send($contact);
}
}
class EnabledContacts implements \Iterator
{
private $contacts;
public function __construct(array $contacts)
(
// ✅ On ne garde ici que les contacts actifs
$this->contacts = array_filter(
$contacts,
fn(Contact $contact) => $contact->isEnabled()
);
)
// ... définition des méthode de l'interface \Iterator
}
Autre exemple :
// 🚫 Deux paramètres sont ici fortement liés
public function findAll(int $start, int $end)
{
// récupération paginée des données en BDD
}
// ✅ On regroupe ici dans une seule classe deux attributs
// qui étaient liés
public function findAll(Pagination $pagination)
{
$start = $pagination->getStart();
$end = $pagination->getEnd();
...// récupération paginée des données en BDD
}
Le code lié à cette collection est désormais encapsulé dans sa propre classe.
class Newsletter
{
private int $id;
private string $title;
// 🚫 L'objet contient déjà deux attributs, il ne peut
// donc pas contenir un array. Il faut l'encapsuler
// dans un objet
private array $subscriberCollection;
public function getNumberOfSubscriberWhoOpen()
{
$subscribersWhoOpen = array_filter(
$this->subscriberCollection,
fn($subscriber) => $subscriber->hasOpenNewsletter()
);
return \count($subscriberWhoOpen);
}
// ....
}
class Newsletter
{
private int $id;
private string $title;
// ✅ Le tableau est désormais encapsulé dans sa propre classe
private SubscriberCollection $subscriberCollection;
public function getNumberOfSubscriberWhoOpen()
{
return $this->subscriberCollection
->getSubscriberWhoOpen()
->count();
}
// ....
}
class SubscriberCollection
{
private array $subscriberCollection;
// ✅ On peut déclarer ici des méthodes "métiers"
// liées aux subscribers
public function getSubscriberWhoOpen()
{
$subscribersWhoOpen = array_filter(
$this->subscriberCollection,
fn($subscriber) => $subscriber->hasOpenNewsletter()
);
return new static($subscribersWhoOpen);
}
// ...
}
L'objectif ici n'est pas d'avoir un code joliment formaté, mais de respecter la loi de Demeter : "Ne parlez qu'à vos amis immédiats".
Bien évidemment $this->monAttribut
ne compte pas dans le décompte
class User
{
private ?Identity $identity;
public function getIdentity(): ?Identity
{
return $this->identity;
}
}
class Identity
{
private string $firstName;
private string $lastName;
public function getFirstName(): string
{
return $this->firstName;
}
public function getLastName(): string
{
return $this->lastName;
}
}
$user = new User();
$fullName = sprintf(
'%s %s',
// 🚫 Non respect de la loi de demeter
// 🚫 getIdentity() pourrait très bien retourner null
// et cela générerait une erreur
$user->getIdentity()->getFirstName(),
$user->getIdentity()->getLastName()
);
class User
{
private ?Identity $identity;
public function getFullName(): string
{
if ($this->identity === null) {
return 'John Doe';
}
return sprintf(
'%s %s',
// La règle d’origine s’applique par exemple au java ou le mot clé « this »
// n’a pas besoin d’être spécifié dans les classes.
// On ne compte donc pas ici la première ->
// car en PHP $this est obligatoire dans les classes
// pour utiliser un attribut
$this->identity->getFirstName(),
$this->identity->getLastName()
);
}
}
class Identity
{
private string $firstName;
private string $lastName;
public function getFirstName(): string
{
return $this->firstName;
}
public function getLastName(): string
{
return $this->lastName;
}
}
$user = new User();
// ✅ Respect de la loi de Demeter
// ✅ Plus de gestion d'erreur ici
$fullName = $user->getFullName();
// ✅ Cette règle ne s'applique par pour les fluent interface
// comme les query builder par exemple
$query = $queryBuilder
->select('user')
->where('user.id = :id')
->setParameter('id', 1);
->getQuery()
;
Une des règles la plus simple à appliquer et surtout à appliquer de suite !
🚫 $m->send($contact);
✅ $mailer->send($contact)
🚫 $cmpMng->getById($id);
✅ $companyManager->getById($contact)
🚫 $translator->trans($input);
✅ $translator->translate($input)
🔥 Contraintes :
✅ Objectif :
Exemple ici avec la limite à 2
class EntityManager
{
// 🚫 4 attributs
private EntityRepository $entityRepository;
private LoggerInterface $logger;
private MiddlewareInterface $middleware;
private NotificationService $notificationService;
public function update(Entity $entity)
{
$this->entityRepository->update($entity);
// 🚫 Ces trois traitements pourraient très bien être délocalisés
// afin d'éviter de surcharger cette méthode
// et pour faciliter l'ajout d'autres traitements plus tard
$this->logger->debug($entity);
$this->middleware->sendMessage($entity);
$this->notificationService->notify($entity);
}
}
class EntityManager
{
// ✅ Moins de dépendances
// ✅ Donc plus facile à mocker pour les tests unitaires
private EntityRepository $entityRepository;
private EventDispatcher $eventDispatcher;
public function update(Entity $entity)
{
$this->entityRepository->update($entity);
// ✅ Il sera très facile d'ajouter un autre traitement
// en ajoutant un listener sur cet événement
$this->eventDispatcher->dispatch(Events::ENTITY_UPDATE, $entity);
}
}
// ✅ Les traitements ont été délocalisés dans 3 listener distincts
// ✅ Classes petites et facilement testables
class EntityToLog
{
private LoggerInterface $logger;
public function onUpdate(Entity $entity)
{
$this->logger->debug($entity);
}
}
class EntityToMiddleware
{
private MiddlewareInterface $middleware;
public function onUpdate(Entity $entity)
{
$this->middleware->sendMessage($entity);
}
}
class EntityNotification
{
private NotificationService $notificationService;
public function onUpdate(Entity $entity)
{
$this->notificationService->notify($entity);
}
}
class Game
{
private Score $score;
public function diceRoll(int $score): void
{
$actualScore = $this->score->getScore();
// 🚫 On modifie en dehors de l'objet sa valeur pour ensuite lui "forcer" le résultat
$newScore = $actualScore + $score;
$this->score->setScore($newScore);
}
}
class Score
{
private int $score;
public function getScore(): int
{
return $this->score;
}
public function setScore(int $score): void
{
$this->score = $score;
}
}
class Game
{
private Score $score;
public function diceRoll(Score $score): void
{
$this->score->addScore($score);
}
}
class Score
{
private int $score;
public function addScore(Score $score): void
{
// ✅ On définit ici la logique
// d'addition de score
$this->score += $score->score;
}
}
Mon programme "S'entraîner pour progresser en PHP" est disponible. Il vous permettra de recevoir chaque semaine un kata de code directement dans votre boîte mail, ainsi que des aides à la réalisation, des vidéos explicatives et des défis supplémentaires.