Quelle est la manière courante de gérer la visibilité dans les bibliothèques?

12

Cette question de savoir quand utiliser privé et quand utiliser protégé en classe m'a fait réfléchir. (Je vais également étendre cette question aux classes et méthodes finales, car elles sont liées. Je programme en Java, mais je pense que cela s'applique à tous les langages POO)

La réponse acceptée est:

Une bonne règle est de rendre tout aussi privé que possible.

Et un autre:

  1. Rendez toutes les classes finales, sauf si vous devez les sous-classer immédiatement.
  2. Définissez toutes les méthodes comme définitives, sauf si vous devez les sous-classer et les remplacer immédiatement.
  3. Rendez tous les paramètres de méthode définitifs, sauf si vous devez les modifier dans le corps de la méthode, ce qui est un peu gênant la plupart du temps de toute façon.

C'est assez simple et clair, mais que faire si j'écris principalement des bibliothèques (Open Source sur GitHub) au lieu d'applications?

Je pourrais nommer beaucoup de bibliothèques et de situations, où

  • Une bibliothèque s'est agrandie d'une manière à laquelle les développeurs n'auraient jamais pensé
  • Cela devait être fait avec "class loader magic" et d'autres hacks à cause des contraintes de visibilité
  • Les bibliothèques ont été utilisées d'une manière pour laquelle elles n'étaient pas conçues et la fonctionnalité requise a été "piratée" dans
  • Les bibliothèques n'ont pas pu être utilisées en raison d'un petit problème (bogue, fonctionnalité manquante, comportement "incorrect") qui n'a pas pu être modifié en raison d'une visibilité réduite
  • Un problème qui n'a pas pu être résolu a conduit à des solutions de contournement énormes, laides et boguées où le remplacement d'une fonction simple (qui était privée ou finale) aurait pu aider

Et j'ai commencé à les nommer jusqu'à ce que la question devienne trop longue et j'ai décidé de les supprimer.

J'aime l'idée de ne pas avoir plus de code que nécessaire, plus de visibilité que nécessaire, plus d'abstraction que nécessaire. Et cela pourrait fonctionner lors de l'écriture d'une application pour l'utilisateur final, où le code n'est utilisé que par ceux qui l'écrivent. Mais comment cela peut-il tenir si le code est destiné à être utilisé par d'autres développeurs, où il est improbable que le développeur d'origine ait pensé à tous les cas d'utilisation possibles à l'avance et que les modifications / refactors soient difficiles / impossibles à faire?

Étant donné que les grandes bibliothèques open source ne sont pas une nouveauté, quelle est la façon la plus courante de gérer la visibilité dans de tels projets avec des langages orientés objet?

piegames
la source
étant donné que vous posez des questions sur l'open source, il est encore moins judicieux de contourner les principes de codage appropriés afin de résoudre les problèmes que vous avez répertoriés que la source fermée, simplement parce que l'on peut apporter les corrections nécessaires directement dans le code de la bibliothèque ou le forger et créer sa propre version avec quelles que soient les corrections qu'ils veulent
moucher
2
mon point n'est pas à ce sujet mais à propos de votre référence à l'open source n'ayant aucun sens dans ce contexte. Je peux imaginer comment des besoins pragmatiques peuvent justifier une dérogation aux principes stricts dans certains cas (également connus sous le nom d'accumulation de dettes techniques ), mais de ce point de vue, peu importe que le code soit fermé ou open source. Ou plus précisément, cela importe dans une direction opposée à celle que vous imaginiez ici car le code étant open source peut rendre ces besoins moins pressants que fermés car il offre des options supplémentaires pour y répondre
gnat
1
@piegames: je suis tout à fait d'accord pour moucher ici, les problèmes que vous avez récupérés sont beaucoup plus susceptibles de se produire dans les bibliothèques de sources fermées - s'il s'agit d'une bibliothèque de système d'exploitation avec une licence permissive, si les responsables ignorent une demande de changement, on peut bifurquer la lib et changer la visibilité par soi-même, si nécessaire.
Doc Brown
1
@piegames: Je ne comprends pas votre question. "Java" est un langage, pas une lib. Et si "votre petite bibliothèque open source" a une visibilité trop stricte, l'extension de la visibilité par la suite ne rompt normalement pas la compatibilité descendante. Seulement l'inverse.
Doc Brown

Réponses:

15

La triste vérité est que de nombreuses bibliothèques sont écrites et non conçues . C'est triste, car un peu de réflexion préalable peut éviter beaucoup de problèmes sur la route.

