La plupart des projets auxquels je participe utilisent plusieurs composants open source. En règle générale, est-ce une bonne idée de toujours éviter de lier tous les composants du code aux bibliothèques tierces et de passer plutôt par un wrapper encapsulant pour éviter les inconvénients du changement?
Par exemple, la plupart de nos projets PHP utilisent directement log4php en tant que framework de journalisation, c'est-à-dire qu'ils instancient via \ Logger :: getLogger (), qu'ils utilisent les méthodes -> info () ou -> warn (), etc. Cependant, un cadre de journalisation hypothétique peut apparaître, ce qui est préférable d'une certaine manière. Dans l'état actuel des choses, tous les projets étroitement liés à la méthode log4php devraient être modifiés, à des dizaines d'endroits, pour s'adapter aux nouvelles signatures. Ceci aurait évidemment un impact important sur la base de code et tout changement est un problème potentiel.
Afin de garantir la pérennité des nouvelles bases de code de ce type de scénario, je considère souvent (et implémente parfois) une classe wrapper pour encapsuler la fonctionnalité de journalisation et permettre plus facilement, bien que non infaillible, de modifier le fonctionnement futur de la journalisation. ; le code appelle le wrapper, le wrapper passe l'appel au framework de journalisation du jour .
Sachant qu'il y a des exemples plus compliqués avec d'autres bibliothèques, est-ce que je fais trop d'ingénierie ou est-ce une sage précaution dans la plupart des cas?
EDIT: Plus de considérations - l’utilisation de l’injection de dépendance et des doublons de test nécessite pratiquement de résumer la plupart des API ("Je veux vérifier que mon code s’exécute et met à jour son état, mais pas écrire un commentaire de journal / accéder à une vraie base de données"). N'est-ce pas un décideur?
la source
Réponses:
Si vous utilisez uniquement un petit sous-ensemble de l'API tierce, il est judicieux d'écrire un wrapper. Cela facilite l'encapsulation et le masquage des informations, vous évitant d'exposer une API potentiellement énorme à votre propre code. Cela peut également vous aider à vous assurer que toute fonctionnalité que vous ne voulez pas utiliser est "cachée".
Une autre bonne raison pour un wrapper est si vous envisagez de changer la bibliothèque tierce. S'il s'agit d'un élément d'infrastructure dont vous savez qu'il ne changera pas, n'écrivez pas de wrapper pour cela.
la source
Sans savoir quelles super-nouvelles fonctionnalités ce futur supposé enregistreur amélioré aura, comment écririez-vous le wrapper? Le choix le plus logique est que votre wrapper instancie une sorte de classe de consignateur et utilise des méthodes telles que
->info()
ou->warn()
. En d'autres termes, essentiellement identique à votre API actuelle.Plutôt que du code à l'épreuve du temps que je n'aurai peut-être jamais besoin de changer, ou qui peut nécessiter de toute façon une réécriture inévitable, je préfère le code "à l'épreuve des années". C'est, dans les rares occasions où je ne change considérablement d' un composant, qui est quand j'écris un emballage pour le rendre compatible avec le code passé. Cependant, tout nouveau code utilise la nouvelle API et je refacture l'ancien code pour l'utiliser chaque fois que je modifie le même fichier, ou lorsque le calendrier le permet. Après quelques mois, je peux retirer l'emballage et le changement a été progressif et robuste.
En d'autres termes, les wrappers n'ont vraiment de sens que si vous connaissez déjà toutes les API que vous devez wrapper. De bons exemples sont si votre application doit actuellement prendre en charge de nombreux pilotes de base de données, systèmes d'exploitation ou versions de PHP.
la source
En encapsulant une bibliothèque tierce, vous ajoutez une couche supplémentaire d’abstraction. Cela présente quelques avantages:
Votre base de code devient plus flexible aux changements
Si vous devez remplacer la bibliothèque par une autre, il vous suffit de modifier votre implémentation dans votre wrapper - à un endroit . Vous pouvez modifier l'implémentation du wrapper sans rien changer d'autre, autrement dit, vous avez un système faiblement couplé. Sinon, vous devrez parcourir tout votre code et apporter des modifications partout - ce qui n'est évidemment pas ce que vous voulez.
Vous pouvez définir l'API du wrapper indépendamment de l'API de la bibliothèque.
Différentes bibliothèques peuvent avoir des API très différentes et en même temps aucune d’entre elles ne correspond exactement à vos besoins. Que se passe-t-il si une bibliothèque a besoin d'un jeton à transmettre avec chaque appel? Vous pouvez faire passer le jeton dans votre application partout où vous avez besoin d'utiliser la bibliothèque ou la sécuriser quelque part de manière plus centralisée, mais dans tous les cas, vous avez besoin du jeton. Votre classe de wrapper simplifie à nouveau tout cela, car vous pouvez simplement conserver le jeton dans votre classe de wrapper, ne jamais l'exposer à aucun composant de votre application et en supprimer complètement la nécessité. Un énorme avantage si vous avez déjà utilisé une bibliothèque qui ne met pas l’accent sur une bonne conception d’API.
Le test unitaire est beaucoup plus simple
Les tests unitaires ne devraient tester qu'une seule chose. Si vous voulez tester une classe, vous devez vous moquer de ses dépendances. Cela devient encore plus important si cette classe passe des appels réseau ou accède à une autre ressource en dehors de votre logiciel. En encapsulant la bibliothèque tierce, il est facile de simuler ces appels et de renvoyer des données de test ou tout autre élément requis par le test unitaire. Si vous n’avez pas une telle couche d’abstraction, il devient beaucoup plus difficile de le faire - et la plupart du temps, cela produit beaucoup de code laid.
Vous créez un système faiblement couplé
Les modifications apportées à votre enveloppe n'ont aucun effet sur les autres parties de votre logiciel, du moins tant que vous ne modifiez pas le comportement de votre enveloppe. En introduisant une couche d'abstraction comme celle-ci, vous pouvez simplifier les appels vers la bibliothèque et supprimer presque complètement la dépendance de votre application à cette bibliothèque. Votre logiciel utilisera simplement l'encapsuleur et cela ne changera rien à la façon dont l'encapsuleur est implémenté ou comment il fait ce qu'il fait.
Exemple pratique
Soyons honnêtes. Les gens peuvent discuter pendant des heures des avantages et des inconvénients d’une chose de la sorte - c’est pourquoi je préfère vous montrer un exemple.
Disons que vous avez une sorte d'application Android et que vous devez télécharger des images. Il existe de nombreuses bibliothèques qui facilitent grandement le chargement et la mise en cache des images, par exemple Picasso ou Universal Image Loader .
Nous pouvons maintenant définir une interface que nous allons utiliser pour envelopper quelle que soit la bibliothèque utilisée:
C'est l'interface que nous pouvons maintenant utiliser dans l'application chaque fois que nous devons charger une image. Nous pouvons créer une implémentation de cette interface et utiliser l’injection de dépendance pour injecter une instance de cette implémentation partout où nous utilisons le
ImageService
.Disons que nous avons initialement décidé d'utiliser Picasso. Nous pouvons maintenant écrire une implémentation pour
ImageService
laquelle Picasso utilise en interne:Assez simple si vous me demandez. Wrapper autour des bibliothèques n'a pas besoin d'être compliqué pour être utile. L’interface et l’implémentation ont moins de 25 lignes de code combinées, ce qui a été un effort de création, mais nous gagnons déjà quelque chose en le faisant. Voir le
Context
champ dans la mise en œuvre? Le framework d'injection de dépendance de votre choix se chargera déjà d'injecter cette dépendance avant même que nous utilisions notreImageService
, votre application n'a plus à se soucier de la façon dont les images sont téléchargées ni des dépendances éventuelles de la bibliothèque. Tout ce que votre application voit est unImageService
et quand il a besoin d'une image, il appelleload()
avec une URL - simple et direct.Cependant, le véritable avantage vient lorsque nous commençons à changer les choses. Imaginez que nous devions maintenant remplacer Picasso par Universal Image Loader, car Picasso ne prend pas en charge certaines fonctionnalités dont nous avons absolument besoin. Devons-nous maintenant passer au crible notre base de code, et remplacer fastidieusement tous les appels de Picasso, puis traiter des dizaines d’erreurs de compilation, car nous avons oublié quelques appels à Picasso? Non. Tout ce que nous avons à faire est de créer une nouvelle implémentation
ImageService
et d'indiquer à notre infrastructure d'injection de dépendance d'utiliser cette implémentation à partir de maintenant:Comme vous pouvez le constater, la mise en œuvre peut être très différente, mais cela n’a aucune importance. Nous n'avons pas eu à modifier une seule ligne de code ailleurs dans notre application. Nous utilisons une bibliothèque complètement différente qui peut avoir des fonctionnalités complètement différentes ou peut être utilisée de manière très différente, mais notre application ne s'en soucie tout simplement pas. Comme auparavant, le reste de notre application ne voit que l'
ImageService
interface avec saload()
méthode. Cependant, cette méthode n'est pas utilisée.Au moins pour moi tout cela sonne déjà déjà bien, mais attendez! Il y a encore plus. Imaginez que vous écrivez des tests unitaires pour une classe sur laquelle vous travaillez et que cette classe utilise le
ImageService
. Bien sûr, vous ne pouvez pas laisser vos tests unitaires faire des appels réseau vers une ressource située sur un autre serveur, mais puisque vous utilisez maintenant le,ImageService
vous pouvez facilement laisserload()
renvoyer une statiqueBitmap
utilisée pour les tests unitaires en implémentant un modèle fictifImageService
:Pour résumer en encapsulant des bibliothèques tierces, votre base de code devient plus souple aux changements, plus simple, plus facile à tester et vous réduisez le couplage des différents composants de votre logiciel - autant de choses qui deviennent de plus en plus importantes plus la maintenance d'un logiciel est longue.
la source
Je pense qu'emballer des bibliothèques tierces aujourd'hui au cas où quelque chose de mieux se présenterait demain est une violation très fastidieuse de YAGNI. Si vous appelez à plusieurs reprises du code tiers d'une manière particulière à votre application, vous devrez (devriez) refactoriser ces appels dans une classe d'habillage pour éliminer la répétition. Sinon, vous utilisez pleinement l'API de la bibliothèque et tout wrapper ressemblerait à la bibliothèque elle-même.
Supposons maintenant qu'une nouvelle bibliothèque apparaisse avec une performance supérieure ou autre. Dans le premier cas, il vous suffit de réécrire l'encapsuleur pour la nouvelle API. Aucun problème.
Dans le second cas, vous créez un wrapper adaptant l'ancienne interface au lecteur de la nouvelle bibliothèque. Un peu plus de travail, mais pas de problème, et pas plus de travail que si vous aviez écrit le wrapper plus tôt.
la source
La raison fondamentale pour écrire un wrapper autour d'une bibliothèque tierce est que vous pouvez échanger cette bibliothèque tierce sans changer le code qui l'utilise. Vous ne pouvez pas éviter le couplage à quelque chose. L'argument va donc selon lequel il est préférable de coupler à une API que vous avez écrite.
Que cela en vaille la peine est une autre histoire. Ce débat continuera probablement pendant longtemps.
Pour les petits projets, où la probabilité qu'un tel changement soit nécessaire est faible, il s'agit probablement d'un effort inutile. Pour les projets de grande envergure, cette flexibilité peut bien compenser les efforts supplémentaires déployés pour emballer la bibliothèque. Cependant, il est difficile de savoir si tel est le cas auparavant.
Une autre façon de voir les choses est le principe de base qui consiste à résumer ce qui est susceptible de changer. Donc, si la bibliothèque tierce est bien établie et qu’elle ne risque pas d’être modifiée, il peut être judicieux de ne pas l’emballer. Cependant, si la bibliothèque tierce est relativement nouvelle, il y a plus de chance qu'elle doive être remplacée. Cela dit, le développement de bibliothèques établies a été abandonné à maintes reprises. Donc, ce n’est pas une question facile à répondre.
la source
En plus de ce que @Oded a déjà dit, j'aimerais ajouter cette réponse uniquement dans le but de se connecter.
J'ai toujours une interface pour la journalisation mais je n'ai jamais encore eu à remplacer un
log4foo
framework.Cela ne prend qu'une demi-heure pour fournir l'interface et écrire le wrapper, donc je suppose que vous ne perdez pas trop de temps si cela s'avère inutile.
C'est un cas particulier de YAGNI. Bien que je n'en ai pas besoin, cela ne prend pas beaucoup de temps et je me sens plus en sécurité avec cela. Si le jour de l'échange de l'enregistreur arrive vraiment, je serai heureux d'avoir investi une demi-heure, car cela me fera économiser plus d'une journée d'échange d'appels dans un projet réel. Et je n'ai jamais écrit ni vu de test unitaire pour la journalisation (mis à part les tests pour l'implémentation de l'enregistreur lui-même), attendez-vous à des défauts sans wrapper.
la source
Je traite de cette question précise sur un projet sur lequel je travaille actuellement. Mais dans mon cas, la bibliothèque est dédiée aux graphiques et je suis donc en mesure de limiter son utilisation à un petit nombre de classes qui traitent des graphiques, au lieu de l’éparpiller tout au long du projet. Ainsi, il est assez facile de changer d'API plus tard si j'en ai besoin. dans le cas d'un bûcheron, la question devient beaucoup plus compliquée.
Ainsi, je dirais que la décision a beaucoup à voir avec ce que fait exactement la bibliothèque tierce partie et combien il serait difficile de la changer. Si changer tous les appels d'API serait facile malgré tout, alors ça ne vaut probablement pas la peine de le faire. Si toutefois changer la bibliothèque plus tard serait vraiment difficile, je le referais probablement maintenant.
Au-delà de cela, d’autres réponses ont très bien répondu à la question principale, je souhaite donc me concentrer sur le dernier ajout, à savoir l’injection de dépendance et les objets fictifs. Cela dépend bien sûr de la manière dont fonctionne exactement votre structure de journalisation, mais dans la plupart des cas, cela ne nécessiterait pas d'encapsuleur (même s'il en bénéficierait probablement). Assurez-vous que l'API de votre objet fictif soit identique à celui de la bibliothèque tierce et vous pourrez ensuite facilement permuter l'objet fictif à des fins de test.
Dans ce cas, le principal facteur est de savoir si la bibliothèque tierce est implémentée même via une injection de dépendance (ou un localisateur de service ou un tel modèle faiblement couplé). Si les fonctions de la bibliothèque sont accessibles via un singleton, des méthodes statiques ou quelque chose de ce type, vous devrez envelopper le tout dans un objet que vous pourrez utiliser lors de l'injection de dépendances.
la source
Je suis fermement dans le camp de l'emballage et non pas dans le fait de pouvoir remplacer la bibliothèque tierce par la plus grande priorité (bien que ce soit un bonus). Mon principal argument en faveur de l'emballage est simple
Et cela se manifeste, généralement, sous la forme d'une charge de duplication de code, telle que les développeurs écrivant 8 lignes de code juste pour créer un
QButton
et le styler comme il se doit pour l'application, mais uniquement pour le concepteur. mais aussi la fonctionnalité des boutons pour changer complètement pour tout le logiciel, ce qui finit par nécessiter de revenir en arrière et de réécrire des milliers de lignes de code, ou de constater que la modernisation d'un pipeline de rendu nécessite une réécriture épique, car la base de code saupoudrait de bas niveau fixe ad-hoc fixe. pipeline code OpenGL partout au lieu de centraliser une conception de rendu en temps réel et de laisser l'utilisation de OGL strictement pour sa mise en œuvre.Ces conceptions ne sont pas adaptées à nos besoins de conception spécifiques. Ils ont tendance à offrir un vaste ensemble de ce qui est réellement nécessaire (et ce qui ne fait pas partie d’un projet est aussi important, sinon plus, que ce qui est en réalité), et leurs interfaces ne sont pas conçues pour répondre spécifiquement à nos besoins dans un environnement de haut niveau. pensée = une requête ", ce qui nous prive de tout contrôle de conception central si nous les utilisons directement. Si les développeurs finissent par écrire du code de niveau inférieur à ce qu'il devrait être nécessaire pour exprimer ce dont ils ont besoin, ils peuvent parfois le résumer eux-mêmes de manière ad-hoc, ce qui permet de se retrouver avec des dizaines de lettres écrites à la hâte et grossières. des emballages conçus et documentés au lieu d’un emballage bien conçu et bien documenté.
Bien sûr, j'appliquerais de fortes exceptions aux bibliothèques dont les wrappers sont des traductions presque uniques de ce que les API tierces ont à offrir. Dans ce cas, il pourrait ne pas être recherché de conception de niveau supérieur exprimant plus directement les exigences commerciales et de conception (tel pourrait être le cas pour quelque chose qui ressemble davantage à une bibliothèque "utilitaire"). Mais s'il existe une conception beaucoup plus personnalisée disponible qui exprime beaucoup plus directement nos besoins, alors je suis tout à fait dans le camp de l'habillage, tout comme je suis fortement en faveur de l'utilisation d'une fonction de niveau supérieur et de la réutilisation par-dessus le code d'assemblage en ligne. partout.
Bizarrement, je me suis heurté aux développeurs d'une manière qui leur semblait si méfiante et si pessimiste quant à notre capacité à concevoir, par exemple, une fonction permettant de créer un bouton et de le renvoyer, préférant écrire 8 lignes de code de niveau inférieur axé sur la microscopie. les détails de la création de boutons (qui devaient éventuellement être modifiés à l'avenir) lors de la conception et de l'utilisation de cette fonction. Je ne vois même pas pourquoi nous essayons de concevoir quoi que ce soit si nous ne pouvons pas nous fier à nous-mêmes pour concevoir ce type d’emballage de manière raisonnable.
En d'autres termes, je considère les bibliothèques tierces comme des moyens de gagner un temps considérable dans la mise en œuvre, et non comme des substituts à la conception de systèmes.
la source
Mon idée sur les bibliothèques tierces:
La communauté iOS a récemment discuté des avantages et inconvénients (OK, principalement des inconvénients) de l'utilisation de dépendances tierces. De nombreux arguments que j'ai vus étaient plutôt génériques - regroupant toutes les bibliothèques tierces dans un même panier. Comme avec la plupart des choses, cependant, ce n'est pas si simple. Alors, essayons de nous concentrer sur un seul cas
Devrions-nous éviter d'utiliser des bibliothèques d'interface utilisateur tierces?
Raisons pour envisager des bibliothèques tierces:
Il semble y avoir deux raisons principales pour lesquelles les développeurs envisagent d’utiliser une bibliothèque tierce:
La plupart des bibliothèques d'interface utilisateur ( pas toutes! ) Ont tendance à tomber dans la deuxième catégorie. Ce matériel n’est pas sournois, mais il faut du temps pour le construire correctement.
Il existe à peu près deux types de contrôles / vues:
UICollectionView
depuisUIKit
.UIPickerView
. La plupart des bibliothèques tierces appartiennent généralement à la deuxième catégorie. De plus, ils sont souvent extraits d'une base de code existante pour laquelle ils ont été optimisés.Hypothèses précoces inconnues
De nombreux développeurs vérifient le code interne de leur code, mais prennent peut-être pour acquis la qualité du code source tiers. Cela vaut la peine de passer un peu de temps à parcourir le code d'une bibliothèque. Vous pourriez finir par être surpris de voir des drapeaux rouges, par exemple utiliser du grésillement là où il n’est pas nécessaire.
Vous ne pouvez pas le cacher
En raison de la manière dont UIKit est conçu, vous ne pourrez probablement pas cacher la bibliothèque d'interface utilisateur tierce, par exemple derrière un adaptateur. Une bibliothèque entremêlera avec votre code d'interface utilisateur devenant de facto de votre projet.
Coût du temps futur
UIKit change avec chaque version iOS. Les choses vont casser. Votre dépendance vis-à-vis de tiers ne sera pas aussi sans entretien que prévu.
Conclusion :
D'après mon expérience personnelle, la plupart des utilisations du code d'interface utilisateur tiers se résument à l'échange d'une plus petite flexibilité contre un gain de temps.
Nous utilisons du code prêt à l'emploi pour expédier notre version actuelle plus rapidement. Tôt ou tard, cependant, nous atteignons les limites de la bibliothèque et nous nous trouvons devant une décision difficile: que faire ensuite?
la source
L'utilisation directe de la bibliothèque est plus conviviale pour l'équipe de développeurs. Lorsqu'un nouveau développeur rejoint le groupe, il peut être pleinement familiarisé avec tous les frameworks utilisés, sans pour autant être en mesure de contribuer de manière productive avant d'apprendre votre propre API. Lorsqu'un jeune développeur tente de progresser dans votre groupe, il est forcé d'apprendre que votre API spécifique n'est présente nulle part ailleurs, au lieu d'acquérir des compétences génériques plus utiles. Si quelqu'un connaît des fonctionnalités utiles ou des possibilités de l'API d'origine, il peut ne pas être en mesure d'atteindre la couche écrite par quelqu'un qui ne le savait pas. Si quelqu'un souhaite exécuter une tâche de programmation tout en cherchant un emploi, il risque de ne pas être en mesure de démontrer les éléments de base qu'il a utilisés plusieurs fois, simplement parce qu'il accédait à ces fonctionnalités par le biais de votre wrapper.
Je pense que ces problèmes peuvent être plus importants que la possibilité plutôt lointaine d'utiliser une bibliothèque complètement différente plus tard. Le seul cas où j'utiliserais un wrapper est lorsque la migration vers une autre implémentation est définitivement planifiée ou que l'API encapsulée n'est pas suffisamment gelée et continue à changer.
la source