Paramétrage incertain comme méthode universelle pour construire l'architecture d'application en C ++ et Java pour un minimum. le prix

C ++ est un langage déroutant, et son principal inconvénient est la difficulté de créer des blocs de code isolés. Dans un projet typique, tout dépend de tout. Cet article montre comment écrire du code hautement isolé qui dépend au minimum de bibliothèques spécifiques (y compris standard), d'implémentations, réduisant la dépendance de tout morceau de code à un ensemble d'interfaces. De plus, des solutions architecturales pour le paramétrage du code seront proposées, qui pourraient intéresser non seulement les programmeurs C ++, mais aussi les programmeurs Java. Et ce qui est important, la solution proposée est très économique en termes de temps de développement.

Avertissement : Dans cet article, j'ai rassemblé mes idées sur l'architecture idéale. Certaines idées ne sont pas les miennes (mais je ne me souviens pas à qui), certaines idées sont courantes et connues de tous - ce n'est pas important, car je n'offre pas mes idées sur une bonne architecture, mais un code spécifique qui permettra d'approcher cette architecture à un prix minimum.

Clause de non-responsabilité N2 : Je serai heureux des commentaires constructifs exprimés en mots. Si vous comprenez pire que moi et que vous me grondez, cela signifie que quelque part je n'ai pas suffisamment expliqué, et il est logique de retravailler le texte. Si vous comprenez mieux que moi, cela signifie que je vais acquérir une expérience précieuse. Merci d'avance.

Avertissement N3 : J'ai écrit de grandes applications à partir de zéro, mais je n'ai pas écrit d'applications d'entreprise serveur et client. Tout y est différent et, probablement, mon expérience semblera étrange aux spécialistes de ce domaine. Et l'article ne traite pas de cela, les mêmes problèmes d'évolutivité ne sont pas du tout pris en compte ici.
Clause de non-responsabilité N4 (mise à jour basée sur les commentaires): Certains commentateurs ont suggéré que je réinvente Fowler et propose des modèles de conception connus depuis longtemps. Ce n'est certainement pas le cas. Je propose un tout petit outil de paramétrage qui vous permet d'implémenter ces patterns avec un minimum de gribouillis. Y compris l'injection de dépendances et le localisateur de services de Fowler, mais pas seulement - en utilisant la classe TypedSet, vous pouvez également implémenter un ensemble de stratégies de manière économique. Dans ce cas, Fowler a accédé via des lignes, ce qui est cher - mon outil à coût nul, à coût nul (si absolument strictement, alors log (N) au lieu de 2M * log (N), où M est la longueur de la chaîne de paramètres pour le localisateur de service. après l'apparition de constexpr typeid en c ++ 20, le prix devrait devenir complètement nul). Par conséquent, je vous demande de ne pas étendre la signification de l'article aux modèles de conception. Ici, vous ne trouverez qu'une méthode pour la mise en œuvre bon marché de ces modèles.

Les exemples seront en C ++, mais tout ce qui précède est assez implémentable en Java. Peut-être, au fil du temps, je donnerai du code de travail pour Java si la demande en est dans les commentaires de votre part.

Partie 1. Architecture sphérique dans le vide


Avant de résoudre brillamment toutes les difficultés, vous devez les créer correctement. Créer magistralement des difficultés pour vous-même au bon endroit, vous pouvez grandement faciliter leur solution. Pour cela, nous formulons un objectif pour la solution duquel nous proposerons des méthodes - les principes minimaux d'une bonne architecture.

En fait, la magie d'une bonne architecture n'est que deux principes, et ce qui est écrit ci-dessous n'est qu'un décodage. Le premier principe est la testabilité du code. La testabilité est comme le fil d'Ariane qui vous mène à une bonne architecture. Si vous ne savez pas comment écrire un test de fonctionnalité, vous avez ruiné l'architecture. Si vous ne savez pas comment créer une bonne architecture, pensez à ce que sera le test pour les fonctionnalités que vous avez prévues - et vous créerez automatiquement une barre de qualité architecturale pour vous-même, et assez élevée. Les réflexions sur les tests augmentent automatiquement la modularité, réduisent la connectivité et rendent l'architecture plus logique.