Si nous décidons de concevoir une bibliothèque, il y aura un ensemble de cas d'utilisation prévus. La bibliothèque peut ne pas satisfaire directement tous les cas d'utilisation, mais peut faire partie d'une solution. La bibliothèque doit donc être suffisamment flexible pour s'adapter.

La contrainte est que ce n'est généralement pas une bonne idée de prendre le code source de la bibliothèque et de le modifier pour gérer le nouveau cas d'utilisation. Pour les bibliothèques propriétaires, la source peut ne pas être disponible et pour les bibliothèques open source, il peut être indésirable de conserver une version fourchue. Il peut ne pas être possible de fusionner des adaptations très spécifiques dans le projet en amont.

C'est là qu'intervient le principe ouvert-fermé: la bibliothèque doit être ouverte à l'extension sans modifier le code source. Cela ne vient pas naturellement. Cela doit être un objectif de conception intentionnel. Il existe une multitude de techniques qui peuvent vous aider ici, les modèles de conception OOP classiques en sont certains. En général, nous spécifions des hooks où le code utilisateur peut se connecter en toute sécurité à la bibliothèque et ajouter des fonctionnalités.

Il ne suffit pas de rendre chaque méthode publique ou de permettre à chaque classe d'être sous-classée pour atteindre l'extensibilité. Tout d'abord, il est vraiment difficile d'étendre la bibliothèque si l'on ne sait pas où l'utilisateur peut se connecter à la bibliothèque. Par exemple, remplacer la plupart des méthodes n'est pas sûr car la méthode de la classe de base a été écrite avec des hypothèses implicites. Vous avez vraiment besoin de concevoir pour l'extensibilité.

Plus important encore, une fois que quelque chose fait partie de l'API publique, vous ne pouvez pas le reprendre. Vous ne pouvez pas le refactoriser sans casser le code en aval. Une ouverture prématurée limite la bibliothèque à une conception sous-optimale. En revanche, rendre les éléments internes privés mais ajouter des crochets si nécessaire, est une approche plus sûre. Bien que ce soit une manière saine d'aborder l'évolution à long terme d'une bibliothèque, cela n'est pas satisfaisant pour les utilisateurs qui ont besoin d'utiliser la bibliothèque en ce moment .

Alors que se passe-t-il à la place? En cas de problème important avec l'état actuel de la bibliothèque, les développeurs peuvent prendre toutes les connaissances sur les cas d'utilisation réels accumulés au fil du temps et écrire une version 2 de la bibliothèque. Ce sera génial! Il corrigera tous ces bogues de conception! Cela prendra également plus de temps que prévu, dans de nombreux cas, il disparaîtra. Et si la nouvelle version est très différente de l'ancienne version, il peut être difficile d'encourager les utilisateurs à migrer. Il vous reste alors à conserver deux versions incompatibles.

amon
la source
J'ai donc besoin d'ajouter des crochets pour l'extension, car la rendre publique / remplaçable ne suffit pas. Et je dois également réfléchir au moment de publier les modifications / nouvelle API en raison de la compatibilité descendante. Mais qu'en est-il de la visibilité des méthodes en particulier?
piegames
@piegames Avec la visibilité, vous décidez quelles parties sont publiques (partie de votre API stable) et quelles parties sont privées (sujettes à changement). Si quelqu'un contourne cela par la réflexion, c'est son problème lorsque cette fonctionnalité se brise à l'avenir. Soit dit en passant, les points d'extension sont souvent sous la forme d'une méthode qui peut être remplacée. Mais il y a une différence entre une méthode qui peut être remplacée et une méthode qui est destinée à être remplacée (voir aussi le modèle de méthode de modèle).
amon
8

Chaque classe / méthode publique et extensible fait partie de votre API qui doit être prise en charge. Limiter cet ensemble à un sous-ensemble raisonnable de la bibliothèque permet la plus grande stabilité et limite le nombre de choses qui peuvent mal tourner. C'est une décision de gestion (et même les projets OSS sont gérés dans une certaine mesure) en fonction de ce que vous pouvez raisonnablement soutenir.

La différence entre OSS et source fermée est que la plupart des gens essaient de créer et de développer une communauté autour du code afin que ce soit plus d'une personne qui gère la bibliothèque. Cela dit, plusieurs outils de gestion sont disponibles:

  • Les listes de diffusion discutent des besoins des utilisateurs et de la façon de mettre en œuvre les choses
  • Les systèmes de suivi des problèmes (problèmes JIRA ou Git, etc.) suivent les bogues et les demandes de fonctionnalités
  • Le contrôle de version gère le code source.

