Qu'est-ce qu'un code composable et comment le créer ?  – Informatique CloudSavvy
Agence web » Actualités du digital » Qu’est-ce qu’un code composable et comment le créer ? – Informatique CloudSavvy

Qu’est-ce qu’un code composable et comment le créer ? – Informatique CloudSavvy

Le code composable décrit des classes et des fonctions qui peuvent être facilement combinées pour créer des constructions de niveau supérieur plus puissantes. La composabilité se compare favorablement aux formes alternatives de réutilisation de code telles que l’héritage orienté objet. Il préconise la création de petites unités autonomes qui sont traitées comme des blocs de construction pour des systèmes plus grands.

Composabilité et inversion de contrôle

Le code composable est souvent un objectif et un effet des stratégies d’inversion de contrôle (IoC). Des techniques telles que l’injection de dépendances fonctionnent avec des composants autonomes qui sont transmis (« injectés ») aux endroits où ils sont nécessaires. Ceci est un exemple d’IoC – l’environnement externe est responsable de la résolution des dépendances des couches de code plus profondes qu’il appelle.

Le concept de composabilité englobe les pièces spécifiques que vous pouvez fournir et comment elles sont intégrées ensemble. Un système composable sera composé d’unités de fonctionnalité distinctes qui ont chacune une responsabilité unique. Des processus plus complexes sont développés en « composant » plusieurs de ces unités en une nouvelle plus grande.

Exemples de composabilité

Voici un exemple de trois unités fonctionnelles possibles :

interface LogMessage {
    public function getMessage() : string;
}
 
interface Mailable {
    public function getEmailContent() : string;
}
 
interface RelatesToUser {
    public function getTargetUserId() : int;
}

Ajoutons maintenant une implémentation de logger dans le mix :

interface Logger {
    public function log(LogMessage $message) : void;
}
 
final class SystemErrorLogMessage implements LogMessage, Mailable {
 
    public function __construct(
        protected readonly Exception $e) {}
 
    public function getMessage() : string {
        return "Unhandled error: " . $this -> e -> getMessage();
    }
 
    public function getEmailContent() : string {
        return $this -> getMessage();
    }
 
}

Considérons maintenant un autre type de message de journal :

final class UserLoggedInLogMessage implements LogMessage, Mailable, RelatesToUser {
 
    public function __construct(
        protected readonly int $UserId) {}
 
    public function getMessage() : string {
        return "User {$this -> UserId} logged in!";
    }
 
    public function getEmailContent() : string {
        return $this -> getMessage();
    }
 
    public function getTargetUserId() : int {
        return $this -> UserId;
    }
 
}

Ici, nous voyons les avantages de la composabilité. En définissant les fonctionnalités de l’application en tant qu’interfaces, les implémentations de classes concrètes sont libres de mélanger et d’assortir les éléments dont elles ont besoin. Tous les messages de journal n’ont pas d’utilisateur associé ; certains messages peuvent ne pas être éligibles aux alertes par e-mail s’ils sont de faible priorité ou contiennent des informations sensibles. Garder les interfaces autonomes vous permet de créer des implémentations flexibles pour chaque situation.

Les exemples ci-dessus sont écrits en PHP mais peuvent être reproduits dans n’importe quel langage orienté objet. La composabilité ne se limite cependant pas au code POO : c’est aussi un aspect fondamental de la programmation fonctionnelle. Ici, des comportements complexes sont obtenus en enchaînant de petites fonctions. Les fonctions peuvent prendre autre fonctionne comme arguments et renvoie un Nouveau fonction d’ordre supérieur en conséquence.

const square = x => (x * x);
const quadruple = x => (x * 4);
 
// 16
console.log(compose(square, quadruple)(2));

Cet exemple JavaScript minimal utilise le compose-function bibliothèque pour composer square et quadruple unités dans une autre fonction qui élève au carré puis quadruple son entrée. le compose() la fonction utilitaire accepte d’autres fonctions pour composer ensemble ; il renvoie une nouvelle fonction qui appelle la chaîne d’entrées en séquence.

Vous rencontrerez également la composabilité dans les cadres de développement modernes à composants. Voici un exemple d’un ensemble simple de composants React :

const UserCard = ({user, children}) => (
    <div>
        <h1>{user.name}</h1>
        {children}
    </div>
);
 
const UserAvatar = ({user}) => {
    if (user.avatarId) {
        return <img src={`/avatars/${user.avatarId}.png`} />;
    }
    else return <img src={`/avatars/generic.png`} />;
};
 
