Comment puis-je réduire l'effort manuel pour encapsuler des bibliothèques tierces avec un modèle d'objet plus grand?

16

Comme l'auteur de cette question de 2012 et celle de 2013 , j'ai une bibliothèque tierce que je dois envelopper pour tester correctement mon application. La première réponse indique:

Vous voulez toujours envelopper les types et méthodes tiers derrière une interface. Cela peut être fastidieux et douloureux. Parfois, vous pouvez écrire un générateur de code ou utiliser un outil pour ce faire.

Dans mon cas, la bibliothèque est pour un modèle d'objet et a par conséquent un plus grand nombre de classes et de méthodes qui devraient être encapsulées pour que cette stratégie réussisse. Au-delà de simplement "fastidieux et douloureux", cela devient une barrière difficile à tester.

Au cours des 4 années qui se sont écoulées depuis cette question, je suis conscient que les cadres d'isolement ont beaucoup progressé. Ma question est la suivante: existe-t-il maintenant un moyen plus simple d'obtenir l'effet d'un habillage complet des bibliothèques tierces? Comment puis-je éliminer la douleur de ce processus et réduire l'effort manuel?


Ma question n'est pas un double des questions auxquelles j'ai initialement lié, car la mienne concerne la réduction de l'effort manuel d'emballage. Ces autres questions ne demandent que si l’emballage a du sens, pas comment l’effort peut être limité.

Tom Wright
la source
De quel langage de programmation et de quel type de bibliothèque parlez-vous?
Doc Brown
@DocBrown C # et une bibliothèque de manipulation PDF.
Tom Wright
2
J'ai commencé un post sur meta pour trouver un support pour rouvrir votre question.
Doc Brown
Merci @DocBrown - J'espère qu'il y aura des perspectives intéressantes là-bas.
Tom Wright
1
Lorsque nous n'obtiendrons pas de meilleures réponses dans les 48 prochaines heures, je placerai une prime à ce sujet.
Doc Brown

Réponses:

4

En supposant que vous ne recherchiez pas un cadre moqueur, car ils sont ultra-omniprésents et faciles à trouver , il y a quelques choses à noter à l'avance:

  1. Il n'y a "jamais" quelque chose que vous devriez "toujours" faire.
    Il n'est pas toujours préférable de conclure une bibliothèque tierce. Si votre application est intrinsèquement dépendante d'une bibliothèque, ou si elle est littéralement construite autour d'une ou deux bibliothèques principales, ne perdez pas votre temps à la boucler. Si les bibliothèques changent, votre application devra quand même changer .
  2. Il est acceptable d'utiliser des tests d'intégration.
    Cela est particulièrement vrai autour des limites qui sont stables, intrinsèques à votre application ou qui ne peuvent pas être facilement moquées. Si ces conditions sont remplies, l'emballage et la moquerie seront compliqués et fastidieux. Dans ce cas, j'éviterais les deux: ne pas envelopper et ne pas se moquer; il suffit d'écrire des tests d'intégration. (Si les tests automatisés sont un objectif.)
  3. Les outils et le cadre ne peuvent pas éliminer la complexité logique.
    En principe, un outil ne peut couper que sur le passe-partout. Mais, il n'y a pas d'algorithme automatisable pour prendre une interface complexe et la rendre simple - sans parler de prendre l'interface X et de l'adapter à vos besoins. (Vous seul connaissez cet algorithme!) Donc, même s'il existe sans aucun doute des outils qui peuvent générer des wrappers fins, je dirais qu'ils ne sont pas déjà omniprésents car, en fin de compte, vous devez toujours coder intelligemment, et donc manuellement, contre l'interface même si elle est cachée derrière un wrapper.

Cela dit, il existe des tactiques que vous pouvez utiliser dans de nombreuses langues pour éviter de vous référer directement à une classe. Et dans certains cas, vous pouvez «simuler» une interface ou un wrapper mince qui n'existe pas réellement. En C #, par exemple, je choisirais l'une des deux voies:

  1. Utilisez une fabrique et une saisie implicite .

Vous pouvez éviter l'effort d'encapsuler complètement une classe complexe avec ce petit combo:

// "factory"
class PdfDocumentFactory {
  public static ExternalPDFLibraryDocument Build() {
    return new ExternalPDFLibraryDocument();
  }
}

// code that uses the factory.
class CoreBusinessEntity {
  public void DoImportantThings() {
    var doc = PdfDocumentFactory.Build();

    // ... i have no idea what your lib does, so, I'm making stuff but.
    // but, you can do whatever you want here without explicitly
    // referring to the library's actual types.
    doc.addHeader("Wee");
    doc.getAllText().makeBiggerBy(4).makeBold().makeItalic();
    return doc.exportBinaryStreamOrSomething();
  }
}

