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 par un encapsuleur pour éviter la douleur du changement?
Par exemple, la plupart de nos projets PHP utilisent directement log4php comme cadre de journalisation, c'est-à-dire qu'ils instancient via\Logger :: getLogger (), ils utilisent -> info () ou -> warn ( ), mais à l'avenir, un cadre de journalisation hypothétique peut apparaître, ce qui est mieux d'une certaine manière. En l'état actuel, tous les projets qui s'apparient étroitement aux signatures de la méthode log4php devraient changer, dans des dizaines d'endroits, dans afin d'adapter les nouvelles signatures, ce qui aurait évidemment un impact important sur la base de code et tout changement est un problème potentiel.
Pour de nouvelles bases de code à l'épreuve du temps de ce type de scénario, je considère souvent (et parfois implémente) une classe wrapper pour encapsuler la fonctionnalité de journalisation et faciliter, mais pas à toute épreuve, la modification de la façon dont la journalisation fonctionne à l'avenir avec un minimum de changements ; le code appelle le wrapper, le wrapper passe l'appel au framework de journalisation du jour.
Sachant qu'il existe des exemples plus compliqués avec d'autres bibliothèques, suis-je en train de faire de l'ingénierie excessive ou s'agit-il d'une sage précaution dans la plupart des cas?
EDIT: Plus de considérations - l'utilisation de l'injection de dépendances et du test double nécessite pratiquement que nous résumions la plupart des API de toute façon ("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?
Si vous n'utilisez qu'un petit sous-ensemble de l'API tierce, il est logique d'écrire un wrapper - cela aide à l'encapsulation et au masquage des informations, vous assurant de ne pas exposer une API potentiellement énorme à votre propre code. Cela peut également vous aider à vous assurer que toutes les fonctionnalités que vous ne pas voulez utiliser sont "cachées".
Une autre bonne raison pour un wrapper est si vous attendez pour changer la bibliothèque tierce. S'il s'agit d'une infrastructure que vous savez vous ne changerez pas, n'écrivez pas de wrapper pour cela.
En enveloppant une bibliothèque tierce, vous ajoutez une couche d'abstraction supplémentaire par-dessus. Cela présente quelques avantages:
Si vous avez besoin de remplacer la bibliothèque par une autre, il vous suffit de modifier votre implémentation dans votre wrapper - en un seul endroit . Vous pouvez changer l'implémentation de l'encapsuleur et ne pas avoir à changer quoi que ce soit d'autre, en d'autres termes, vous avez un système faiblement couplé. Sinon, vous devrez parcourir l'intégralité de votre base de code et apporter des modifications partout - ce qui n'est évidemment pas ce que vous voulez.
Différentes bibliothèques peuvent avoir des API très différentes et en même temps, aucune d'entre elles ne peut être exactement ce dont vous avez besoin. Que se passe-t-il si une bibliothèque a besoin d'un jeton à transmettre à chaque appel? Vous pouvez passer le jeton dans votre application partout où vous devez utiliser la bibliothèque ou vous pouvez le sécuriser quelque part de manière plus centrale, mais dans tous les cas, vous avez besoin du jeton. Votre classe wrapper simplifie à nouveau tout cela, car vous pouvez simplement conserver le jeton dans votre classe wrapper, sans jamais l'exposer à aucun composant de votre application et en supprimer complètement le besoin. Un énorme avantage si vous avez déjà utilisé une bibliothèque qui ne met pas l'accent sur une bonne conception d'API.
Les tests unitaires ne devraient tester qu'une seule chose. Si vous voulez tester unitaire une classe, vous devez vous moquer de ses dépendances. Cela devient encore plus important si cette classe effectue 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 se moquer de ces appels et de renvoyer des données de test ou tout ce que ce test unitaire requiert. Si vous n'avez pas une telle couche d'abstraction, cela devient beaucoup plus difficile à faire - et la plupart du temps, cela entraîne beaucoup de code laid.
Les modifications apportées à votre wrapper n'ont aucun effet sur les autres parties de votre logiciel - du moins tant que vous ne modifiez pas le comportement de votre wrapper. En introduisant une couche d'abstraction comme ce wrapper, vous pouvez simplifier les appels à la bibliothèque et supprimer presque complètement la dépendance de votre application sur cette bibliothèque. Votre logiciel utilisera simplement l'encapsuleur et cela ne fera aucune différence sur la façon dont l'encapsuleur est implémenté ou comment il fait ce qu'il fait.
Soyons honnêtes. Les gens peuvent discuter des avantages et des inconvénients de quelque chose comme ça pendant des heures - 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 un tas de bibliothèques qui rendent le chargement et la mise en cache des images un jeu d'enfant par exemple Picasso ou le Universal Image Loader .
Nous pouvons maintenant définir une interface que nous allons utiliser pour envelopper la bibliothèque que nous finirons par utiliser:
public interface ImageService {
Bitmap load(String url);
}
Il s'agit de l'interface que nous pouvons désormais 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épendances pour injecter une instance de cette implémentation partout où nous utilisons le ImageService
.
Disons que nous décidons initialement d'utiliser Picasso. Nous pouvons maintenant écrire une implémentation pour ImageService
qui utilise Picasso en interne:
public class PicassoImageService implements ImageService {
private final Context mContext;
public PicassoImageService(Context context) {
mContext = context;
}
@Override
public Bitmap load(String url) {
return Picasso.with(mContext).load(url).get();
}
}
Assez simple si vous me demandez. L'enroulement 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, donc cela n'a pratiquement pas été un effort pour le créer, mais nous gagnons déjà quelque chose en faisant cela. Voir le champ Context
dans l'implémentation? L'infrastructure d'injection de dépendances de votre choix se chargera déjà d'injecter cette dépendance avant que nous n'utilisions jamais notre ImageService
, votre application n'a plus à se soucier de la façon dont les images sont téléchargées et des dépendances que peut avoir la bibliothèque. Tout ce que votre application voit est un ImageService
et lorsqu'elle a besoin d'une image, elle appelle load()
avec une URL - simple et directe.
Cependant, le véritable avantage vient lorsque nous commençons à changer les choses. Imaginez que nous devons maintenant remplacer Picasso par Universal Image Loader car Picasso ne prend pas en charge certaines fonctionnalités dont nous avons absolument besoin en ce moment. Devons-nous maintenant passer au peigne fin notre base de code et remplacer fastidieusement tous les appels à Picasso, puis traiter des dizaines d'erreurs de compilation parce que nous avons oublié quelques appels Picasso? Non. Tout ce que nous devons faire est de créer une nouvelle implémentation de ImageService
et dire à notre framework d'injection de dépendances d'utiliser cette implémentation à partir de maintenant :
public class UniversalImageLoaderImageService implements ImageService {
private final ImageLoader mImageLoader;
public UniversalImageLoaderImageService(Context context) {
DisplayImageOptions defaultOptions = new DisplayImageOptions.Builder()
.cacheInMemory(true)
.cacheOnDisk(true)
.build();
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.defaultDisplayImageOptions(defaultOptions)
.build();
mImageLoader = ImageLoader.getInstance();
mImageLoader.init(config);
}
@Override
public Bitmap load(String url) {
return mImageLoader.loadImageSync(url);
}
}
Comme vous pouvez le voir, la mise en œuvre peut être très différente, mais cela n'a pas d'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 pourrait avoir des fonctionnalités complètement différentes ou pourrait être utilisée très différemment, mais notre application s'en fiche. Comme avant, le reste de notre application ne voit que l'interface ImageService
avec sa méthode load()
et cependant cette méthode est implémentée n'a plus d'importance.
Au moins pour moi tout cela sonne déjà assez bien déjà, 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 effectuer des appels réseau vers une ressource située sur un autre serveur, mais puisque vous utilisez maintenant le ImageService
, vous pouvez facilement laisser load()
retourner un statique Bitmap
utilisé pour les tests unitaires en implémentant un ImageService
simulé:
public class MockImageService implements ImageService {
private final Bitmap mMockBitmap;
public MockImageService(Bitmap mockBitmap) {
mMockBitmap = mockBitmap;
}
@Override
public Bitmap load(String url) {
return mMockBitmap;
}
}
Pour résumer en enveloppant des bibliothèques tierces, votre base de code devient plus flexible aux changements, globalement plus simple, plus facile à tester et vous réduisez le couplage de différents composants dans votre logiciel - toutes choses qui deviennent de plus en plus importantes plus vous maintenez un logiciel.
Sans savoir quelles sont les super nouvelles fonctionnalités de ce prétendu futur enregistreur amélioré, comment écririez-vous le wrapper? Le choix le plus logique consiste à faire en sorte que votre wrapper instancie une sorte de classe de consignateur et à disposer de méthodes telles que ->info()
ou ->warn()
. En d'autres termes, essentiellement identique à votre API actuelle.
Plutôt qu'un code à l'épreuve du temps que je n'aurai peut-être jamais besoin de changer, ou qui peut nécessiter une réécriture inévitable de toute façon, je préfère un code "à l'épreuve du passé". Autrement dit, dans les rares occasions où je change de manière significative un composant, c'est lorsque j'écris un wrapper pour le rendre compatible avec le code précédent. Cependant, tout nouveau code utilise la nouvelle API, et je refactorise l'ancien code pour l'utiliser chaque fois que je fais une modification dans le même fichier de toute façon, ou comme le calendrier le permet. Après quelques mois, je peux retirer l'emballage, et le changement a été progressif et robuste.
Autrement dit, les wrappers n'ont vraiment de sens que si vous connaissez déjà toutes les API que vous devez encapsuler. 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 PHP PHP.
Je pense que le fait d'envelopper des bibliothèques tierces aujourd'hui au cas où quelque chose de mieux se produirait demain est une violation très inutile 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 des performances supérieures 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 pour piloter la nouvelle bibliothèque. Un peu plus de travail, mais pas de problème, et pas plus de travail que vous n'auriez fait si vous aviez écrit le wrapper plus tôt.
La raison fondamentale d'é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 de vous coupler à quelque chose, donc l'argument veut qu'il vaut mieux se coupler à une API que vous avez écrite.
Que cela en vaille la peine est une autre histoire. Ce débat se poursuivra probablement pendant longtemps.
Pour les petits projets, où la probabilité qu'un tel changement soit nécessaire est faible, il s'agit probablement d'efforts inutiles. Pour les projets plus importants, cette flexibilité peut très bien l'emporter sur l'effort supplémentaire pour envelopper la bibliothèque. Cependant, il est difficile de savoir si tel est le cas à l'avance.
Une autre façon de voir les choses est ce principe de base d'abstraire ce qui est susceptible de changer. Donc, si la bibliothèque tierce est bien établie et peu susceptible d'être modifiée, il peut être judicieux de ne pas la boucler. Cependant, si la bibliothèque tierce est relativement nouvelle, il y a plus de chances 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.
Je suis fortement dans le camp de l'emballage et je ne peux pas remplacer la bibliothèque tierce avec la plus grande priorité (bien que ce soit un bonus). Ma principale justification qui favorise l'emballage est simple
Les bibliothèques tierces ne sont pas pas conçues pour nos besoins spécifiques.
Et cela se manifeste généralement sous la forme d'une charge de duplication de code, comme des développeurs écrivant 8 lignes de code juste pour créer un QButton
et le styler comme il se doit pour l'application, uniquement pour le concepteur vouloir non seulement changer l'apparence mais aussi la fonctionnalité des boutons pour l'ensemble du logiciel, ce qui nécessite 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 parce que la base de code est parsemée de bas niveau du code OpenGL de pipeline fixe ad hoc partout au lieu de centraliser une conception de rendu en temps réel et de laisser l'utilisation d'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 sur-ensemble massif de ce qui est réellement nécessaire (et ce qui ne fait pas partie d'une conception est aussi important, sinon plus, que ce qui est), et leurs interfaces ne sont pas conçues pour répondre spécifiquement à nos besoins de manière "de haut niveau" pensée = une demande "sorte de chemin 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 beaucoup plus bas que ce qui devrait être nécessaire pour exprimer ce dont ils ont besoin, ils peuvent parfois finir par les emballer eux-mêmes de manière ad hoc, ce qui vous permet de vous retrouver avec des dizaines de textes écrits à la hâte et grossièrement- des enveloppes conçues et documentées au lieu d'une enveloppe bien conçue et bien documentée.
Bien sûr, j'appliquerais de fortes exceptions aux bibliothèques où les wrappers sont presque des traductions un à un de ce que les API tierces ont à offrir. Dans ce cas, il pourrait ne pas y avoir de conception de niveau supérieur à rechercher qui exprime 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 qui exprime beaucoup plus directement nos besoins, alors je suis fortement dans le camp de l'emballage, tout comme je suis fortement en faveur de l'utilisation d'une fonction de niveau supérieur et de sa réutilisation sur le code d'assemblage en ligne. partout.
Curieusement, je me suis affronté avec des développeurs de façons où ils semblaient si méfiants et si pessimistes quant à notre capacité à concevoir, disons, une fonction pour créer un bouton et le renvoyer qu'ils préfèrent écrire 8 lignes de code de niveau inférieur axées sur la microscopie les détails de la création du bouton (qui a fini par devoir changer à plusieurs reprises dans le futur) sur la conception et l'utilisation de ladite fonction. Je ne vois même pas pourquoi nous essayons de concevoir quoi que ce soit en premier lieu si nous ne pouvons pas nous faire confiance pour concevoir ces sortes d'emballages d'une manière raisonnable.
En d'autres termes, je vois les bibliothèques tierces comme des moyens de potentiellement gagner énormément de temps dans la mise en œuvre, et non comme des substituts à la conception de systèmes.
En plus de ce que @ Oded a déjà dit, je voudrais juste ajouter cette réponse dans le but spécial de la journalisation.
J'ai toujours une interface pour la journalisation mais je n'ai jamais eu à remplacer un log4foo
cadre encore.
Il ne faut 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é. Si le jour de l'échange de l'enregistreur arrive vraiment, je serai heureux d'avoir investi une demi-heure car cela me fera gagner plus d'une journée à échanger des appels dans un projet réel. Et je n'ai jamais écrit ni vu de test unitaire pour la journalisation (à part les tests pour l'implémentation de l'enregistreur lui-même), alors attendez-vous à des défauts sans le wrapper.
Je traite ce problème exact sur un projet sur lequel je travaille actuellement. Mais dans mon cas, la bibliothèque est pour les graphiques et je suis donc en mesure de restreindre son utilisation à un petit nombre de classes qui traitent des graphiques, par opposition à la saupoudrer 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 enregistreur, 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 et combien de douleur serait associée à sa modification. Si changer tous les appels d'API serait facile malgré tout, cela ne vaut probablement pas la peine. Si toutefois changer la bibliothèque plus tard était vraiment difficile, je l'envelopperais probablement maintenant.
Au-delà de cela, d'autres réponses ont très bien couvert la question principale, je veux donc me concentrer sur ce dernier ajout, à propos de l'injection de dépendances et des objets fictifs. Cela dépend bien sûr du fonctionnement exact de votre infrastructure de journalisation, mais dans la plupart des cas, cela ne nécessiterait pas de wrapper (bien qu'il en bénéficiera probablement). Rendez simplement l'API de votre objet simulé exactement la même que la bibliothèque tierce et vous pourrez facilement échanger l'objet simulé pour le test.
Le facteur principal ici est de savoir si la bibliothèque tierce est implémentée ou non via une injection de dépendance (ou un localisateur de service ou un modèle de ce type à couplage lâche). Si les fonctions de la bibliothèque sont accessibles via un singleton ou des méthodes statiques ou quelque chose, vous devrez envelopper cela dans un objet avec lequel vous pouvez travailler dans l'injection de dépendances.
Mon idée sur les bibliothèques tierces:
Il y a eu ne discussion récente dans la communauté iOS sur les avantages et les inconvénients (OK, principalement les 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 pour la plupart des choses, cependant, ce n'est pas si simple. Essayons donc de nous concentrer sur un seul cas
Devrions-nous éviter d'utiliser des bibliothèques d'interface utilisateur tierces?
Raisons d'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 truc n'est pas sorcier, mais il faut du temps pour bien le construire.
S'il s'agit d'une fonction commerciale principale - faites-la vous-même, quelle qu'elle soit.
Il existe à peu près deux types de contrôles/vues:
UICollectionView
de UIKit
.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 effectuent des révisions de code de leur code interne, mais peuvent considérer la qualité du code source tiers comme acquise. 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 swizzling utilisé là où il n'est pas nécessaire.
Souvent, apprendre l'idée est plus bénéfique que d'obtenir le code résultant lui-même.
Vous ne pouvez pas le cacher
En raison de la conception d'UIKit, vous ne pourrez probablement pas masquer la bibliothèque d'interface utilisateur tierce, par exemple derrière un adaptateur. Une bibliothèque s'entrelacera avec votre code d'interface utilisateur devenant de facto de votre projet.
Coût futur du temps
UIKit change avec chaque version d'iOS. Les choses vont casser. Votre dépendance vis-à-vis de tiers ne sera pas aussi exempte de maintenance que vous ne le pensez.
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é pour 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 tenons devant une décision difficile: que faire ensuite?
L'utilisation directe de la bibliothèque est plus conviviale pour l'équipe de développeurs. Lorsqu'un nouveau développeur rejoint, il peut être pleinement expérimenté avec tous les cadres utilisés mais ne sera pas en mesure de contribuer de manière productive avant d'apprendre votre API maison. Lorsqu'un jeune développeur tenterait de progresser dans votre groupe, il serait obligé d'apprendre votre API spécifique qui 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 ou des possibilités utiles de l'API d'origine, il se peut qu'il ne puisse pas atteindre la couche écrite par quelqu'un qui ne les connaissait pas. Si quelqu'un obtient une tâche de programmation tout en recherchant un emploi, il peut ne pas être en mesure de démontrer les choses de base qu'il a utilisées plusieurs fois, simplement parce qu'il accédait à toutes les fonctionnalités nécessaires via 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 wrappée n'est pas suffisamment figée et continue de changer.