Et je ne parle pas de TDD. Une maladie typique de nombreux programmeurs est le culte religieux des technologies lues quelque part sans comprendre les limites de leur efficacité. TDD est bon lorsque plusieurs programmeurs travaillent sur le code, lorsqu'il y a un service de test et que les autorités comprennent pourquoi de bonnes pratiques de codage sont nécessaires et qu'elle est prête à payer non seulement pour un code qui résout le problème, mais aussi pour sa fiabilité. Si vos supérieurs ne sont pas prêts à payer, vous devrez travailler plus économiquement. Néanmoins, vous devez toujours tester le code - à moins, bien sûr, que vous ayez un sentiment d'auto-préservation.

Le deuxième principe est la modularité. Plus précisément, une modularité hautement isolée sans l'utilisation de bibliothèques / codes durs qui ne sont pas liés au module lui-même. Désormais, lors de la conception d'architectures de serveurs, il est à la mode de diviser un monolithe en microservices. Je vais vous dire un terrible secret - chaque module du monolithe devrait être comme un microservice. Dans le sens où il devrait facilement se démarquer du code général avec un minimum d'en-têtes connectés dans l'environnement de test. Ce n'est pas encore clair, mais je vais vous expliquer avec un exemple: Avez-vous déjà essayé d'allouer shared_ptr à partir d'un boost? Si en même temps vous parvenez à tirer non seulement tout le coup de pouce, mais seulement la moitié de ses matières premières, cela signifie que vous avez tué trois à cinq jours pour éliminer les dépendances inutiles !!! En même temps, vous faites glisser le fait que shared_ptr n'a définitivement rien à voir !!!

Et c'est pire qu'une erreur - c'est un crime architectural.