Si vous pouvez éviter de stocker ces objets en tant que membres, soit par une approche plus "fonctionnelle", soit en les stockant dans un dictionnaire (ou autre ), cette approche a l'avantage de vérifier le type au moment de la compilation sans que vos entités métier principales n'aient besoin de savoir exactement avec quelle classe ils travaillent.

Tout ce qui est requis, c'est qu'au moment de la compilation, la classe renvoyée par votre fabrique contient en fait les méthodes que votre objet métier utilise.

  1. Utilisez la saisie dynamique .

C'est dans la même veine que l'utilisation du typage implicite , mais implique un autre compromis: vous perdez les vérifications de type compilation et avez la possibilité d'ajouter anonymement des dépendances externes en tant que membres de la classe et injectez vos dépendances.

class CoreBusinessEntity {
  dynamic Doc;

  public void InjectDoc(dynamic Doc) {
    Doc = doc;
  }

  public void DoImortantThings() {
    Doc.addHeader("Wee");
    Doc.getAllText().makeBiggerBy(4).makeBold().makeItalic();
    return Doc.exportBinaryStreamOrSomething();
  }
}

Avec ces deux tactiques, quand vient le temps de se moquer ExternalPDFLibraryDocument, comme je l'ai dit plus tôt, vous avez du travail à faire - mais c'est un travail que vous devez faire de toute façon . Et, avec cette construction, vous avez évité de définir fastidieusement des centaines de petites classes de wrapper minces. Vous avez simplement utilisé la bibliothèque sans la regarder directement - pour la plupart.

Cela dit, il y a trois grandes raisons pour lesquelles j'envisagerais toujours de conclure explicitement une bibliothèque tierce - aucune ne suggérant l'utilisation d'un outil ou d'un framework:

  1. La bibliothèque spécifique n'est pas intrinsèque à l'application.
  2. Il serait très coûteux d'échanger sans le boucler.
  3. Je n'aime pas l'API elle-même.

Si je n'ai pas un certain niveau de préoccupation dans ces trois domaines, vous ne faites aucun effort important pour conclure. Et, si vous avez des inquiétudes dans les trois domaines, un wrapper mince généré automatiquement ne va pas vraiment aider.

Si vous avez décidé de conclure une bibliothèque, l'utilisation la plus efficace et la plus efficace de votre temps est de créer votre application avec l'interface que vous souhaitez ; pas contre une API existante.

Autrement dit, tenez compte du conseil classique: reporter toutes les décisions que vous pouvez. Créez d'abord le «cœur» de votre application. Codez contre des interfaces qui finiront par faire ce que vous voulez, qui seront finalement remplies par des "choses périphériques" qui n'existent pas encore. Comblez les lacunes au besoin.

Cet effort peut ne pas ressembler à un gain de temps; mais si vous sentez que vous avez besoin d'un emballage, c'est le moyen le plus efficace de le faire en toute sécurité.

Pense-y de cette façon.

Vous devez coder par rapport à cette bibliothèque dans un coin sombre de votre code - même s'il est terminé. Si vous vous moquez de la bibliothèque pendant les tests, il y a inévitablement un effort manuel - même si elle est terminée. Mais cela ne signifie pas que vous devez reconnaître directement cette bibliothèque par son nom dans la majeure partie de votre application.

TLDR

Si la bibliothèque vaut la peine d'être emballée, utilisez des tactiques pour éviter les références directes et étendues à votre bibliothèque tierce, mais ne prenez pas de raccourcis pour générer des enveloppes minces. Construisez d'abord votre logique métier, réfléchissez à vos interfaces et sortez vos adaptateurs de manière organique, au besoin.

Et, si cela arrive, n'ayez pas peur des tests d'intégration. Ils sont un peu plus flous, mais ils offrent toujours des preuves de fonctionnement du code, et ils peuvent toujours être facilement effectués pour garder les régressions à distance.

svidgen
la source
2
Il s'agit d'un autre article qui ne répond pas à la question - qui n'était pas explicitement "quand envelopper", mais "comment réduire l'effort manuel".
Doc Brown
1
Désolé, mais je pense que vous avez manqué le point central de la question. Je ne vois pas comment vos suggestions pourraient réduire les efforts manuels d'emballage. Supposons que la bibliothèque en jeu possède un modèle d'objet complexe en tant qu'API, avec des dizaines de classes et des centaines de méthodes. Son API est très bien comme pour un usage standard, mais comment l'envelopper pour des tests unitaires avec moins de douleur / effort?
Doc Brown
1
TLDR; si vous voulez des points bonus, dites-nous quelque chose que nous ne savons pas déjà ;-)
Doc Brown
1
@DocBrown Je n'ai pas manqué le point. Mais, je pense que vous avez manqué le point de ma réponse. Investir dans le bon effort vous fait économiser beaucoup de travail. Il y a des implications de test - mais ce n'est qu'un effet secondaire. L'utilisation d'un outil pour générer automatiquement une enveloppe mince autour d'une bibliothèque vous laisse toujours construire votre bibliothèque principale autour de l'API de quelqu'un d'autre et créer des maquettes - ce qui représente beaucoup d'efforts qui ne peuvent être évités si vous insistez pour laisser la bibliothèque de côté de vos tests.
svidgen
1
@DocBrown C'est absurde. "Cette classe de problèmes a-t-elle une classe d'outils ou d'approches?" Oui. Bien sûr que oui. Codage défensif et ne pas devenir dogmatique à propos des tests unitaires ... comme le dit ma réponse. Si vous a fait avoir une enveloppe mince généré automagiquement, quelle valeur serait - il fournir !? ... Cela ne vous permettrait pas d'injecter des dépendances pour les tests, vous devez toujours le faire manuellement. Et cela ne vous permettrait pas d'échanger la bibliothèque, car vous codez toujours contre l'API de la bibliothèque ... c'est juste indirect maintenant.
svidgen
9