const UserCardWithAvatar = ({user}) => (
    <UserCard user={user}>
        <UserAvatar user={user} />
    </UserCard>
);

Chaque composant reste simple en ne s’intéressant qu’à une partie spécifique de la fonctionnalité globale. Vous pouvez rendre le UserCard seul ou composez une nouvelle variante avec un avatar ou tout autre composant React. le UserCard n’est pas compliqué par la logique responsable du rendu du fichier image d’avatar correct.

Composition vs Héritage

Les langages orientés objet permettent souvent de réutiliser le code par le biais de l’héritage. Choisir l’héritage comme stratégie par défaut peut être une erreur coûteuse qui rend plus difficile la maintenance d’un projet dans le temps.

La plupart des langages ne prennent pas en charge l’héritage multiple, de sorte que vos options pour des combinaisons complexes de fonctionnalités sont limitées. Voici le message de journal du précédent refactorisé dans un modèle d’héritage similaire :

class LogMessage implements LogMessage {
    public function __construct(
        public readonly string $message) : void;
}
 
class LogMessageWithEmail extends LogMessage implements Mailable {
    public function getEmailContent() : string {
        return "New Log Message: {$this -> message}";
    }
}
 
class LogMessageWithUser extends LogMessage implements RelatedToUser {
 
    public function __construct(
        public readonly string $message,
        public readonly int $userId) {}
 
    public function getTargetUserId() : int {
        return $this -> userId;
    }
 
}

Ces cours peuvent sembler utiles pour commencer. Maintenant, vous n’avez plus besoin d’implémentations de messages de journal spécifiques comme notre UserLoggedInMessage classer. Il y a cependant un gros problème : si vous avez besoin d’écrire un message de journal qui se rapporte à un utilisateur et envoie un e-mail, il n’y a pas de classe pour cela. Vous pourriez écrire un LogMessageWithEmailAndUser mais vous commenceriez sur la pente glissante de couvrir toutes les permutations possibles avec des implémentations de classes concrètes « génériques ».

Malgré ces problèmes, le code utilisant l’héritage pour ce type de modèle de relation reste répandu dans les projets, petits et grands. Il a des cas d’utilisation valides mais est souvent mis en œuvre pour de mauvaises raisons. Les petites unités composables basées sur des interfaces et des fonctions sont plus polyvalentes, vous font penser à une vue d’ensemble de votre système et ont tendance à créer un code plus maintenable avec moins d’effets secondaires.

Une bonne règle d’or pour l’héritage est de l’utiliser lorsqu’un objet est autre chose. La composition est généralement le meilleur choix lorsqu’un objet a autre chose:

  • Journal / E-mail – Un message de journal n’est pas intrinsèquement un e-mail, mais il peut avoir le contenu des e-mails qui lui sont associés. Le journal doit inclure le contenu de l’e-mail en tant que dépendance. Si tous les journaux n’ont pas de composant de courrier électronique, la composition doit être utilisée comme indiqué ci-dessus.
  • Utilisateur / Administrateur – L’Administrateur hérite de tous les comportements de l’Utilisateur et en ajoute quelques nouveaux. Cela pourrait être un bon cas pour l’héritage – Administrator extends User.

Atteindre l’héritage trop tôt peut vous restreindre plus tard, car vous trouverez des scénarios plus uniques dans votre application. Garder les unités de fonctionnalité aussi petites que possible, les définir comme des interfaces et créer des classes concrètes qui mélangent et correspondent à ces interfaces est un moyen plus efficace de conceptualiser des systèmes complexes. Il facilite la réutilisation de vos composants dans des emplacements disparates.

Sommaire

Le code composable fait référence à une source qui combine des unités modulaires autonomes en plus gros morceaux de fonctionnalités. C’est une incarnation des relations « a-un » entre différentes entités. Le mécanisme de composition réel dépend du paradigme que vous utilisez ; pour les langages POO, vous devez programmer sur des interfaces plutôt que sur des classes concrètes, alors que les domaines fonctionnels vous guident souvent vers une bonne composabilité dès la conception.

Être proactif dans votre utilisation des techniques composables conduit à un code plus résilient, faiblement couplé, plus facile à raisonner et plus adaptable aux futurs cas d’utilisation. La création de blocs composables est souvent le point de départ le plus efficace lorsque vous refactorisez des systèmes à grande échelle. Bien que des alternatives telles que l’héritage aient également des rôles valides, elles sont moins largement applicables et plus sujettes aux abus que la composabilité, l’injection de dépendances et l’IoC.

★★★★★