Avec une bonne architecture, vous devriez être en mesure d'arracher shared_ptr, en remplaçant sans douleur et rapidement tout ce qui n'est pas lié à shared_ptr par des versions de test. Par exemple, une version de test de l'allocateur. Ou oubliez le coup de pouce. Disons que vous écrivez un analyseur xml / html. Vous devez travailler avec des chaînes et travailler avec des fichiers pour l'analyseur. Et si nous parlons d'une architecture idéale qui n'est pas liée aux besoins d'une société de production / logiciel particulière, alors pour un analyseur avec une architecture idéale, nous n'avons pas le droit d'utiliser std :: istream, std :: file_system, std :: string et des opérations de recherche de code dur avec des chaînes dans l'analyseur. Nous devons fournir une interface de flux, une interface pour les opérations sur les fichiers (peut-être divisée en sous-interfaces, mais l'accès aux sous-interfaces devra toujours se faire via l'interface du module d'opérations sur les fichiers), une interface pour travailler avec des chaînes, une interface d'allocateur, et idéalement aussi une interface pour la ligne elle-même. En conséquence, nous pouvons remplacer sans peine tout ce qui n'est pas lié à l'analyse avec des blancs de test, ou insérer une version de test de l'allocateur / travailler avec des fichiers / rechercher des chaînes avec des vérifications supplémentaires. Et la polyvalence de la solution augmentera - demain, sous l'interface du flux, il n'y aura pas un fichier, mais un site quelque part sur Internet, et personne ne le remarquera. Vous pouvez remplacer la bibliothèque standard par Qt, puis basculer vers Visual C ++, puis commencer à utiliser uniquement des choses Linux - et les modifications seront minimes. En tant que spoiler, je dirai qu'avec cette approche, la question du prix se pose en pleine croissance - couvrir tout avec des interfaces, y compris des éléments de la bibliothèque standard, coûte cher, mais ce n'est pas un objectif, mais une solution.

En général, le principe radical module-as-microservice proclamé dans cet article est un point sensible en C ++ et généralement un code plus typique. Si vous créez des fichiers de déclaration et des interfaces distinctes séparément des implémentations, vous pouvez toujours créer l'indépendance / l'isolement des fichiers cpp les uns des autres, puis, relatifs, et non à 100%, les en-têtes sont généralement tissés dans un monolithe solide, à partir duquel rien ne peut être déchiré sans viande. Et bien que cela ait un effet terrible sur le temps de compilation, ça l'est. De plus, même si l'indépendance des rubriques est atteinte, cela signifie automatiquement l'impossibilité de regrouper les classes. En fait, la seule façon d'obtenir l'indépendance des fichiers .cpp et des en-têtes en c ++ est de déclarer les classes pré-utilisées (sans les définir), puis d'utiliser uniquement des pointeurs vers elles. dès que vous utilisez la classe elle-même au lieu du pointeur de classe dans le fichier d'en-tête (c'est-à-dire l'agréger), vous allez créer un tas de tous les .cpp-shniks qui incluent cet en-tête, et ce .cpp-shnik qui contient la définition de classe. Il y a toujours fastpimpl, mais il est juste garanti de créer des dépendances au niveau cpp.

Ainsi, pour une bonne architecture, l'isolement des modules est important - la possibilité de retirer un module avec le premier en-tête connectant les macros et les principaux types de bibliothèque, avec un deuxième en-tête pour les déclarations et plusieurs inclusions reliant un ensemble d'interfaces. Et seulement ce qui se rapporte à cette fonctionnalité, et tout le reste doit être stocké dans d'autres modules et accessible uniquement via des interfaces.

Nous exposons les principales caractéristiques d'une bonne architecture, y compris les points indiqués ci-dessus, point par point.

Définissons le terme «module». Un module est la somme des fonctionnalités logiquement liées. Par exemple, travaillez avec des flux ou des fichiers, ou avec un analyseur html.

Le module «File Work» peut combiner de nombreuses fonctionnalités - ouvrir un fichier, fermer, positionner, lire les propriétés, lire la taille du fichier. Dans le même temps, le scanner de dossiers peut être conçu dans le cadre de l'interface «File Work», ou en tant que module séparé, et le travail avec les flux peut être placé dans un module séparé à coup sûr. Ce qui, cependant, n'interfère pas avec l'organisation indirecte de l'accès à tous les autres modules aux flux et au scanner de dossiers, via le "File Work". Ce n'est pas nécessaire, mais tout à fait logique.

  1. Modularité. "Module-comme-microservice" impératif.
  2. Allocation de 20% du code exécuté 80% du temps dans une bibliothèque séparée - le cœur du programme
  3. Testabilité de chaque fonctionnalité de chaque module
  4. Interface, c'est le manque de hardcode. Vous pouvez uniquement appeler le code physique directement lié aux fonctionnalités du module, et vous devez effectuer les autres appels directs de bibliothèque vers un module distinct et y accéder via l'interface.
  5. Isolation complète du module par des interfaces de l'environnement externe. L'interdiction de «clouer» les implémentations qui ne sont pas liées à la fonctionnalité de la classe. Et plus radicalement, isoler les bibliothèques (y compris les bibliothèques standard) avec des interfaces / adaptateurs / décorateurs
  6. L'agrégation d'une classe ou la création d'une variable de classe ou de fastpimpl n'est utilisée que lorsqu'elle est essentielle aux performances.

Bien sûr, nous trouverons comment réaliser tout cela rapidement pour un prix inférieur, mais je voudrais attirer l'attention sur un autre problème, dont la solution sera un bonus pour nous - le transfert de paramètres dépendants de la plate-forme. Par exemple, si vous devez créer du code qui fonctionne également sur Android et Windows, il sera logique d'allouer des algorithmes dépendants de la plate-forme dans des modules séparés. Dans ce cas, probablement, l'implémentation pour l'androïde peut nécessiter une référence à l'environnement Java (jni), JNIEnv *, et éventuellement à quelques objets Java. Et la mise en œuvre sur Windows peut nécessiter un dossier de travail du programme (qui sur Android peut être demandé au système, ayant JNIEnv *). L'astuce est que le même JNIEnv * n'existe pas dans le contexte Windows, donc même une union typée ou son alternative c ++ à std :: variant est impossible. Vous pouvez bien sûr utiliser le vecteur void * ou le vecteur std :: any comme paramètre, mais honnêtement, il s'agit d'une béquille atypique. Atypique - car il rejette le principal avantage du c ++, le typage fort. Et c'est plus dangereux que le SRAS.

De plus, nous analyserons comment résoudre ce problème de manière strictement typée.

Partie 2. Les balles magiques et leur prix


Supposons donc que nous ayons une grande quantité de code qui doit être écrit à partir de zéro, et le résultat sera un très grand projet.

Comment l'assembler selon les principes que nous avons déterminés?

La méthode classique, approuvée par tous les manuels, consiste à tout diviser en interfaces et stratégies. À l'aide d'interfaces et de stratégies, s'il y en a beaucoup, n'importe quelle sous-tâche de notre projet peut être isolée à tel point que le principe «module-comme-microservice» commence à travailler dessus. Mais mon expérience personnelle est que si vous divisez le projet en 20 à 30 parties, qui seront isolées au niveau du "module en tant que microservice", vous réussirez. Mais la principale caractéristique d'une bonne architecture est la possibilité de tester n'importe quelle classe en dehors du contexte du projet. Et si vous isolez déjà chaque classe, il y a déjà plus de 500 modules, et d'après mon expérience, cela augmente le temps de développement de 3 à 5 fois, ce qui signifie que dans les "conditions de combat" vous ne le ferez pas et vous compromettrez entre prix et qualité.

Quelqu'un peut douter et sera de son propre chef. Faisons une estimation approximative. Laissez la classe moyenne avoir 3-5 membres et 20 fonctions et 3 constructeurs. Plus 6-10 getters et setters (mutateurs) pour l'accès à nos membres. Total d'environ 40 unités dans la classe. Dans un projet typique, chaque classe «centrale» doit avoir accès à une moyenne de cinq fonctionnalités, pas un centre à 3. Par exemple, de très nombreuses classes ont besoin d'un allocateur, d'un système de fichiers, de travailler avec des chaînes, de travailler avec des flux et d'accéder à des bases de données.

Chaque stratégie / interface requiert un membre de type std::shared_ptr<CreateStreamStrategy> m_create_stream; . Deux mutateurs, plus l'initialisation dans chacun des trois constructeurs. plus quelque part dans l'initialisation de notre classe, vous devrez appeler quelque chose comme myclass->SetCreateStreamStrategy( my_create_stream_strategy ) plusieurs fois, pour un total de 8 unités par interface / stratégie, et puisque nous en avons environ cinq, il y aura 40 unités. Autrement dit, nous avons rendu la classe source deux fois plus lourde. Et la perte de simplicité affectera inévitablement la lisibilité, et ailleurs dans le processus de débogage, et demi fois, malgré le fait que rien ne semble avoir fondamentalement changé.

La question est donc. Comment faire de même, mais à un prix minimum? La première chose qui me vient à l'esprit est le paramétrage statique sur les modèles, dans le style d'Alexandrescu et de la bibliothèque Loki.

Nous écrivons une classe avec style

 template < struct Traits > class MyClass { public: void DoMainTaskFunction() { ... MyStream stream = Traits::streamwork::Open( stream_name ); ... } }; 

Cette décision présente tous les avantages architecturaux que nous avons identifiés dans la première partie. Mais il y a aussi beaucoup d'inconvénients.

Moi-même, j'adore me brouiller, mais je regrette pour moi-même, j'avoue: les modèles dans le code ordinaire ne sont aimés que par les magiciens des modèles. Une masse importante de programmeurs avec le mot «modèle» fronce légèrement les sourcils. De plus, dans l'industrie, la grande majorité des avantages ne sont en fait pas des avantages, mais sont légèrement recyclés chez les syshniks c ++ qui n'ont pas une connaissance approfondie des avantages, mais tombent sous le mot «modèle» et font semblant d'être morts.

Si nous traduisons cela dans un langage de production, alors maintenir le code sur le paramétrage statique est plus cher et plus compliqué.

Dans le même temps, si nous voulons, pour une meilleure lisibilité, supprimer soigneusement le corps de la fonction en dehors de la classe, nous obtenons beaucoup de gribouillages avec les noms des modèles et des paramètres de modèle. Et en cas d'erreur de compilation, nous obtenons de longues étagères lisibles par l'homme des causes et des problèmes avec un tas de modèles imbriqués complexes.

Mais, il existe une solution simple. En tant que magicien de modèles, je déclare que presque tout ce qui peut être fait en utilisant le paramétrage statique / le polymorphisme statique peut être transféré au polymorphisme dynamique. Non, bien sûr, nous n'éradiquerons pas le modèle à la fin - mais nous ne le disperserons pas avec une main généreuse pour le paramétrage dans chaque classe, mais nous le limiterons à quelques classes instrumentales.

Troisième partie. La solution proposée et le code encodé pour cette solution


Alors là !!! Rencontrez la classe de modèles TypedSet. Il associe un pointeur intelligent de ce type à un seul type. De plus, pour le type spécifié, il peut avoir un objet, mais il ne peut pas. Je n'aime pas le nom - je serai donc reconnaissant si dans les commentaires me dire une option plus réussie.

Un type - un objet. Mais le nombre de types n'est pas limité! Par conséquent, vous pouvez passer une telle classe en tant que paramétriseur.

Je veux attirer votre attention sur un point. Il peut sembler qu'à un moment donné, vous ayez besoin de deux objets sous une seule interface. En fait, si un tel besoin se fait sentir, alors (à mon avis) cela signifie une erreur architecturale. Autrement dit, si vous avez deux objets sous une interface, ce ne sont plus des interfaces d'accès fonctionnelles: ce sont soit des variables d'entrée pour la fonction, soit vous n'avez pas une mais deux fonctionnalités auxquelles vous devez accéder, alors il vaut mieux diviser l'interface en deux .

Nous ferons trois fonctions de base: Créer, Obtenir et A. En conséquence, la création, la réception et la vérification de la présence d'un élément.

 /// @brief    .      ,    ///           /// class TypedSet { public: template <class TypedElement> void Create( const std::shared_ptr<TypedElement> & value ); template <class TypedElement> std::shared_ptr<TypedElement> Get() const; template <class TypedElement> bool Has() const; size_t GetSize() const { return storage_.size(); } protected: typedef std::map< size_t, std::shared_ptr<void> > Storage; Storage const & storage() const { return storage_; } Storage & get_storage() { return storage_; } private: Storage storage_; }; template <class TypedElement> void TypedSet::Create( const std::shared_ptr<TypedElement> & value ) { size_t hash = typeid(TypedElement).hash_code(); if ( storage().count( hash ) > 0 ) { LogError( "Access Violation" ); return; } std::shared_ptr<void> to_add ( value ); get_storage().insert( std::pair( typeid(TypedElement).hash_code(), to_add ) ); } template <class TypedElement> bool TypedSet::Has() const { size_t hash = typeid(TypedElement).hash_code(); return storage().count( hash ) > 0; } template <class TypedElement> std::shared_ptr<TypedElement> TypedSet::Get() const { size_t hash = typeid(TypedElement).hash_code(); if ( storage().count( hash ) > 0 ) { std::shared_ptr<void> ret( storage().at(hash) ); return std::static_pointer_cast<TypedElement>( ret ); } else { LogError( "Access Violation" ); return std::shared_ptr<TypedElement> (); } } 

Au fait, j'ai vu une solution alternative de collègues écrivant en Qt. Là, l'accès à l'interface souhaitée a été effectué via un singleton, qui a «mappé» l'interface souhaitée, compressée dans Varaint, via une ligne de texte (!!!), et après avoir casté cette option, le résultat a pu être utilisé.

 GlobalConfigurator()["FileSystem"].Get().As<FileSystem>() 

Cela fonctionne certainement, mais le surcoût de compter la longueur et de hacher davantage la chaîne est quelque peu effrayant pour mon âme optimiste. Ici, la surcharge est nulle, car le choix de l'interface souhaitée est effectué au moment de la compilation.

Sur la base de TypedSet, nous pouvons créer la classe StrategiesSet, qui est déjà plus avancée. Nous y stockons non seulement un objet par interface d'accès pour chaque fonctionnalité, mais également pour chaque interface (ci-après dénommée la stratégie) un TypedSet supplémentaire avec des paramètres pour cette stratégie. Je précise: les paramètres, contrairement aux variables de fonction, sont ceux qui sont définis une fois lors de l'initialisation du programme ou une fois pour une exécution de programme volumineuse. Les paramètres vous permettent de rendre le code véritablement multiplateforme. C'est en eux que nous conduisons toute la cuisine dépendante de la plateforme.

Ici, nous aurons des fonctions plus basiques: Create, Get, CreateParamsSet et GetParamsSet. N'a pas été posé, car il est redondant sur le plan architectural: si votre code fait référence à la fonctionnalité de travailler avec le système de fichiers, mais que le code appelant ne l'a pas fourni, vous pouvez uniquement lever une exception ou affirmer, ou faire en sorte que le programme sebukka appelle la fonction abort ().

 class StrategiesSet { public: template <class Strategy> void Create( const std::shared_ptr<Strategy> & value ); template <class Strategy> std::shared_ptr<Strategy> Get(); template <class Strategy> void CreateParamsSet(); template <class Strategy> std::shared_ptr<TypedSet> GetParamsSet(); template <class Strategy, class ParamType> void CreateParam( const std::shared_ptr<ParamType> & value ); template <class Strategy, class ParamType> std::shared_ptr<ParamType> GetParam(); protected: TypedSet const & strategies() const { return strategies_; } TypedSet & get_strategies() { return strategies_; } TypedSet const & params() const { return params_; } TypedSet & get_params() { return params_; } template <class Type> struct ParamHolder { ParamHolder( ) : param_ptr( std::make_shared<TypedSet>() ) {} std::shared_ptr<TypedSet> param_ptr; }; private: TypedSet strategies_; TypedSet params_; }; template <class Strategy> void StrategiesSet::Create( const std::shared_ptr<Strategy> & value ) { get_strategies().Create<Strategy>( value ); } template <class Strategy> std::shared_ptr<Strategy> StrategiesSet::Get() { return get_strategies().Get<Strategy>(); } template <class Strategy> void StrategiesSet::CreateParamsSet( ) { typedef ParamHolder<Strategy> Holder; std::shared_ptr< Holder > ptr = std::make_shared< Holder >( ); ptr->param_ptr = std::make_shared< TypedSet >(); get_params().Create< Holder >( ptr ); } template <class Strategy> std::shared_ptr<TypedSet> StrategiesSet::GetParamsSet() { typedef ParamHolder<Strategy> Holder; if ( get_params().Has< Holder >() ) { return get_params().Get< Holder >()->param_ptr; } else { LogError("StrategiesSet::GetParamsSet : get unexisting!!!"); return std::shared_ptr<TypedSet>(); } } template <class Strategy, class ParamType> void StrategiesSet::CreateParam( const std::shared_ptr<ParamType> & value ) { typedef ParamHolder<Strategy> Holder; if ( !params().Has<Holder>() ) CreateParamsSet<Strategy>(); if ( params().Has<Holder>() ) { std::shared_ptr<TypedSet> params_set = GetParamsSet<Strategy>(); params_set->Create<ParamType>( value ); } else { LogError( "Param creating error: Access Violation" ); } } template <class Strategy, class ParamType> std::shared_ptr<ParamType> StrategiesSet::GetParam() { typedef ParamHolder<Strategy> Holder; if ( params().Has<Holder>() ) { return GetParamsSet<Strategy>()->template Get<ParamType>(); //   template          .    . } else { LogError( "Access Violation" ); return std::shared_ptr<ParamType> (); } } 

Un autre avantage supplémentaire est qu'au stade du prototypage, vous pouvez créer une classe de typage très grande, y créer un accès à tous les modules et le passer à tous les modules en tant que paramètre, devenir rapidement petit, puis le diviser en morceaux qui sont minimalement nécessaires pour chaque module.

Eh bien, et un cas d'utilisation petit et (encore) trop simplifié. J'espère que vous me suggérerez dans les commentaires ce que vous aimeriez voir comme un exemple simple, et je ferai de l'article une petite mise à jour. Comme le dit la sagesse de la programmation populaire, «lancez le plus tôt possible et améliorez l'utilisation des commentaires après la sortie».

 class Interface1 { public: virtual void Fun() { printf("\niface1\n");} virtual ~Interface1() {} }; class Interface2 { public: virtual void Fun() { printf("\niface2\n");} virtual ~Interface2() {} }; class Interface3 { public: virtual void Fun() { printf("\niface3\n");} virtual ~Interface3() {} }; class Implementation1 : public Interface1 { public: virtual void Fun() override { printf("\nimpl1\n");} }; class Implementation2 : public Interface2 { public: virtual void Fun() override { printf("\nimpl2\n");} }; class PrintParams { public: virtual ~PrintParams() {} virtual std::string GetOs() = 0; }; class PrintParamsUbuntu : public PrintParams { public: virtual std::string GetOs() override { return "Ubuntu"; } }; class PrintParamsWindows : public PrintParams { public: virtual std::string GetOs() override { return "Windows"; } }; class PrintStrategy { public: virtual ~PrintStrategy() {} virtual void operator() ( const TypedSet& params, const std::string & str ) = 0; }; class PrintWithOsStrategy : public PrintStrategy { public: virtual void operator()( const TypedSet& params, const std::string & str ) override { auto os = params.Get< PrintParams >()->GetOs(); printf(" Printing: %s (OS=%s)", str.c_str(), os.c_str() ); } }; void TestTypedSet() { using namespace std; TypedSet a; a.Create<Interface1>( make_shared<Implementation1>() ); a.Create<Interface2>( make_shared<Implementation2>() ); a.Get<Interface1>()->Fun(); a.Get<Interface2>()->Fun(); Log("Double creation:"); a.Create<Interface1>( make_shared<Implementation1>() ); Log("Get unexisting:"); a.Get<Interface3>(); } void TestStrategiesSet() { using namespace std; StrategiesSet printing; printing.Create< PrintStrategy >( make_shared<PrintWithOsStrategy>() ); printing.CreateParam< PrintStrategy, PrintParams >( make_shared<PrintParamsWindows>() ); auto print_strategy_ptr = printing.Get< PrintStrategy >(); auto & print_strategy = *print_strategy_ptr; auto & print_params = *printing.GetParamsSet< PrintStrategy >(); print_strategy( print_params, "Done!" ); } int main() { TestTypedSet(); TestStrategiesSet(); return 0; } 

Résumé


Ainsi, nous avons résolu un problème important: nous n'avons laissé dans la classe que l'interface directement liée aux fonctionnalités de la classe. Le reste a été «poussé» dans le StrategiesSet, tout en évitant d'encombrer la classe avec des éléments inutiles et de «clouer» certains algorithmes de la fonctionnalité requise aux algorithmes. Cela nous permettra non seulement d'écrire du code hautement isolé, sans aucune dépendance vis-à-vis des implémentations et des bibliothèques, mais également de gagner un temps considérable.

Le code de l'exemple et des classes d'outils peut être trouvé ici.

Upd. à partir du 13/11/2019
En fait, le code présenté ici n'est qu'un exemple simplifié de lisibilité. Le fait est que typeid (). Hash_code est implémenté dans les compilateurs modernes lentement et de manière inefficace. Son utilisation tue une grande partie du sens. De plus, comme le suggère le respecté 0xd34df00d , la norme ne garantit pas la possibilité de distinguer les types par hashcode (en pratique, cette approche fonctionne cependant). Mais l'exemple est bien lu. J'ai réécrit TypedSet sans typeid (). De plus, Hash_code () a remplacé map par array (mais avec la possibilité de passer rapidement de map à array et vice versa en changeant un chiffre dans #if). Cela s'est avéré plus difficile, mais plus intéressant pour une utilisation pratique.
à coliru
 namespace metatype { struct Counter { size_t GetAndIncrease() { return counter_++; } private: size_t static inline counter_ = 1; }; template <typename Type> struct HashGetterBody { HashGetterBody() : hash_( counter_.GetAndIncrease() ) { } size_t GetHash() { return hash_; } private: Counter counter_; size_t hash_; }; template <typename Type> struct HashGetter { size_t GetHash() {return hasher_.GetHash(); } private: static inline HashGetterBody<Type> hasher_; }; } // namespace metatype template <typename Type> size_t GetTypeHash() { return metatype::HashGetter<Type>().GetHash(); } namespace details { #if 1 //   ,        () class TypedSetStorage { public: static inline const constexpr size_t kMaxTypes = 100; typedef std::array< std::shared_ptr<void>, kMaxTypes > Storage; void Set( size_t hash_index, const std::shared_ptr<void> & value ) { ++size_; assert( hash_index < kMaxTypes ); // too many types data_[hash_index] = value; } std::shared_ptr<void> & Get( size_t hash_index ) { assert( hash_index < kMaxTypes ); return data_[hash_index]; } const std::shared_ptr<void> & Get( size_t hash_index ) const { if ( hash_index >= kMaxTypes ) return empty_ptr_; return data_[hash_index]; } bool Has( size_t hash_index ) const { if ( hash_index >= kMaxTypes ) return 0; return (bool)data_[hash_index]; } size_t GetSize() const { return size_; } private: Storage data_; size_t size_ = 0; static const inline std::shared_ptr<void> empty_ptr_; }; #else //    ,        (std::map) class TypedSetStorage { public: typedef std::map< size_t, std::shared_ptr<void> > Storage; void Set( size_t hash_index, const std::shared_ptr<void> & value ) { data_[hash_index] = value; } std::shared_ptr<void> & Get( size_t hash_index ) { return data_[hash_index]; } const std::shared_ptr<void> & Get( size_t hash_index ) const { return data_.at(hash_index); } bool Has( size_t hash_index ) const { return data_.count(hash_index) > 0; } size_t GetSize() const { return data_.size(); } private: Storage data_; }; #endif } // namespace details /// @brief    .      ,    ///           /// class TypedSet { public: template <class TypedElement> void Create( const std::shared_ptr<TypedElement> & value ); template <class TypedElement> std::shared_ptr<TypedElement> Get() const; template <class TypedElement> bool Has() const; size_t GetSize() const { return storage_.GetSize(); } protected: typedef details::TypedSetStorage Storage; Storage const & storage() const { return storage_; } Storage & get_storage() { return storage_; } private: Storage storage_; }; template <class TypedElement> void TypedSet::Create( const std::shared_ptr<TypedElement> & value ) { size_t hash = GetTypeHash<TypedElement>(); if ( storage().Has( hash ) ) { LogError( "Access Violation" ); return; } std::shared_ptr<void> to_add ( value ); get_storage().Set( hash, to_add ); } template <class TypedElement> bool TypedSet::Has() const { size_t hash = GetTypeHash<TypedElement>(); return storage().Has( hash ); } template <class TypedElement> std::shared_ptr<TypedElement> TypedSet::Get() const { size_t hash = GetTypeHash<TypedElement>(); if ( storage().Has( hash ) ) { std::shared_ptr<void> ret( storage().Get( hash ) ); return std::static_pointer_cast<TypedElement>( ret ); } else { LogError( "Access Violation" ); return std::shared_ptr<TypedElement> (); } } 

Ici, l'accès est effectué en temps linéaire, les hachages de type sont comptés avant le lancement de main (), les pertes sont uniquement pour les contrôles de validation, qui peuvent être supprimés si vous le souhaitez.

Source: https://habr.com/ru/post/fr475268/


All Articles