Ne testez pas ce code à l'unité. Écrivez plutôt des tests d'intégration. Dans certains cas, les tests unitaires, moqueurs, sont fastidieux et douloureux. Abandonnez les tests unitaires et écrivez des tests d'intégration qui font réellement appel au fournisseur.

Remarque, ces tests doivent être exécutés après le déploiement en tant qu'activité automatisée post-déploiement. Ils ne sont pas exécutés dans le cadre de tests unitaires ou dans le cadre du processus de génération.

Parfois, un test d'intégration est mieux adapté qu'un test unitaire dans certaines parties de votre application. Les cerceaux que l'on doit traverser pour rendre le code "testable" peuvent parfois être préjudiciables.

Jon Raynor
la source
2
La première chose que j'ai pensé en lisant votre réponse était "irréaliste pour PDF, car comparer les fichiers au niveau binaire ne vous dit pas ce qui a changé ou si le changement est problématique". Ensuite, j'ai trouvé cet ancien article SO , indique qu'il pourrait réellement fonctionner (donc +1).
Doc Brown
@DocBrown, l'outil du lien SO ne compare pas les PDF au niveau binaire. Il compare la structure et le contenu des pages. Structure interne PDF: safaribooksonline.com/library/view/pdf-explained/9781449321581/…
linuxunil
1
@linuxunil: oui, je sais, comme je l'ai écrit, d' abord j'ai pensé ... mais ensuite j'ai trouvé la solution mentionnée ci-dessus.
Doc Brown
oh .. je vois .. peut-être que "ça pourrait vraiment marcher" dans la finale de votre réponse m'a embrouillé.
linuxunil
1

Si je comprends bien, cette discussion se concentre sur les opportunités d'automatisation de la création d'encapsuleurs plutôt que sur l'idée d'encapsulation et les directives d'implémentation. J'essaierai de m'abstraire de l'idée car il y en a déjà beaucoup ici.

Je vois que nous jouons autour des technologies .NET, nous avons donc de puissantes capacités de réflexion entre nos mains. Vous pouvez envisager:

  1. Outil comme .NET Wrapper Class Generator . Je n'ai pas utilisé cet outil et je sais qu'il fonctionne sur une pile technologique héritée, mais peut-être pour votre cas, il conviendrait. Bien sûr, la qualité du code, la prise en charge de l'inversion de dépendance et de la ségrégation des interfaces doivent être étudiées séparément. Peut-être qu'il y a d'autres outils comme ça mais je n'ai pas beaucoup cherché.
  2. Écrire votre propre outil qui recherchera dans l'assembly référencé les méthodes / interfaces publiques et effectuera le mappage et la génération de code. Une contribution à la communauté serait plus que bienvenue!
  3. Si .NET n'est pas un cas ... regardez peut-être ici .

Je pense qu'une telle automatisation peut constituer un point de base, mais pas une solution définitive. Une refactorisation manuelle du code sera nécessaire ... ou dans le pire des cas une refonte car je suis entièrement d'accord avec ce que svidgen a écrit:

Construisez votre application contre l'interface que vous souhaitez; pas contre une API tierce.

tom3k
la source
Ok, au moins une idée de la façon dont le problème pourrait être traité. Mais pas basé sur sa propre expérience pour ce cas d'utilisation, je suppose?
Doc Brown
La question est basée sur ma propre expérience dans le domaine du génie logiciel (wrapper, réflexion, di)! En ce qui concerne les outils - je ne l'ai pas utilisé comme indiqué dans la réponse.
tom3k
0

Suivez ces instructions lors de la création de bibliothèques d'encapsuleurs:

  • n'exposer qu'un petit sous-ensemble de la bibliothèque tierce dont vous avez besoin en ce moment (et étendre à la demande)
  • garder le wrapper aussi mince que possible (aucune logique n'y appartient).
Rumen Georgiev
la source
1
Je me demande simplement pourquoi vous avez obtenu 3 votes positifs pour un message qui manque le point de la question.
Doc Brown