Dans les projets matures, ce que vous verrez est quelque chose du genre:

  1. Quelqu'un veut faire quelque chose avec la bibliothèque qu'elle n'était pas conçue à l'origine
  2. Ils ajoutent un ticket au suivi des problèmes
  3. L'équipe peut discuter du problème dans la liste de diffusion ou dans les commentaires, et le demandeur est toujours invité à participer à la discussion
  4. Le changement d'API est accepté et priorisé ou rejeté pour une raison quelconque

À ce stade, si la modification a été acceptée mais que l'utilisateur souhaite accélérer sa correction, il peut effectuer le travail et soumettre une demande d'extraction ou un correctif (selon l'outil de contrôle de version).

Aucune API n'est statique. Cependant, sa croissance doit être façonnée d'une manière ou d'une autre. En gardant tout fermé jusqu'à ce qu'il y ait un besoin avéré d'ouvrir les choses, vous évitez d'avoir la réputation d'un buggy ou d'une bibliothèque instable.

Berin Loritsch
la source
1
Je suis entièrement d'accord et j'ai mis en pratique avec succès le processus de demande de changement que vous avez récupéré pour les bibliothèques fermées tierces, ainsi que pour les bibliothèques open source.
Doc Brown
D'après mon expérience, même de petits changements dans de petites bibliothèques demandent beaucoup de travail (même si c'est juste pour convaincre les autres) et peuvent prendre un certain temps (attendre la prochaine version si vous ne pouvez pas vous permettre d'utiliser un instantané jusque-là). Donc ce n'est clairement pas une option pour moi. Je serais intéressé cependant: y a-t-il des bibliothèques plus grandes (dans GitHub) qui utilisent vraiment ce concept?
piegames
C'est toujours beaucoup de travail. Presque tous les projets auxquels j'ai contribué ont un processus similaire à celui-ci. Dans mes jours Apache, nous pourrions discuter de quelque chose pendant des jours parce que nous étions passionnés par ce que nous avons créé. Nous savions que beaucoup de gens allaient utiliser les bibliothèques, nous avons donc dû discuter de choses comme si le changement proposé allait casser l'API, la fonctionnalité proposée en vaut-elle la peine, quand devrions-nous le faire? etc.
Berin Loritsch
0

Je vais reformuler ma réponse car il semble qu'elle ait frappé un nerf avec quelques personnes.

La visibilité de la propriété / méthode de classe n'a rien à voir avec la sécurité ni l'ouverture des sources.

La visibilité existe parce que les objets sont fragiles à 4 problèmes spécifiques:

  1. simultanéité

Si vous construisez votre module sans encapsulation, vos utilisateurs s'habitueront à modifier directement l'état du module. Cela fonctionne très bien dans un environnement à thread unique, mais une fois que vous pensez même à ajouter des threads; vous serez obligé de rendre l'État privé et d'utiliser des verrous / moniteurs avec des getters et des setters qui font que d'autres threads attendent les ressources, plutôt que de courir dessus. Cela signifie que vos programmes utilisateurs ne fonctionneront plus car les variables privées ne sont pas accessibles de manière conventionnelle. Cela peut signifier que vous avez besoin de beaucoup de réécritures.

La vérité est qu'il est beaucoup plus facile de coder avec un seul runtime threadé à l'esprit, et un mot-clé privé vous permet d'ajouter simplement le mot-clé synchronisé, ou quelques verrous, et le code de vos utilisateurs ne se cassera pas si vous l'encapsulez depuis le début .

  1. Empêchez les utilisateurs de se tirer une balle dans le pied / rationalisez l'utilisation de l'interface. En substance, il vous aide à contrôler les invariants de l'objet.

Chaque objet a un tas de choses dont il a besoin pour être vrai afin d'être dans un état cohérent. Malheureusement, ces choses vivent dans l'espace visible du client car il est coûteux de déplacer chaque objet dans son propre processus et de lui parler via des messages. Cela signifie qu'il est très facile pour un objet de planter tout le programme si l'utilisateur a une visibilité complète.

Ceci est inévitable, mais vous pouvez éviter de mettre accidentellement un objet dans un état incohérent en fermant l'interface sur ses services qui empêchent les plantages accidentels en permettant uniquement à l'utilisateur d'interagir avec l'état de l'objet via une interface soigneusement conçue qui rend le programme beaucoup plus robuste. . Cela ne signifie pas que l'utilisateur ne peut pas corrompre intentionnellement les invariants, mais s'ils le font, c'est leur client qui se bloque, il leur suffit de redémarrer le programme (les données que vous souhaitez protéger ne doivent pas être stockées côté client ).

Un autre bel exemple où vous pouvez améliorer la convivialité de vos modules est de rendre le constructeur privé; car si le constructeur lève une exception, il tuera le programme. Une approche paresseuse pour résoudre ce problème est de faire en sorte que le constructeur vous envoie une erreur de temps de compilation que vous ne pouvez pas la construire à moins qu'elle ne soit dans un bloc try / catch. En rendant le constructeur privé et en ajoutant une méthode de création statique publique, vous pouvez demander à la méthode create de retourner null si elle ne parvient pas à le construire, ou de prendre une fonction de rappel pour gérer l'erreur, ce qui rend le programme plus convivial.

  1. Portée de la pollution

De nombreuses classes ont beaucoup d'états et de méthodes et il est facile de se laisser submerger en essayant de les parcourir; Beaucoup de ces méthodes ne sont que du bruit visuel comme les fonctions d'assistance, l'état. rendre les variables et les méthodes privées permet de réduire la pollution de la portée et de faciliter la recherche des services recherchés par l'utilisateur.

Essentiellement, cela vous permet de vous passer de fonctions d'assistance à l'intérieur de la classe plutôt qu'à l'extérieur de la classe; sans contrôle de la visibilité sans distraire l'utilisateur avec un tas de services que l'utilisateur ne devrait jamais utiliser, vous pouvez donc vous passer de décomposer les méthodes en un tas de méthodes d'assistance (même si cela polluera toujours votre portée, mais pas l'utilisateur).

  1. être lié à des dépendances

