Fermer

juin 5, 2023

Quand les utiliser (Partie 2) —

Quand les utiliser (Partie 2) —


Dans Partie 1 de la série, Kirill a expliqué comment faire en sorte que les objets JavaScript normaux se comportent comme des valeurs primitives. Examinons maintenant de près l’utilité des objets primitifs, en explorant comment la réduction des capacités pourrait être un avantage pour votre projet.

L’écriture de programmes en JavaScript est accessible au début. La langue est indulgente et vous vous habituez à ses affordances. Avec le temps et l’expérience de travail sur des projets complexes, vous commencez à apprécier des choses comme le contrôle et la précision dans le flux de développement.

Une autre chose que vous pourriez commencer à apprécier est la prévisibilité, mais c’est beaucoup moins une garantie en JavaScript. Alors que les valeurs primitives sont suffisamment prédictives, les objets ne le sont pas. Lorsque vous obtenez un objet en entrée, vous devez tout vérifier :

  • Est-ce un objet ?
  • A-t-il cette propriété que vous recherchez ?
  • Lorsqu’une propriété détient undefinedest-ce sa valeur, ou la propriété elle-même manque-t-elle ?

Il est compréhensible que ce niveau d’incertitude vous laisse légèrement paranoïaque dans le sens où vous commencez à remettre en question tous vos choix. Par la suite, votre code devient défensif. Vous vous demandez davantage si vous avez traité tous les cas défectueux ou non (il y a de fortes chances que ce ne soit pas le cas). Et au final, votre programme est surtout une collection de chèques plutôt que d’apporter une réelle valeur au projet.

En fabriquant des objets primitif, de nombreux points de défaillance potentiels sont déplacés vers un seul emplacement, celui où les objets sont initialisés. Si vous pouvez vous assurer que vos objets sont initialisés avec un certain ensemble de propriétés et que ces propriétés contiennent certaines valeurs, vous n’avez pas à vérifier des choses comme l’existence de propriétés ailleurs dans votre programme. Vous pourriez garantir que undefined est une valeur si vous en avez besoin.

Regardons l’une des façons dont nous pouvons créer des objets primitifs. Ce n’est pas le seul moyen ni même le plus intéressant. Son but est plutôt de démontrer que travailler avec des objets en lecture seule n’a pas à être fastidieux ou difficile.

Note: Je vous conseille également de vérifier la première partie de la sérieoù j’ai couvert certains aspects de JavaScript qui aident à rapprocher les objets des valeurs primitives, ce qui nous permet en retour de bénéficier de fonctionnalités de langage communes qui ne sont généralement pas associées à un objet, comme les comparaisons et les opérateurs arithmétiques.

Faire des objets primitifs en vrac

Le plus simple, le plus primitif (jeu de mots) pour créer un objet primitif est la suivante :

const my_object = Object.freeze({});

Cette ligne unique donne un objet qui peut représenter n’importe quoi. Par exemple, vous pouvez implémenter une interface à onglets en utilisant un objet vide pour chaque onglet.

import React, { useState } from "react";

const summary_tab = Object.freeze({});
const details_tab = Object.freeze({});

function TabbedContainer({ summary_children, details_children }) {
    const [ active, setActive ] = useState(summary_tab);
  
    return (
        <div className="tabbed-container">
            <div className="tabs">
                <label
                    className={active === summary_tab ? "active" : ""}
                    onClick={() => {
                        setActive(summary_tab);
                    }}
                >
                    Summary
                </label>
                <label
                    className={active === details_tab ? "active": ""}
                    onClick={() => {
                        setActive(details_tab);
                    }}
                >
                    Details
                </label>
            </div>
            <div className="tabbed-content">
                {active === summary_tab && summary_children}
                {active === details_tab && details_children}
            </div>
        </div>
    );
}

export default TabbedContainer;

Si vous êtes comme moi, ça tabs l’élément ne demande qu’à être retravaillé. En y regardant de plus près, vous remarquerez que les éléments d’onglet sont similaires et nécessitent deux choses, comme un référence d’objet et un chaîne d’étiquette. Incluons le label propriété dans le tabs objets et déplacer les objets eux-mêmes dans un tableau. Et puisque nous ne prévoyons pas de changer tabs de toute façon, rendons également ce tableau en lecture seule pendant que nous y sommes.

const tab_kinds = Object.freeze([
    Object.freeze({ label: "Summary" }),
    Object.freeze({ label: "Details" })
]);

Cela fait ce dont nous avons besoin, mais c’est verbeux. L’approche que nous allons examiner maintenant est souvent utilisée pour masquer les opérations répétitives afin de réduire le code aux seules données. De cette façon, il est plus évident que les données sont incorrectes. Ce que nous voulons aussi, c’est freeze objets (y compris le tableau) par défaut plutôt que d’être quelque chose que nous devons nous rappeler de taper. Pour la même raison, le fait que nous devions spécifier un nom de propriété à chaque fois laisse place à des erreurs, comme des fautes de frappe.

