Gestion des paramètres dans l'application OOP

15

J'écris une application de POO de taille moyenne en C ++ comme moyen de pratiquer les principes de POO.

J'ai plusieurs classes dans mon projet, et certaines d'entre elles doivent accéder aux paramètres de configuration au moment de l'exécution. Ces paramètres sont lus à partir de plusieurs sources lors du démarrage de l'application. Certains sont lus à partir d'un fichier de configuration dans le répertoire personnel des utilisateurs, certains sont des arguments de ligne de commande (argv).

J'ai donc créé une classe ConfigBlock. Cette classe lit toutes les sources de paramètres et les stocke dans une structure de données appropriée. Les exemples sont les noms de chemin et de fichier qui peuvent être modifiés par l'utilisateur dans le fichier de configuration, ou l'indicateur CLI --verbose. Ensuite, on peut appeler ConfigBlock.GetVerboseLevel()pour lire ce paramètre spécifique.

Ma question: est-ce une bonne pratique de collecter toutes ces données de configuration d'exécution dans une classe?

Ensuite, mes classes ont besoin d'accéder à tous ces paramètres. Je peux penser à plusieurs façons d'y parvenir, mais je ne sais pas lequel prendre. Le constructeur d'une classe peut être une référence donnée à mon ConfigBlock, comme

public:
    MyGreatClass(ConfigBlock &config);

Ou ils incluent simplement un en-tête "CodingBlock.h" qui contient une définition de mon CodingBlock:

extern CodingBlock MyCodingBlock;

Ensuite, seul le fichier .cpp des classes doit inclure et utiliser les éléments ConfigBlock.
Le fichier .h n'introduit pas cette interface à l'utilisateur de la classe. Cependant, l'interface de ConfigBlock est toujours là, cependant, elle est cachée du fichier .h.

Est-il bon de le cacher de cette façon?

Je veux que l'interface soit aussi petite que possible, mais à la fin, je suppose que chaque classe qui a besoin de paramètres de configuration doit avoir une connexion à mon ConfigBlock. Mais à quoi devrait ressembler cette connexion?

lugge86
la source

Réponses:

10

Je suis assez pragmatique, mais ma principale préoccupation ici est que vous puissiez permettre à cela ConfigBlockde dominer vos conceptions d'interface de manière potentiellement mauvaise. Lorsque vous avez quelque chose comme ça:

explicit MyGreatClass(const ConfigBlock& config);

... une interface plus appropriée pourrait ressembler à ceci:

MyGreatClass(int foo, float bar, const string& baz);

... par opposition à la simple sélection de ces foo/bar/bazchamps dans un massif ConfigBlock.

Conception d'interface paresseuse

Sur le plan positif, ce type de conception facilite la conception d'une interface stable pour votre constructeur, par exemple, car si vous finissez par avoir besoin de quelque chose de nouveau, vous pouvez simplement le charger dans un ConfigBlock(éventuellement sans aucune modification de code), puis cerise- choisissez tout ce dont vous avez besoin sans aucun changement d'interface, seulement un changement dans l'implémentation de MyGreatClass.

C'est donc à la fois un avantage et un inconvénient que cela vous libère de la conception d'une interface plus réfléchie qui n'accepte que les entrées dont elle a réellement besoin. Il applique la mentalité de «Donnez-moi juste cette masse massive de données, je vais choisir ce dont j'ai besoin» plutôt que quelque chose comme «Ces paramètres précis sont ce dont cette interface a besoin pour fonctionner».

Il y a donc certainement des avantages ici, mais ils pourraient être largement contrebalancés par les inconvénients.

Couplage

Dans ce scénario, toutes ces classes construites à partir d'une ConfigBlockinstance finissent par avoir leurs dépendances comme ceci:

entrez la description de l'image ici

Cela peut devenir un PITA, par exemple, si vous souhaitez effectuer un test unitaire Class2dans ce diagramme de manière isolée. Vous devrez peut-être simuler superficiellement diverses ConfigBlockentrées contenant les champs pertinents Class2pour pouvoir le tester dans diverses conditions.

Dans tout type de nouveau contexte (que ce soit un test unitaire ou un tout nouveau projet), de telles classes peuvent finir par devenir plus un fardeau à (ré) utiliser, car nous finissons par devoir toujours apporter ConfigBlockavec nous pour le trajet et le configurer en conséquence.