Une interface bien conçue peut masquer ses bases de données internes / fenêtres / imagerie dont elle dépend pour faire son travail, et si vous souhaitez passer à une autre base de données / un autre système de fenêtrage / une autre bibliothèque d'imagerie, vous pouvez garder l'interface identique et celle des utilisateurs ne remarquera pas.

D'un autre côté, si vous ne le faites pas, vous pouvez facilement tomber dans l'impossibilité de modifier vos dépendances, car elles sont exposées et le code s'appuie dessus. Avec un système suffisamment grand, le coût de la migration peut devenir inabordable, tandis qu'un encapsulage peut protéger les utilisateurs clients qui se comportent bien des décisions futures de permuter les dépendances.

Dmitry
la source
1
"Inutile de cacher quoi que ce soit" - alors pourquoi même penser à l'encapsulation? Dans de nombreux contextes, la réflexion nécessite des privilèges spéciaux.
Frank Hileman
Vous pensez à l'encapsulation car elle vous donne de l'espace pour respirer lors du développement de votre module et réduit les risques de mauvaise utilisation. Par exemple, si vous avez 4 threads modifiant directement l'état interne d'une classe, cela causera facilement des problèmes, tandis que rendre la variable privée, encourage l'utilisateur à utiliser les méthodes publiques pour manipuler l'état mondial, qui peut utiliser des moniteurs / verrous pour éviter les problèmes . C'est le seul véritable avantage de l'encapsulation.
Dmitry
Cacher des objets pour des raisons de sécurité est un moyen facile de créer un design où vous vous retrouvez avec des trous dans votre api. Un bon exemple de cela est les applications multi-documents, où vous avez de nombreuses boîtes à outils et de nombreuses fenêtres avec des sous-fenêtres. Si vous devenez fou sur l'encapsulation, vous finirez par avoir une situation où dessiner quelque chose sur un document, vous devez demander à la fenêtre de demander au document intérieur de demander au document intérieur de demander au document intérieur de faire une demande pour dessiner quelque chose et invalider son contexte. Si le côté client veut jouer avec le côté client, vous ne pouvez pas les empêcher.
Dmitry
OK, cela a plus de sens, bien que la sécurité puisse être atteinte via le contrôle d'accès si l'environnement le supporte, et c'était l'un des objectifs originaux de la conception du langage OO. Vous faites également la promotion de l'encapsulation et vous dites de ne pas l'utiliser en même temps; un peu déroutant.
Frank Hileman
Je n'ai jamais voulu ne pas l'utiliser; Je voulais dire ne pas l'utiliser pour des raisons de sécurité; utilisez-le stratégiquement pour améliorer l'expérience de vos utilisateurs et pour vous offrir un environnement de développement plus fluide. mon point de vue est que cela n'a rien à voir avec la sécurité ni l'ouverture des sources. Les objets côté client sont par définition vulnérables à l'introspection, et les déplacer hors de l'espace de processus utilisateur rend les choses non encapsulées tout aussi inaccessibles qu'encapsulées.
Dmitry