Pour initialiser facilement et de manière cohérente des tableaux d’objets primitifs, j’utilise un populate fonction. Je n’ai pas réellement une seule fonction qui fait le travail. J’en crée généralement un à chaque fois en fonction de ce dont j’ai besoin sur le moment. Dans le cas particulier de cet article, c’est l’un des plus simples. Voici comment nous allons procéder :

function populate(...names) {
    return function(...elements) {
        return Object.freeze(
            elements.map(function (values) {
                return Object.freeze(names.reduce(
                    function (result, name, index) {
                        result[name] = values[index];
                        return result;
                    },
                    Object.create(null)
                ));
            })
        );
    };
}

Si celui-ci semble dense, en voici un qui est plus lisible :

function populate(...names) {
    return function(...elements) {
        const objects = [];

        elements.forEach(function (values) {
            const object = Object.create(null);

            names.forEach(function (name, index) {
                object[name] = values[index];
            });

            objects.push(Object.freeze(object));
        });

        return Object.freeze(objects);
    };
}

Avec ce genre de fonction à portée de main, nous pouvons créer le même tableau d’objets à onglets comme suit :

const tab_kinds = populate(
    "label"
)(
    [ "Summary" ],
    [ "Details" ]
);

Chaque tableau du deuxième appel représente les valeurs des objets résultants. Disons maintenant que nous voulons ajouter plus de propriétés. Nous aurions besoin d’ajouter un nouveau nom au premier appel et une valeur à chaque tableau lors du second appel.

const tab_kinds = populate(
    "label",
    "color",
    "icon"
)(                                          
    [ "Summary", colors.midnight_pink, "💡" ],
    [ "Details", colors.navi_white, "🔬" ]
);

Avec un peu d’espace, vous pourriez le faire ressembler à un tableau. De cette façon, il est beaucoup plus facile de repérer une erreur dans les définitions énormes.

Vous avez peut-être remarqué que populate renvoie une autre fonction. Il y a plusieurs raisons de le conserver dans deux appels de fonction. Tout d’abord, j’aime la façon dont deux appels contigus créent une ligne vide qui sépare les clés et les valeurs. Deuxièmement, j’aime pouvoir créer ce genre de générateurs pour des objets similaires. Par exemple, disons que nous devons créer ces objets d’étiquette pour différents composants et que nous voulons les stocker dans différents tableaux.

Revenons à l’exemple et voyons ce que nous avons gagné avec le populate fonction:

import React, { useState } from "react";
import populate_label from "./populate_label";

const tabs = populate_label(
    [ "Summary" ],
    [ "Details" ]
);

const [ summary_tab, details_tab ] = tabs;

function TabbedContainer({ summary_children, details_children }) {
    const [ active, setActive ] = useState(summary_tab);
      
    return (
        <div className="tabbed-container">
            <div className="tabs">
                {tabs.map((tab) => (
                    <label
                        key={tab.label}
                        className={tab === active ? "active" : ""}
                        onClick={() => {
                            setActive(tab);
                        }}
                    >
                        {tab.label}
                    </label>
                )}
            </div>
            <div className="tabbed-content">
                {summary_tab === active && summary_children}
                {details_tab === active && details_children}
            </div>
        </div>
    );
}

export default TabbedContainer;

L’utilisation d’objets primitifs facilite l’écriture de la logique de l’interface utilisateur.

En utilisant des fonctions comme populate est moins fastidieux pour créer ces objets et voir à quoi ressemblent les données.

Plus après saut! Continuez à lire ci-dessous ↓

Vérifiez cette radio

L’une des alternatives à l’approche ci-dessus que j’ai rencontrée est de conserver le active état — que l’onglet soit sélectionné ou non — stocké en tant que propriété de tabs objet:

const tabs = [
    {
        label: "Summary",
        selected: true
    },
    {
        label: "Details",
        selected: false
    },
];

De cette façon, nous remplaçons tab === active avec tab.selected. Cela peut sembler une amélioration, mais regardez comment nous devrions changer l’onglet sélectionné :

function select_tab(tab, tabs) {
    tabs.forEach((tab) => tab.selected = false);
    tab.selected = true;
}

Comme c’est logique pour un bouton radio, un seul élément peut être sélectionné à la fois. Ainsi, avant de définir un élément à sélectionner, nous devons d’abord nous assurer que tous les autres éléments sont désélectionnés. Oui, c’est idiot de le faire comme ça pour un tableau avec seulement deux éléments, mais le monde réel est plein de listes plus longues que cet exemple.

Avec un objet primitif, il faut une seule variable qui représente l’état sélectionné. Je suggère de définir la variable sur l’un des éléments pour en faire l’élément actuellement sélectionné ou de le définir sur undefined si votre implémentation ne permet aucune sélection.

Avec les éléments à choix multiples comme les cases à cocher, l’approche est presque la même. Nous remplaçons la variable de sélection par un tableau. Chaque fois qu’un élément est sélectionné, nous le poussons vers ce tableau, ou dans le cas de Redux, nous créons un nouveau tableau avec cet élément présent. Pour le désélectionner, nous le raccordons ou le filtrons.

let selected = []; // Nothing is selected.

// Select.
selected = selected.concat([ to_be_selected ]);

// Unselect.
selected = selected.filter((element) => element !== to_be_unselected);

// Check if an element is selected.
selected.includes(element);

Encore une fois, c’est simple et concis. Vous n’avez pas besoin de vous rappeler si la propriété s’appelle selected ou active; vous utilisez l’objet lui-même pour le déterminer. Lorsque votre programme devient plus complexe, ces lignes sont les moins susceptibles d’être refactorisées.

À la fin, ce n’est pas le travail d’un élément de liste de décider s’il est sélectionné ou non. Il ne devrait pas contenir ces informations dans son état. Par exemple, que se passe-t-il s’il est simultanément sélectionné et non sélectionné dans plusieurs listes à la fois ?

Alternative aux chaînes

La dernière chose que j’aimerais aborder est un exemple d’utilisation de chaîne que je rencontre souvent.

Le texte est un bon compromis pour l’interopérabilité. Vous définissez quelque chose comme une chaîne et obtenez instantanément une représentation d’un contexte. C’est comme avoir une poussée d’énergie instantanée en mangeant du sucre. Comme pour le sucre, le meilleur des cas est que vous n’obtenez rien à long terme. Cela dit, ce n’est pas satisfaisant et vous avez inévitablement à nouveau faim.

Le problème avec les cordes, c’est qu’elles sont pour les humains. Il est naturel pour nous de distinguer les choses en leur donnant un nom. Mais un programme ne comprend pas la signification de ces noms.

La plupart des éditeurs de code et des environnements de développement intégrés (IDE) ne comprennent pas les chaînes. En d’autres termes, vos outils ne vous diront pas si la chaîne est correcte ou non.

Votre programme ne sait que si deux chaînes sont égal ou non. Et même dans ce cas, dire si les chaînes sont égales ou inégales ne permet pas nécessairement de savoir si l’une de ces chaînes contient ou non une faute de frappe.

Les objets fournissent plus de moyens de voir que quelque chose ne va pas avant d’exécuter votre programme. Comme vous ne pouvez pas avoir de littéraux pour les objets primitifs, vous devez obtenir une référence quelque part. Par exemple, s’il s’agit d’une variable et que vous faites une faute de frappe, vous obtenez un erreur de référence. Il existe des outils qui pourraient détecter ce genre de choses avant que le fichier ne soit enregistré.

Si vous deviez obtenir vos objets à partir d’un tableau ou d’un autre objet, alors JavaScript ne vous donnera pas d’erreur lorsque la propriété ou un index n’existe pas. Ce que vous obtenez est undefined, et c’est quelque chose que vous pouvez vérifier. Vous avez une seule chose à vérifier. Avec les chaînes, vous avez des surprises que vous voudrez peut-être éviter, comme lorsqu’elles sont vides.

Une autre utilisation des chaînes que j’essaie d’éviter est de vérifier si nous obtenons l’objet que nous voulons. Habituellement, cela se fait en stockant une chaîne dans une propriété nommée id. Comme, disons que nous avons une variable. Afin de vérifier s’il contient l’objet que nous voulons, nous pourrions avoir besoin de vérifier si une chaîne dans le id propriété correspond à celle que nous attendons. Pour ce faire, nous vérifierions d’abord si la variable contient un objet. Si la variable contient un objet, mais que l’objet n’a pas le id propriété, alors on obtient undefined, et nous allons bien. Cependant, si nous avons l’une des valeurs inférieures de cette variable, nous ne pouvons pas demander directement la propriété. Au lieu de cela, nous devons faire quelque chose pour nous assurer que seuls les objets arrivent à ce stade ou pour effectuer les deux vérifications en place.

const myID = "Oh, it's so unique";

function magnification(value) {
    if (value && typeof value === "object" && value.id === myID) {
        // do magic
    }
}

Voici comment nous pouvons faire la même chose avec des objets primitifs :

import data from "./the file where data is stored";

function magnification(value) {
    if (value === data.myObject) {
        // do magic
    }
}

L’avantage des chaînes est qu’elles sont une seule chose qui pourrait être utilisée pour l’identification interne et qui sont immédiatement reconnaissables dans les journaux. Ils sont certes faciles à utiliser dès la sortie de la boîte, mais ils ne sont pas vos amis à mesure que la complexité d’un projet augmente.

je trouve il y a peu d’avantages à s’appuyer sur des chaînes pour autre chose que la sortie vers l’utilisateur. Le manque d’interopérabilité des chaînes dans les objets primitifs pourrait être résolu progressivement et sans qu’il soit nécessaire de modifier la façon dont vous gérez les opérations de base, comme les comparaisons.

Emballer

Travailler directement avec des objets nous libère des pièges qui viennent avec d’autres méthodes. Notre code devient plus simple parce que nous écrivons ce que votre programme doit faire. En organisant votre code avec des objets primitifs, nous sommes moins affectés par la nature dynamique de JavaScript et de certains de ses bagages. Les objets primitifs nous donnent plus de garanties et un plus grand degré de prévisibilité.

Lectures complémentaires sur SmashingMag

Éditorial fracassant
(dd, yk, le)






Source link