Le S de SOLID

Besoin d'aide pour une architecture

Le problème exposé dans ce sujet a été résolu.

Bonjour

J’aurai besoin d’explication, au sujet de "Single responsibility principle".

Je vais avoir besoin de développer un outil (mode batch), et pour son architecture, j’aurai besoin de votre avis.

Il s’agit de transcoder une structure de données d’un type (sûrement XML) en un type binaire (normalisé). Donc l’outil doit :

  • lire les données d’un fichier
  • faire quelques contrôles
  • encodé les données dans un fichier.

Pour faire simple, j’aurais fait : Un objet, avec toutes les données en variable membre, le constructeur lirait le fichier d’input, une méthode pour ajouter les contrôles, une méthode pour sortir dans un fichier les données avec la syntaxe de sortie. Mais …

« Une classe ne doit changer que pour une seule raison », donc je vois 4 raisons de changer :

  • Si on ajoute/retire l’une des données de la structure à transcoder —> Objet 1, "Data"
  • Nouvelle syntaxe en entrée —> Objet 2, "Input"
  • Nouveaux contrôles —> Objet 3, "Check"
  • Changement/update de norme, Nouvelle syntaxe de sortie —> Objet 4, "Output"
  • (Note : Vu l’objectif de l’outil, je ne vois pas de changement sur les moyens d’input/output, ce sera toujours des fichiers)

Donc Data, Objet avec pour seule responsabilité la définition de la structure de donnée, et les 3 autres objets qui accèdent aux données de Data.

class Data {
    int M_data1;
    ...
}

class Input {
    Input (std::string  conts fileName, Data myData);
    ...
}

class Check {
    bool isValid (Data const myData) const;
         ...
}

class Output {
    Output (Data const myData, std::string const fileName);
    ...
}

Ma question : Comment ces 3 dernières classes accèdent aux membres internes de Data ?

0) On ne fait pas hériter les 3 classe de Data, ça n’a pas de sens !
1) On fait des "getter/Setter" pour chaque membre de Data, (pas beaucoup de sens non plus !)
2) On met les membres de Data en public ?
3) On met les 3 classes en "friend" de Data ?
4) Je n’ai rien compris, et c’est autre chose qu’il faut faire ?

Merci pour votre aide.

Cordialement.

PS: J’avais l’impression d’avoir déjà posé cette question, mais je ne retrouve pas.

  1. Effectivement, il n’y a aucun besoin de faire de l’héritage et ce serait une mauvaise raison d’en faire

  2. Data n’a pas besoin de setter puisque les valeurs sont définies par la lecture de ton fichier. Par contre, tu peux exposer les champs de Data dont tu as besoin via des getters. Par contre, comme tu as des contrôles à effectuer, il faut éviter de tomber sur un problème d’objets anémiques (i.e. que tu fasses des traitements sur les données en dehors de ta classe Data)

  3. C’est une solution dès l’instant où Data est un DTO et n’a aucun métier. Ce qui n’est pas le cas vu que tu as des contrôles à faire sur tes données

  4. Techniquement, Data n’a pas besoin de connaître les classes qui la manipule et tu risques de créer du couplage fort.

L’approche la plus courante que j’ai pu voir pour ce genre de problème, c’est de passer par un DSL ou un builder dont l’objectif est de faire le lien entre les représentations XML et binaires.

En gros, tu décris pour chaque valeur le champ XML associé et la représentation dans ta trame binaire de façon programmatique avec quelque chose du style (j’ai simplifié le code) :

Frame.composedBy(
Field
.named('vehicle.identifier')
.atPosition(Offset.of(0), Size.of(8)),
Field
.named('position.latitude')
.atPosition(Offset.of(8), Size.of(20))
.encodedWith(latitudeCodec)
)
)

