C'est un peu ésotérique pour le moins, comme vous l'avez déjà reconnu, ce qui pourrait me faire me gratter la tête un instant lorsque je commence à rencontrer votre code en me demandant ce que vous faites et où ces classes d'assistance sont implémentées jusqu'à ce que je commence à choisir votre style / habitudes (à quel point je pourrais m'y habituer totalement).
J'aime que vous réduisiez la quantité d'informations dans les en-têtes. Surtout dans les très grandes bases de code, cela peut avoir des effets pratiques pour réduire les dépendances au moment de la compilation et, finalement, les temps de génération.
Ma réaction instinctive est que si vous ressentez le besoin de masquer les détails de l'implémentation de cette manière, pour favoriser le passage des paramètres aux fonctions autonomes avec liaison interne dans le fichier source. Habituellement, vous pouvez implémenter des fonctions utilitaires (ou des classes entières) utiles pour implémenter une classe particulière sans avoir accès à tous les internes de la classe et simplement passer les fonctions pertinentes de l'implémentation d'une méthode à la fonction (ou constructeur). Et naturellement cela a l'avantage de réduire le couplage entre votre classe et les "aides". Il a également tendance à généraliser davantage ce qui aurait pu autrement être des "aides" si vous constatez qu'ils commencent à servir un objectif plus généralisé applicable à plusieurs implémentations de classe.
Il m'arrive aussi parfois de grincer des dents quand je vois beaucoup "d'aides" dans le code. Ce n'est pas toujours vrai, mais parfois ils peuvent être symptomatiques d'un développeur qui décompose les fonctions au gré des volonté pour éliminer la duplication de code avec d'énormes taches de données transmises à des fonctions avec des noms / objectifs à peine compréhensibles au-delà du fait qu'elles réduisent la quantité de code requis pour implémenter d'autres fonctions. Un peu plus de réflexion à l'avance peut parfois conduire à une plus grande clarté sur la façon dont l'implémentation d'une classe est décomposée en fonctions supplémentaires, et favoriser la transmission de paramètres spécifiques à la transmission d'instances entières de votre objet avec un accès complet aux internes peut aider promouvoir ce style de pensée de conception. Je ne dis pas que vous faites ça, bien sûr (je n'en ai aucune idée),
Si cela devient compliqué, j'envisagerais une deuxième solution plus idiomatique qui est le bouton (je me rends compte que vous en avez parlé, mais je pense que vous pouvez généraliser une solution pour éviter celles avec un effort minimal). Cela peut déplacer beaucoup d'informations que votre classe doit implémenter, y compris ses données privées, loin du gros de l'en-tête. Les problèmes de performances du pimpl peuvent être largement atténués avec un allocateur à temps constant bon marché * comme une liste gratuite tout en préservant la sémantique des valeurs sans avoir à implémenter un ctor de copie défini par l'utilisateur à part entière.
- Pour l'aspect performance, le bouton présente un minimum de surcharge de pointeur, mais je pense que les cas doivent être assez sérieux lorsque cela pose un problème pratique. Si la localité spatiale n'est pas dégradée de manière significative via l'allocateur, vos boucles serrées itérant sur l'objet (qui devraient généralement être homogènes si les performances sont très préoccupantes) auront toujours tendance à minimiser les erreurs de cache dans la pratique à condition d'utiliser quelque chose comme une liste gratuite pour allouer le bouton, plaçant les champs de la classe dans des blocs de mémoire largement contigus.
Personnellement, ce n'est qu'après avoir épuisé ces possibilités que j'envisagerais quelque chose comme ça. Je pense que c'est une idée décente si l'alternative est comme des méthodes plus privées exposées à l'en-tête avec peut-être seulement la nature ésotérique qui est la préoccupation pratique.
Une alternative
Une alternative qui m'est venue à l'esprit tout à l'heure et qui accomplit en grande partie les mêmes objectifs que vos amis absents est la suivante:
struct PredicateListData
{
int somePrivateField;
};
class PredicateList
{
PredicateListData data;
public:
bool match() const;
};
// In source file:
static bool fullMatch(const PredicateListData& p)
{
// Can access p.somePrivateField here.
}
bool PredicateList::match() const
{
return fullMatch(data);
}
Maintenant, cela peut sembler être une différence très théorique et je l'appellerais toujours un "assistant" (dans un sens peut-être désobligeant puisque nous passons toujours tout l'état interne de la classe à la fonction, qu'elle en ait besoin ou non) sauf qu'il évite le facteur "choc" de la rencontre friend
. En général, cela friend
semble un peu effrayant de voir fréquemment une inspection plus approfondie, car cela dit que vos internes de classe sont accessibles ailleurs (ce qui implique qu'il pourrait être incapable de maintenir ses propres invariants). Avec la façon dont vous l'utilisez, friend
cela devient plutôt théorique si les gens sont conscients de lafriend
réside simplement dans le même fichier source, aidant à implémenter la fonctionnalité privée de la classe, mais ce qui précède produit à peu près le même effet au moins avec l'avantage peut-être défendable qu'il n'implique aucun ami, ce qui évite tout ce type ("Oh tirer, cette classe a un ami. Où ses soldats sont-ils accessibles / mutés? "). Alors que la version immédiatement ci-dessus communique immédiatement qu'il n'y a aucun moyen pour les utilisateurs privés d'accéder / de muter en dehors de tout ce qui est fait dans la mise en œuvre de PredicateList
.
Cela évolue peut-être vers des territoires quelque peu dogmatiques avec ce niveau de nuance, car n'importe qui peut rapidement déterminer si vous nommez uniformément les choses *Helper*
et les mettez toutes dans le même fichier source qu'il est en quelque sorte regroupé dans le cadre de l'implémentation privée d'une classe. Mais si nous devenons pointilleux, alors le style immédiatement ci-dessus ne provoquera pas autant de réactions instinctives en un coup d'œil en l'absence du friend
mot - clé qui a tendance à sembler un peu effrayant.
Pour les autres questions:
Un consommateur peut définir sa propre classe PredicateList_HelperFunctions et lui permettre d'accéder aux champs privés. Bien que je ne considère pas cela comme un énorme problème (si vous vouliez vraiment dans ces domaines privés, vous pourriez faire du casting), peut-être que cela encouragerait les consommateurs à l'utiliser de cette façon?
Cela pourrait être une possibilité au-delà des limites de l'API où le client pourrait définir une deuxième classe avec le même nom et accéder aux internes de cette façon sans erreurs de liaison. Là encore, je suis en grande partie un codeur C travaillant dans les graphiques où les problèmes de sécurité à ce niveau de "et si" sont très faibles sur la liste des priorités, donc des préoccupations comme celles-ci ne sont que celles auxquelles j'ai tendance à agiter les mains et à faire une danse et essayez de faire comme s'ils n'existaient pas. :-D Si vous travaillez dans un domaine où de telles préoccupations sont plutôt sérieuses, je pense que c'est une considération décente à prendre.
La proposition alternative ci-dessus évite également de souffrir de ce problème. Si vous souhaitez toujours vous en tenir à l'utilisation friend
, vous pouvez également éviter ce problème en faisant de l'assistant une classe imbriquée privée.
class PredicateList
{
...
// Declare nested class.
class Helper;
// Make it a friend.
friend class Helper;
public:
...
};
// In source file:
class PredicateList::Helper
{
...
};
Est-ce un modèle de conception bien connu pour lequel il y a un nom?
Aucun à ma connaissance. Je doute qu'il y en ait un car il s'agit vraiment de la minutie des détails et du style de mise en œuvre.
"Helper Hell"
J'ai reçu une demande d'éclaircissements sur le point sur la façon dont je grince parfois des dents lorsque je vois des implémentations avec beaucoup de code "d'aide", et cela peut être légèrement controversé avec certains, mais c'est en fait factuel car j'ai vraiment grincé des dents lorsque je déboguais certains de la mise en œuvre d'une classe par mes collègues pour trouver des tas d '"aides". :-D Et je n'étais pas le seul de l'équipe à me gratter la tête en essayant de comprendre ce que tous ces assistants sont censés faire exactement. Je ne veux pas non plus me montrer dogmatique comme "Tu n'utiliseras pas d'aide", mais je ferais une petite suggestion qu'il pourrait aider à réfléchir à la façon de mettre en œuvre des choses absentes quand cela est pratique.
Les fonctions membres privées ne sont-elles pas toutes des fonctions auxiliaires par définition?
Et oui, j'inclus des méthodes privées. Si je vois une classe avec comme une interface publique simple mais comme un ensemble sans fin de méthodes privées qui sont quelque peu mal définies dans le but comme find_impl
ou find_detail
ou find_helper
, alors je grince des dents d'une manière similaire.
Ce que je suggère comme alternative, ce sont des fonctions non membres non amis avec un lien interne (déclaré static
ou à l'intérieur d'un espace de noms anonyme) pour aider à implémenter votre classe avec au moins un objectif plus général que "une fonction qui aide à implémenter les autres". Et je peux citer Herb Sutter de C ++ 'Coding Standards' ici pour savoir pourquoi cela peut être préférable d'un point de vue général de SE:
Évitez les frais d'adhésion: dans la mesure du possible, préférez rendre les fonctions non membres non amis. [...] Les fonctions non amis non membres améliorent l'encapsulation en minimisant les dépendances: le corps de la fonction ne peut pas dépendre des membres non publics de la classe (voir le point 11). Ils séparent également les classes monolithiques pour libérer la fonctionnalité séparable, réduisant encore le couplage (voir point 33).
Vous pouvez également comprendre les «frais d'adhésion» dont il parle dans une certaine mesure en termes de principe de base de rétrécissement de la portée variable. Si vous imaginez, comme l'exemple le plus extrême, un objet Dieu qui a tout le code requis pour que votre programme entier s'exécute, alors privilégiez les "assistants" de ce type (fonctions, qu'il s'agisse de fonctions membres ou d'amis) qui peuvent accéder à tous les éléments internes ( privés) d'une classe rendent fondamentalement ces variables non moins problématiques que les variables globales. Vous avez toutes les difficultés à gérer correctement l'état et la sécurité des threads et à maintenir les invariants que vous obtiendriez avec des variables globales dans cet exemple le plus extrême. Et bien sûr, la plupart des exemples réels ne sont, espérons-le, pas proches de cet extrême, mais la dissimulation d'informations n'est utile que car elle limite la portée des informations consultées.
Maintenant, Sutter donne déjà une belle explication ici, mais j'ajouterais également que le découplage a tendance à favoriser comme une amélioration psychologique (du moins si votre cerveau fonctionne comme le mien) en termes de conception des fonctions. Lorsque vous commencez à concevoir des fonctions qui ne peuvent pas accéder à tout dans la classe, sauf uniquement les paramètres pertinents que vous lui transmettez ou, si vous passez l'instance de la classe en tant que paramètre, uniquement ses membres publics, cela tend à promouvoir un état d'esprit de conception qui favorise des fonctions qui ont un objectif plus clair, en plus du découplage et de la promotion d'une encapsulation améliorée, que ce que vous pourriez autrement être tenté de concevoir si vous pouviez simplement accéder à tout.
Si nous revenons aux extrémités, une base de code criblée de variables globales ne tente pas exactement les développeurs de concevoir des fonctions d'une manière claire et généralisée. Très rapidement, plus vous pouvez accéder à des informations dans une fonction, plus nous sommes nombreux, les mortels, à être tentés de la dégénérer et de réduire sa clarté au profit de l'accès à toutes ces informations supplémentaires que nous avons au lieu d'accepter des paramètres plus spécifiques et pertinents pour cette fonction. restreindre son accès à l'État et élargir son applicabilité et améliorer la clarté de ses intentions. Cela s'applique (bien que généralement dans une moindre mesure) aux fonctions des membres ou aux amis.