Réutilisation / déployabilité / testabilité

Au lieu de cela, si vous concevez ces interfaces de manière appropriée, nous pouvons les découpler ConfigBlocket nous retrouver avec quelque chose comme ceci:

entrez la description de l'image ici

Si vous remarquez dans ce diagramme ci-dessus, toutes les classes deviennent indépendantes (leurs couplages afférents / sortants diminuent de 1).

Cela conduit à beaucoup plus de classes indépendantes (au moins indépendantes de ConfigBlock) qui peuvent être beaucoup plus faciles à (ré) utiliser / tester dans de nouveaux scénarios / projets.

Maintenant, ce Clientcode finit par être celui qui doit dépendre de tout et l'assembler tous ensemble. La charge finit par être transférée vers ce code client pour lire les champs appropriés à partir de a ConfigBlocket les passer dans les classes appropriées en tant que paramètres. Pourtant, ce code client est généralement conçu de manière étroite pour un contexte spécifique, et son potentiel de réutilisation sera généralement zilch ou fermé de toute façon (il pourrait s'agir de la fonction de mainpoint d'entrée de votre application ou quelque chose comme ça).

Du point de vue de la réutilisabilité et des tests, cela peut aider à rendre ces classes plus indépendantes. Du point de vue de l'interface pour ceux qui utilisent vos classes, cela peut également aider à indiquer explicitement les paramètres dont ils ont besoin au lieu d'un seul massif ConfigBlockqui modélise tout l'univers des champs de données requis pour tout.

Conclusion

En général, ce type de conception orientée classe qui dépend d'un monolithe qui a tout le nécessaire a tendance à avoir ce genre de caractéristiques. Leur applicabilité, déployabilité, réutilisabilité, testabilité, etc. peuvent en conséquence être considérablement dégradées. Pourtant, ils peuvent en quelque sorte simplifier la conception de l'interface si nous tentons une rotation positive. C'est à vous de mesurer ces avantages et inconvénients et de décider si les compromis en valent la peine. En règle générale, il est beaucoup plus sûr de se tromper contre ce type de conception où vous choisissez parmi un monolithe dans des classes qui sont généralement destinées à modéliser une conception plus générale et plus largement applicable.

Enfin et surtout:

extern CodingBlock MyCodingBlock;

... c'est potentiellement encore pire (plus asymétrique?) en termes de caractéristiques décrites ci-dessus que l'approche d'injection de dépendance, car elle finit par coupler vos classes non seulement à ConfigBlocks, mais directement à une instance spécifique de celle-ci. Cela dégrade davantage l'applicabilité / la déployabilité / la testabilité.

Mon conseil général serait de préférer la conception d'interfaces qui ne dépendent pas de ces types de monolithes pour fournir leurs paramètres, au moins pour les classes les plus généralement applicables que vous concevez. Et évitez l'approche globale sans injection de dépendance si vous le pouvez, sauf si vous avez vraiment une raison très forte et confiante de ne pas l'éviter.

marstato
la source
1

Habituellement, la configuration d'une application est principalement consommée par les objets d'usine. Tout objet reposant sur la configuration doit être généré à partir de l'un de ces objets d'usine. Vous pouvez utiliser le modèle abstrait d'usine pour implémenter une classe qui prend l' ConfigBlockobjet entier . Cette classe exposerait les méthodes publiques pour renvoyer d'autres objets de fabrique et ne transmettrait que la partie du ConfigBlockpertinent à cet objet de fabrique particulier. De cette façon, les paramètres de configuration "se répercutent" de l' ConfigBlockobjet à ses membres, et de l'usine Factory aux usines.

J'utiliserai C # car je connais mieux le langage, mais cela devrait être facilement transférable en C ++.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

Après cela, vous avez besoin d'une sorte de classe qui agit comme une "application" qui est instanciée dans votre procédure principale:

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

Comme dernière remarque, ceci est connu comme un "objet fournisseur" dans .NET. Les objets fournisseur dans .NET semblent marier les données de configuration aux objets d'usine, ce qui est essentiellement ce que vous voulez faire ici.

Voir aussi Modèle de fournisseur pour les débutants . Encore une fois, cela est axé sur le développement .NET, mais avec C # et C ++ étant tous deux des langages orientés objet, le modèle devrait être principalement transférable entre les deux.

Une autre bonne lecture liée à ce modèle: le modèle de fournisseur .

Enfin, une critique de ce modèle: le fournisseur n'est pas un modèle

Greg Burghardt
la source
Tout va bien, sauf les liens vers les modèles des fournisseurs. La réflexion n'est pas prise en charge par c ++, et cela ne fonctionnera pas.
BЈовић
@ BЈовић: Correct. La réflexion de classe n'existe pas, mais vous pouvez créer une solution de contournement manuelle, qui revient essentiellement à une switchinstruction ou à une ifinstruction testant une valeur lue dans les fichiers de configuration.
Greg Burghardt
0

Première question: est-ce une bonne pratique de collecter toutes ces données de configuration d'exécution dans une classe?

Oui. Il est préférable de centraliser les constantes et valeurs d'exécution et le code pour les lire.

Un constructeur de classe peut être une référence donnée à mon ConfigBlock

C'est mauvais: la plupart de vos constructeurs n'auront pas besoin de la plupart des valeurs. Au lieu de cela, créez des interfaces pour tout ce qui n'est pas trivial à construire:

ancien code (votre proposition):

MyGreatClass(ConfigBlock &config);

nouveau code:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

instancier une MyGreatClass:

auto x = MyGreatClass{ current_config_block.great_class_values() };

Ici, current_config_blockest une instance de votre ConfigBlockclasse (celle qui contient toutes vos valeurs) et la MyGreatClassclasse reçoit une GreatClassDatainstance. En d'autres termes, ne transmettez aux constructeurs que les données dont ils ont besoin et ajoutez des fonctionnalités à votre ConfigBlockpour créer ces données.

Ou ils incluent simplement un en-tête "CodingBlock.h" qui contient une définition de mon CodingBlock:

 extern CodingBlock MyCodingBlock;

Ensuite, seul le fichier .cpp des classes doit inclure et utiliser les éléments ConfigBlock. Le fichier .h n'introduit pas cette interface à l'utilisateur de la classe. Cependant, l'interface de ConfigBlock est toujours là, cependant, elle est cachée du fichier .h. Est-il bon de le cacher de cette façon?

Ce code suggère que vous disposerez d'une instance globale de CodingBlock. Ne faites pas cela: normalement, vous devriez avoir une instance déclarée globalement, quel que soit le point d'entrée utilisé par votre application (fonction principale, DllMain, etc.) et le passer comme argument partout où vous en avez besoin (mais comme expliqué ci-dessus, vous ne devez pas passer toute la classe, il suffit d'exposer les interfaces autour des données et de les transmettre).

Aussi, ne liez pas vos classes clientes (vos MyGreatClass) au type de CodingBlock; Cela signifie que, si vous prenez MyGreatClassune chaîne et cinq entiers, vous ferez mieux de passer cette chaîne et ces entiers que vous ne passerez dans a CodingBlock.

utnapistim
la source
Je pense que c'est une bonne idée de dissocier les usines de la configuration. Il n'est pas satisfaisant que l'implémentation de la configuration sache comment instancier des composants, car cela entraîne nécessairement une dépendance bidirectionnelle alors qu'auparavant, seule une dépendance unidirectionnelle existait. Cela a de grandes implications lors de l'extension de votre code, en particulier lors de l'utilisation de bibliothèques partagées où les interfaces sont vraiment importantes
Joel Cornett
0

Réponse courte:

Vous n'avez pas besoin de tous les paramètres pour chacun des modules / classes de votre code. Si vous le faites, il y a un problème avec votre conception orientée objet. Surtout en cas de test unitaire, définir toutes les variables dont vous n'avez pas besoin et passer cet objet n'aiderait pas à la lecture ou à la maintenance.

Dawid Pura
la source
De cette façon, je peux rassembler le code de l'analyseur (ligne de commande d'analyse et fichiers de configuration) dans un emplacement central. Ensuite, chaque classe peut choisir à partir de là ses paramètres pertinents. Quel est un bon design à votre avis?
lugge86
Peut-être que je l'ai mal écrit - je veux dire que vous devez (et c'est une bonne pratique) avoir une abstraction générale avec tous les paramètres obtenus à partir du fichier de configuration / des variables d'environnement - qui pourraient être votre ConfigBlockclasse. Le point ici est de ne pas fournir tous, dans ce cas, le contexte de l'état du système, juste particulier, les valeurs requises pour le faire.
Dawid Pura