La classe Field décrit la valeur que je veux récupérer dans mon fichier xml (ici, le identifier dans le noeud vehicle) et me permettra de remplir ma trame à partir des infos sur la position du champs dans la trame, sa taille et si besoin, une fonction de conversion (par exemple une fonction qui permet de convertir une latitude en angle en valeur binaire normée.

Ma classe Frame liste tous les champs de ma trame. Je lui passerai chaque valeur de mon fichier XML (ou autre vu que Frame ne connait pas la sources des données) et c’est elle qui décidera quoi en faire.

L’avantage de cette approche, c’est que :

  • c’est facile à tester
  • je peux facilement modifier ma trame ou ajouter des infos sur mes champs (comme une description)
  • je peux me passer de DTO pour la conversion et éviter de devoir maintenir plusieurs représentations de mes données
  • je peux ajouter des contrôles pour valider que je n’ai pas de trous/overlap dans ma trame, que les tailles sont correctes, etc.
  • si je passe par un visiteur, je peux faire ce que je veux de cette représentation : générer une documentation, prendre une source de données, faire des diffs, etc.

Bonjour,

J’ai laissé ouvert le sujet pour avoir plusieurs réponses, hélas, ce ne fut pas le cas.

Merci Mzungu, pour ta réponse.

Oui, on peut considérer Data comme un DTO (data transfer object).

Par contre, je n’ai pas identifié ce que tu appelles un "un DSL ou un builder". Peux-tu m’en dire plus ?

Merci encore.

Mais je ne sais pas a quoi il pense spécifiquement.

« Une classe ne doit changer que pour une seule raison », donc je vois 4 raisons de changer

Dedeun

Oui, mais il faut quand même conserver le but à l’esprit, pour ne pas partir dans un découpage inutilement complexe. Si on a une classe File, quel serait le bénéfice de séparer les fonctions read et write dans 2 classes différentes ? On gagnerait en maintenance ou lisibilité ? Bof dans la majorité des cas. Mais on peut imaginer aussi que l’écriture et la lecture sont des tâches complexes avec des paramètres différents et que cela justifie de séparer dans des classes (exemple avec QXmlStreamReader et QXmlStreamWriter).

Donc pas de choix absolue, il n’est pas forcément nécessaire de séparer l’écriture et la lecture. (Mais dans ton cas, ça semble pertinent, vu que les formats sont différents. Mais il faut des noms plus explicits que Input et Output).

On peut aussi considérer de séparer l’encodage/décodage du format de fichier et l’écriture/lecture proprement dite. Et donc avoir des classes comme TextFile (lecture d’un fichier texte), XmlReader (décodage du XML à partir d’un fichier texte), XxxWriter (encodage des données dans ton format je-sais-pas-quoi) et BinaryFile (écriture d’une fichier binaire). Ou avoir une seule classe File avec un paramètre texte/binary pour le format.

On peut aussi considérer que Check n’est pas une responsabilité mais une contrainte qui s’applique à tes données. Donc potentiellement un invariant de classe (donc que la vérification est faite lorsque les variables membre de Data sont modifiées et donc passer par des setters/getters et mettre les membres en privés).

Donc à mon avis :

  • 0) non
  • 1) pourquoi pas
  • 2) pourquoi pas
  • 3) (c’est l’inverse, non ? Data qui doit être friend des autres classes ?) Possible… cf ensuite
  • 4) possible :)

Pour friend, il est possible de contrôler l’accès a une classe via d’autres moyens. Par exemple que Data soit dans un private header et donc inaccessible en dehors du module.

Ca depend beaucoup de la complexité et l’architecture du reste du projet. Mais en réalité, est-ce un choix critique ? Si Data a que des membres publiques, tu vas t’amuser a utiliser cette classe en dehors de tes 3 classes ? Et si tu dois changer, est-ce un drame ? Quel sera le cout de changement ?

Hors sujet : pourquoi tes classes Input et Output ont un ordre de paramètre inversé ?

+0 -0

Bonjour et Merci, GBDivers, pour cette réponse.

Super ce chemin sur les patterns.

Pour les noms, il n’y a rien d’arrêté.

En fait, les structures seront complexes, et il faudra découper en X classes/Fonctions. En effet, ça a un sens de découper aussi en lecture fichier/décodage et encodage/écriture de fichier. Ce n’est que le premier niveau. (C’est la gestion de ces données globales à tous les modules qui me pose PB)

Ce n’est pas un choix critique, mais un exercice : Comment on applique tout ce qu’on lit sur les forums.

Je vois un peu mieux ce que je vais faire.

  • Un objet DATA avec des setters qui vérifie les contraintes et des getters qui retournent les données. (Mettre toutes les contraintes et dans le constructeur sera une usine à gaz !)
  • Un objet ou une procédure qui lit le fichier d’enté et qui appel ces setters,
  • Un objet ou une procédure qui écrit le fichier de sortie, en utilisant les getters.

En ce qui concerne l’ordre des arguments, c’est un reste des règles de codage de quand je codais en C : en premier les arguments input, en second les arguments output.

Cordialement.

Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte