J'écris une taille moyenne OOP application en C++ comme moyen de pratiquer OOP Principes.
J'ai plusieurs classes dans mon projet et certains d'entre eux doivent accéder aux paramètres de configuration du temps d'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 les utilisateurs Home-dir, 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 le stocke dans une structure de données appropriée. Les exemples sont des noms de chemin et de fichiers qui peuvent être modifiés par l'utilisateur dans le fichier de configuration ou le drapeau CLI -verbose. Ensuite, on peut appeler ConfigBlock.GetVerboseLevel()
afin de 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'un accès à tous ces paramètres. Je peux penser à plusieurs façons de y parvenir, mais je ne suis pas sûr de la prise de laquelle prendre. Un constructeur de classe 'peut être une référence donnée à mon configlock, comme
public:
MyGreatClass(ConfigBlock &config);
Ou ils incluent simplement un en-tête "codingblock.h" qui contient une définition de mon codeblock:
extern CodingBlock MyCodingBlock;
Ensuite, seuls les classes. CPPP doivent inclure et utiliser les trucs ConfitBlock.
[.____] Le fichier .h ne présente pas cette interface à l'utilisateur de la classe. Cependant, l'interface à configlock est toujours là, cependant, elle est cachée à partir du fichier .h.
Est-ce bon de le cacher de cette façon?
Je veux que l'interface soit aussi petite que possible, mais à la fin, chaque classe qui a besoin de paramètres de configuration doit avoir une connexion à mon configlock. Mais, que devrait-on ressembler cette connexion?
Je suis tout à fait le pragmatiste, mais ma principale préoccupation ici est que vous autorisez peut-être à cette ConfigBlock
de dominer vos conceptions d'interface d'une manière éventuellement mauvaise. Quand tu as quelque chose comme ça:
explicit MyGreatClass(const ConfigBlock& config);
... une interface plus appropriée pourrait être comme ceci:
MyGreatClass(int foo, float bar, const string& baz);
... par opposition à des cerises qui cueillent ces foo/bar/baz
champs hors d'un massif ConfigBlock
.
Conception d'interface paresseuse
Sur le côté plus, ce type de conception facilite la conception d'une interface stable pour votre constructeur, par exemple, comme si vous avez besoin de quelque chose de nouveau, vous pouvez simplement charger cela dans un ConfigBlock
(éventuellement sans aucun code changements) et ensuite Cherry-Choisissez toutes les nouvelles choses dont vous avez besoin sans aucune sorte de changement d'interface, seulement une modification de la mise en œuvre de MyGreatClass
.
Il est donc à la fois un type de pro et de consein que cela vous libère de concevoir une interface plus soigneusement pensée qui n'accepte que les intrants dont elle a besoin. Il applique la mentalité de, "Donnez-moi simplement cette touche massive de données, je vais choisir ce que j'en ai besoin de cela" par opposition à quelque chose de plus comme, "ces éléments précis Les paramètres sont ce que cette interface doit fonctionner. "
Donc, il y a définitivement des avantages ici, mais ils pourraient être fortement dépassés par les inconvénients.
Couplage
Dans ce scénario, toutes ces classes sont construites à partir d'une instance ConfigBlock
finissent par avoir que leurs dépendances ressemblent à ceci:
Cela peut devenir un pita, par exemple, si vous voulez un test de test Class2
Dans ce diagramme isolément. Vous pourriez avoir à simuler superficiellement diverses entrées ConfigBlock
contenant les champs pertinents Class2
est intéressé à pouvoir le tester dans une variété de conditions.
Dans toute sorte de nouveau contexte (que ce soit des tests unitaires ou du tout nouveau projet), de telles classes peuvent finir par devenir plus un fardeau de (re) utiliser, car nous finissons à adopter toujours ConfigBlock
pour le monter et le mettre en place en conséquence.
Réutilisabilité/Déploiement/Testabilité
Au lieu de cela, si vous concevez ces interfaces de manière appropriée, nous pouvons les découpler de ConfigBlock
et vous retrouver avec quelque chose comme ceci:
Si vous remarquez dans ce schéma ci-dessus, toutes les classes deviennent indépendantes (leurs couplages afférents/sortants réduisent de 1).
Cela conduit à beaucoup plus de classes indépendantes (au moins indépendantes de ConfigBlock
) qui peut être beaucoup plus facile à utiliser (re) utiliser/tester dans de nouveaux scénarios/projets.
Maintenant, ce code Client
finit par être celui qui doit dépendre de tout et l'assemblage ensemble. Le fardeau finit par être transféré dans ce code client pour lire les champs appropriés à partir d'un ConfigBlock
et de les transmettre dans les classes appropriées comme 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 va généralement être zilch ou fermé de toute façon (il s'agissait peut-être de la fonction de point d'entrée de votre application main
ou quelque chose comme ça).
Donc, d'un point de vue de réutilisabilité et de test, cela peut aider à rendre ces classes plus indépendantes. Du point de vue de l'interface pour ceux qui utilisent vos classes, vous pouvez également aider à indiquer explicitement quels paramètres ils ont besoin au lieu d'un seul massif ConfigBlock
qui modélise l'ensemble de l'univers des champs de données requis pour tout.
Conclusion
En général, ce type de design axé sur la classe qui dépend d'un monolithe qui a tout besoin a tendance à avoir ce type de caractéristiques. Leur applicabilité, leur déploiement, leur réutilisabilité, leur testabilité, etc. peuvent être considérablement dégradées. Pourtant, ils peuvent en quelque sorte simplifier la conception de l'interface si nous essayons de tourner positif dessus. C'est à vous de mesurer ces avantages et les inconvénients et décider si les compromis en valent la peine. Typiquement, il est beaucoup plus sûr de vous tromper contre ce type de design où vous êtes en cerisier d'un monolithe dans des classes qui sont généralement destinées à modéliser une conception plus générale et plus applicable.
Enfin et surtout:
extern CodingBlock MyCodingBlock;
... cela est potentiellement pire (plus asymégué?) 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 pour ConfigBlocks
, mais directement à A semble spécifique de celui-ci. Cela dégrade davantage l'applicabilité/le déploiement/la testabilité.
Mon conseil général voudrait se tromper du côté de la conception d'interfaces qui ne dépendent pas de ce type de monolithes pour fournir leurs paramètres, du moins pour les classes les plus généralement applicables que vous concevez. Et éviter l'approche globale sans injection de dépendance Si vous le pouvez, à moins que vous n'ayez vraiment pas une raison très forte et confiante de ne pas l'éviter.
Habituellement, la configuration d'une application est consommée principalement par des objets d'usine. Tout objet s'appuyant sur la configuration doit être généré à partir de l'un de ces objets d'usine. Vous pouvez utiliser le Abstract Factory Motif pour implémenter une classe qui prend dans l'ensemble de l'objet ConfigBlock
. Cette classe exposerait des méthodes publiques pour renvoyer d'autres objets d'usine et ne passerait que dans la partie du ConfigBlock
pertinent pour cet objet d'usine particulier. De cette façon, les paramètres de configuration "ruissellement" à partir de l'objet ConfigBlock
à ses membres et de l'usine d'usine aux usines.
Je vais utiliser c # car je connais mieux la langue, mais cela devrait être facilement transférable à 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 une dernière note, celle-ci est connue sous le nom d'un "objet de fournisseur" dans .NET parle. Les objets fournisseurs dans .NET semblent marier des 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 débutants . Encore une fois, cela est orienté vers le développement .NET, mais avec C # et C++ étant les deux langues orientées objet, le motif devrait être principalement transférable entre les deux.
Une autre bonne lecture est liée à ce modèle : le modèle de fournisseur .
Enfin, une critique de ce modèle: fournisseur n'est pas un modèle
Première question: est-il 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 les valeurs d'exécution et le code pour les lire.
Un constructeur de classe peut être une référence donnée à mon configlock
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 pour construire:
vieux code (votre proposition):
MyGreatClass(ConfigBlock &config);
nouveau code:
struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();
instanciez une myGreatClass:
auto x = MyGreatClass{ current_config_block.great_class_values() };
Ici, current_config_block
est une instance de votre classe ConfigBlock
(celui qui contient toutes vos valeurs) et la classe MyGreatClass
reçoit une instance GreatClassData
. En d'autres termes, ne transmettez qu'aux constructeurs les données dont elles ont besoin et additionnent des installations à votre ConfigBlock
pour créer ces données.
Ou ils incluent simplement un en-tête "codingblock.h" qui contient une définition de mon codeblock:
extern CodingBlock MyCodingBlock;
Ensuite, seuls les classes. CPPP doivent inclure et utiliser les trucs ConfitBlock. Le fichier .h ne présente pas cette interface à l'utilisateur de la classe. Cependant, l'interface à configlock est toujours là, cependant, elle est cachée à partir du fichier .h. Est-ce bon de le cacher de cette façon?
Ce code suggère que vous aurez une instance globale de codingblock. Ne faites pas cela: Normalement, vous devriez avoir une instance déclarée globalement, dans tout point d'entrée de votre application utilisée (fonction principale, DLLMain, etc.) et transmettez-la dans la mesure où vous avez besoin (mais comme expliqué ci-dessus, vous ne devez pas passer Toute la classe autour, exposez simplement des interfaces autour des données et passez celles).
En outre, ne liez pas vos classes client (votre MyGreatClass
) au type de CodingBlock
; Cela signifie que si votre MyGreatClass
prend une chaîne et cinq entiers, vous ferez mieux de passer dans cette chaîne et des entiers que vous passerez dans un CodingBlock
.
Vous NE PAS besoin de tous les paramètres de chacun des modules/classes de votre code. Si vous faites, alors il y a quelque chose qui ne va pas avec votre conception orientée objet. Surtout en cas de test de l'unité définissant toutes les variables dont vous n'avez pas besoin et en passant par cet objet ne vous aiderait pas à lire ou à entretenir.