Existe-t-il des études scientifiquement rigoureuses sur les principes de style de codage? [fermé]


25

Un principe de style de codage - par exemple le principe de sortie unique - est-il vraiment une bonne chose? Toujours ou juste parfois? Quelle différence cela fait-il vraiment?

Quelles que soient vos opinions, ce sont évidemment des questions subjectives. Ou sont-ils?

Quelqu'un a-t-il tenté de faire une étude objective et scientifiquement rigoureuse des principes de style de codage?

Je ne peux pas imaginer comment quelqu'un ferait une étude en double aveugle de la lisibilité, mais peut-être que le double ignorant pourrait être possible - utilisez des étudiants qui ne connaissent pas le principe étudié en tant que sujets et des non-programmeurs pour administrer l'étude.


5
Vous pouvez être intéressé par la lecture complète du code. Tout n'est pas mesurable, mais beaucoup l'est, et vous trouverez un bon aperçu des données brutes ou des sources dans ce livre.
deadalnix

Cela dépend aussi fortement de la langue, certains principes s'appliquent à des langues spécifiques et pas à d'autres. Par exemple, le single-exit principlene s'applique pas vraiment au C ++ à cause de RAII
Martin York


@Loki - J'ai dû y penser, et je ne suis pas sûr d'être d'accord. Il est vrai que RAII a été conçu en grande partie pour faire face à des exceptions, qui sont des points de sortie alternatifs, mais (au moins pour certaines personnes) , ils comptent comme d' autres points de sortie alternatifs - sans compter vraiment contre le principe de sortie unique de la manière que break, gotoou returnfaire. La sortie unique IOW n'est pas un absolu en C ++, mais c'est à peu près mon point de vue en C et dans la plupart des autres langages de toute façon. Mais c'est toujours pertinent dans un sens non strict.
Steve314

1
@ Steve314, l'article est au moins d'une pertinence éloignée - il décrit la conception d'une méthodologie pour une telle expérience, ce qui est assez important en raison d'un manque évident de preuves expérimentales correctement enregistrées dans ce domaine.
SK-logic

Réponses:


11

Je fais écho au commentaire de deadalnix: lire Code Complete 2 . L'auteur (Steve McConnell) discute du style de codage en profondeur et fréquemment des références d'articles et de données.


Aperçu fondamental et bien présenté du développement de logiciels professionnels, j'espère qu'un jour j'en trouverai un similaire pour l'assurance qualité. Les chapitres sur la programmation défensive et la programmation pseudocode m'ont été particulièrement utiles. Le chapitre sur les pratiques de développement collaboratif semble être le plus convaincant de tout ce que j'ai lu sur ces questions jusqu'à présent.
moucher

Je n'ai pas lu ce livre, et je devrais peut-être le faire, mais - sur la base des commentaires dans la réponse de moucherons - ces articles référencés sont-ils vraiment rigoureux et objectifs scientifiquement? Si la réponse est "autant que possible", quels compromis étaient nécessaires? Comme je l'ai suggéré dans la question, était-il nécessaire de remplacer le double aveugle par une norme plus faible?
Steve314

@ Steve314: Je ne sais pas, je n'ai pas vérifié les sources! Mais vous n'avez pas toujours besoin de rigueur scientifique pour établir les meilleures pratiques. Une discussion des avantages et des inconvénients est parfois suffisante.
M. Dudley

@emddudley - absolument vrai, mais pas vraiment sur quoi portait cette question.
Steve314

@ Steve314: Code Complete serait un excellent point de départ pour vous, et je suis convaincu que certaines de ses références abordent la question de l'analyse scientifique du style de codage.
M. Dudley

12

Je doute fortement de la possibilité même d'une étude sur le sujet donnant des résultats objectifs et je resterai sceptique jusqu'à ce qu'on me montre des recherches convaincantes.

Les programmeurs qui ont passé des années à lire et à écrire du code qui a suivi un certain style de codage le trouveront évidemment plus lisible qu'un style de codage parfait qu'ils verraient pour la première fois de leur vie.

C'est exactement la même chose avec la disposition de frappe QWERTY la plus courante - il est facile de prouver qu'elle est assez sous-optimale en termes d'ergonomie (pensez-vous que tous les caractères du mot TYPEWRITER ont été placés dans la rangée du haut avec notre commodité quotidienne à l'esprit?) .

Mais des alternatives améliorées telles que Dvorak ou Colemak n'ont jamais fait leur chemin et sont peu susceptibles de le faire. Et donc les gens ne sont pas plus productifs avec eux - fait. Même s'ils sont supérieurs dans un sens abstrait.

En outre, il serait difficile de trouver des sujets sans exposition préalable à la programmation (car cela contaminerait le résultat de notre étude), MAIS une aptitude à la programmation ET la volonté de participer à une étude pendant une période suffisamment longue pour montrer à la fois les courts -les avantages à long terme et les avantages à long terme afin qu'ils puissent être comparés les uns aux autres ... (Je ne sais pas s'ils s'excluent mutuellement, mais les chercheurs ne pouvaient pas simplement supposer qu'ils ne le sont jamais).


1
Cool, je n'avais jamais entendu parler de Colemak auparavant
CaffGeek

1
@Chad encore moins connu est Carpal X, avec lequel j'ai joué pendant un certain temps. Je l'ai trouvé plus agréable que Colemak (j'ai atteint 90-100 wpm avec carpalx). Même si vous n'avez pas l'intention de passer à des dispositions exotiques, le site Web de carpalx fait une lecture extrêmement intéressante sur l'évaluation et l'optimisation des dispositions de clavier et l'utilisation d'algorithmes génétiques pour cette catégorie de problèmes. Voir mkweb.bcgsc.ca/carpalx
Konrad Morawski

1
Parfois, les avantages marginaux d'une approche alternative seront suffisamment importants pour justifier le coût de son adoption; sinon nous serions tous encore en train de programmer assembleur et fortran. Cette réponse ne répond pas vraiment à la question initiale de savoir s'il y a effectivement des avantages marginaux. Dans l'exemple Dvorak, il y en a certainement et cela a été prouvé, mais ce ne sont pas des avantages suffisamment importants pour justifier l'apprentissage du Dvorak.
Jeremy

@Jeremy "cette réponse ne répond pas vraiment à la question initiale de savoir s'il y a ou non des avantages marginaux" - le PO n'a pas directement demandé les résultats de telles études, il a demandé si quelqu'un avait tenté de mener de telles études, qui est une question plus ouverte. J'ai répondu en soulignant quelques raisons logiques pour lesquelles cela serait techniquement difficile et pourquoi les résultats d'une telle étude seraient probablement considérablement contaminés par le bruit statistique. Donc, si ma réponse a été jugée inutile sur la base des motifs que vous avez avancés, je pense que j'ai été injustement rejeté.
Konrad Morawski

1
@Jeremy l'essentiel de ces coûts d'adoption est que les gens obtiennent de meilleurs résultats avec un outil inférieur tant qu'ils ont plus de pratique avec lui. Et c'est exactement ce qui apparaîtrait dans toute étude tentant d'examiner dans quelle mesure ses sujets font face à différents styles de codage. Le bruit causé par leur familiarité / méconnaissance préalable avec les styles de codage que vous leur demanderiez d'utiliser éclipserait l'impact de toutes les qualités innées de ces styles. Sauf si vous avez nivelé le terrain de jeu en prenant des débutants complets. Mais cela pose une difficulté pratique, comme je l'ai souligné dans le dernier paragraphe de ma réponse.
Konrad Morawski

4

La réponse est un NON définitif! Est-ce que «casser» et «continuer» sont de mauvaises pratiques de programmation? est un sous-ensemble de cette question, donc je vais commencer par une réponse à peine modifiée à cela ...

Vous pouvez [ré] écrire des programmes sans instructions break (ou retours depuis le milieu des boucles, qui font la même chose). Mais ce faisant, vous devrez peut-être introduire des variables supplémentaires et / ou une duplication de code, ce qui rend généralement le programme plus difficile à comprendre. Pascal (le langage de programmation de la fin des années 1960) était très mauvais en particulier pour les programmeurs débutants pour cette raison.

Il y a un résultat informatique appelé la hiérarchie des structures de contrôle de Kosaraju, qui remonte à 1973 et qui est mentionné dans le (plus) célèbre article Structured Programming de Knuth avec des déclarations de 1974. Ce que S. Rao Kosaraju a prouvé en 1973, c'est que ce n'est pas possible de réécrire tous les programmes qui ont des sauts de profondeur n à plusieurs niveaux dans des programmes avec une profondeur de coupure inférieure à n sans introduire de variables supplémentaires. Mais disons que ce n'est qu'un résultat purement théorique. (Ajoutez simplement quelques variables supplémentaires?! Vous pouvez sûrement le faire pour vous sentir en contact avec les utilisateurs 3K + sur stackexchange ...)

Ce qui est beaucoup plus important du point de vue de l'ingénierie logicielle est un article plus récent de 1995 d'Eric S. Roberts intitulé Loop Exits and Structured Programming: Reopening the Debate (doi: 10.1145 / 199688.199815). Roberts résume plusieurs études empiriques menées par d'autres avant lui. Par exemple, lorsqu'un groupe d'étudiants de type CS101 a été invité à écrire du code pour une fonction implémentant une recherche séquentielle dans un tableau, l'auteur de l'étude a déclaré ce qui suit au sujet des étudiants qui ont utilisé une pause / retour pour quitter le séquentiel. boucle de recherche dès que l'élément a été trouvé:

Je n'ai pas encore trouvé une seule personne qui a tenté un programme en utilisant [ce style] qui a produit une solution incorrecte.

Roberts dit également que:

Les étudiants qui ont tenté de résoudre le problème sans utiliser un retour explicite de la boucle for s'en sont beaucoup moins bien sortis: seuls sept des 42 étudiants qui ont essayé cette stratégie ont réussi à générer des solutions correctes. Ce chiffre représente un taux de réussite inférieur à 20%.

Oui, vous pouvez être plus expérimenté que les étudiants CS101, mais sans utiliser l'instruction break (ou renvoyer / aller de façon équivalente au milieu des boucles), vous finirez par écrire du code qui, bien qu'étant nominalement bien structuré, est assez velu en termes de logique supplémentaire les variables et la duplication de code que quelqu'un, probablement vous-même, y mettra des bogues logiques tout en essayant de suivre une idée de passe de style de codage "correct".

Et il y a un problème plus important ici en plus des instructions return / break-type, donc cette question est un peu plus large que celle concernant les ruptures. Les mécanismes de gestion des exceptions violent également le paradigme du point de sortie unique selon certains

Donc, fondamentalement, tous ceux qui ont soutenu ci-dessus que le principe de sortie unique est toujours utile aujourd'hui argumentent également contre le paradigme de gestion des exceptions, à moins qu'il ne soit utilisé de la manière extrêmement restrictive décrite dans ce dernier lien; ces directives contraignent fondamentalement toutes les exceptions d'une fonction à throw (), c'est-à-dire qu'aucune propagation d'exceptions inter-fonctions n'est autorisée du tout. Profitez de votre nouveau Pascal avec une syntaxe de type C ++.

Je vois d' où vient la notion de "un seul retour"?que l'opinion répandue sur ce site est contraire à ce que j'ai posté ici, donc je comprends parfaitement pourquoi j'ai déjà été rejeté, même si je suis la première réponse ici à fournir quelque chose que la question demandait: quelques informations sur les tests d'utilisabilité réels axés sur le problème de sortie unique. Je suppose que je ne devrais pas laisser la connaissance entraver les idées préconçues, en particulier sur un site de gamification. Je vais m'en tenir à l'édition de Wikipedia à partir de maintenant. Au moins, les informations provenant de bonnes sources sont appréciées et les affirmations vagues ou incorrectes prétendant être soutenues par des sources finissent par être interdites. Sur ce site, c'est tout le contraire qui se produit: les opinions non étayées par les faits dominent. Je m'attends à ce qu'un mod supprime cette dernière partie, mais au moins ce mec saura pourquoi vous m'avez perdu pour toujours en tant que contributeur ici.


Je n'ai pas dévalorisé cela, mais sur votre "Mais ce faisant, vous devrez peut-être introduire des variables supplémentaires et / ou la duplication de code qui rendent généralement le programme plus difficile à comprendre." point, c'est une affirmation subjective. Je suis d'accord que l'ajout d'une duplication de variable ou de code le rend difficile à comprendre, mais sans doute l'ajout d'un goto le rend difficile à comprendre aussi, et sans doute les dommages causés par la duplication peuvent être atténués en factorisant le code dupliqué dans une fonction (bien que le déplacement de l'OMI la complexité du graphique d'appel ne l'élimine pas automatiquement).
Steve314

Je n'ai vu votre point sur le document de 1995 qu'après ce dernier commentaire, et j'ai décidé de voter - point intéressant. Je pense que votre downvote peut être plus parce que votre message est long et commence par un point subjectif, donc probablement le downvoter n'a pas lu le tout (le même que moi, au début). Fondamentalement, c'est une bonne idée de présenter votre vrai point au début.
Steve314

Quoi qu'il en soit, je pense que beaucoup de gens considèrent les exceptions comme une sorte de points de sortie alternatifs alternatifs - parce qu'ils sont destinés aux cas d'erreur (en quelque sorte), ils ne comptent pas vraiment. Je comprends que c'est un peu sensible à la culture linguistique, cependant. Dans certaines langues, «exception» est plus que le nom - un cas de réussite exceptionnel est valide (et IIRC Stroustrup a dit quelque chose comme ça à propos de C ++, soulevant un point philosophique sur le fait qu'une erreur soit une erreur si elle est gérée). Certains disent même que les exceptions ne sont qu'un autre flux de contrôle à utiliser chaque fois qu'il donne le flux de contrôle dont vous avez besoin.
Steve314

1
@ Steve314 " et sans doute les dommages causés par la duplication peuvent être atténués en intégrant le code dupliqué dans une fonction " Mise hors ligne et hors de vue immédiate d'une partie de la logique de la fonction, une partie qui n'a aucun sens isolée. Rendant encore plus difficile la compréhension de la logique de la fonction.
curiousguy

1
@curiousguy - oui, c'est vrai, et probablement une partie de l'intention de mon point de "déplacement de la complexité dans le graphique d'appel". Ma religion est que chaque choix que vous faites est un compromis, alors soyez conscient de toutes les options plausibles et de leurs avantages et inconvénients, et connaître les atténuations communes est important, mais méfiez-vous si la guérison est pire que la maladie. Sauf bien sûr qu'une partie du compromis est le temps que vous passez (ou perdez) à vous occuper de choses.
Steve314

1

http://dl.acm.org/citation.cfm?id=1241526

http://www.springerlink.com/content/n82qpt83n8735l7t/

http://ieeexplore.ieee.org/xpl/freeabs_all.jsp?arnumber=661092

[Vos questions semblent répondre par un seul mot, "oui". On m'a dit, cependant, que fournir des réponses courtes est «dédaigneux» de la question. Si vous pensez que j'ai été dédaigneux, veuillez signaler la réponse afin qu'un modérateur puisse la supprimer.]


1
@ luis.espinal: Vers quelle fin? Quelles informations le texte contiendrait-il? La question tourne un peu autour. Quelle partie de la question devrait être abordée avec un texte?
S.Lott

1
Par style, et peut-être pour fournir plus d'informations que les résumés des liens peuvent fournir (étant donné que nous ne savons pas si l'OP est un membre ACM / IEEE / Springer Verlag payant ayant accès aux articles complets et trouver des réponses à ses questions.) Par exemple, le résumé de l’article ACM ne fait aucune mention du style de codage. Tout au plus parle-t-il de corroborer le théorème du programme structuré (qui lui-même ne parle pas du problème du retour simple ou multiple). Vous auriez donc pu expliquer pourquoi ce lien est pertinent.
luis.espinal

1
Le troisième article (heureusement, j'ai accès à IEEE Xplore) ne semble pas lié à ce que l'OP demande pour autant que je sache. C'est un merveilleux article, mais j'imprime pour une lecture plus approfondie plus tard. Alors peut-être auriez-vous pu aussi expliquer comment cet article aide le PO à répondre à sa question. Dans l'ensemble, il semble que vous ayez simplement jeté un ensemble de liens. Ce n'est pas une manière d'être dédaigneux (sauf si c'était votre intention), mais encore une fois, je ne vois pas comment cela a aidé le PO. Et c'est pourquoi une affiche devrait ajouter du texte le long de ses liens. Alors maintenant, vous savez pourquoi je l'ai dit;)
luis.espinal

1
de la bouche de l'OP Is a coding style principle - e.g. the single-exit principle - really a good thing?- qui donne un contexte à la question qu'il pose, sur les styles de codage. De plus, le style de codage n'est pas la même que la méthodologie de programmation, en particulier les méthodes de conception de haut niveau qui sont au centre de l'article de l'IEEE (clairement indiqué par les auteurs). C'est pourquoi je dis "non" - les portées sont complètement différentes.
luis.espinal

1
Je soupçonne d'où vient le PO. Il indique clairement les styles de codage (pas les méthodologies), et en particulier, les retours simples vs multiples. J'ai dû faire face à cela plusieurs fois avec du code bien écrit et intrinsèquement évident en utilisant plusieurs déclarations de retour réécrites dans des versions plus compliquées en utilisant des retours simples (en particulier dans les grandes organisations lourdes de paperasse) * comme par "le processus". Et l'on se demande (et conteste les preuves) la validité, l'utilisabilité et la rentabilité de ces mandats arbitraires. Les personnes qui imposent de tels mandats vivent toujours dans les années 60: /
luis.espinal

1

Est un principe de style de codage - par exemple le principe de sortie unique

Les gens qui se demandent encore s'il s'agit d'une sortie unique ou de sorties multiples sont toujours bloqués à la fin des années 1960. À l'époque, une telle discussion était importante car nous étions dans les balbutiements du programmeur structuré, et il y avait un camp assez nombreux proclamant que les découvertes derrière le théorème du programme structuré de Bohm-Jacopini n'étaient pas universellement applicables à toutes les constructions de programmation.

C'est quelque chose qui aurait dû être réglé depuis longtemps. Eh bien, c'est réglé (près de 4 décennies pour être précis, à la fois dans le monde universitaire et dans l'industrie), mais les gens (ceux qui sont absolument pro ou contre) n'ont pas fait attention.

Quant au reste de mes réponses, tout est relatif (qu'est-ce qui n'est pas dans le logiciel?):

  • vraiment une bonne chose?

Oui. La plupart du temps pour le cas général, avec des mises en garde spécifiques aux cas marginaux et des constructions de programmation spécifiques au langage.

Toujours ou juste parfois?

Le plus souvent.

Quelle différence cela fait-il vraiment?

Dépend.

Code lisible vs code illisible. Complexité accrue (que nous devrions connaître maintenant augmente la probabilité d'introduction d'erreurs) vs complexité plus simple (et ergo, probabilité d'erreurs plus faible). Langages dont les compilateurs n'ajoutent pas de retour implicite (par exemple, Pascal, Java ou C #) et ceux qui par défaut à int (C et C ++).

En fin de compte, c'est une compétence affinée avec des heures / homme derrière un clavier. Parfois, c'est correct d'avoir plusieurs instructions de retour, comme ici (dans certains pseudocodes Pascal'esque):

function foo() : someType
  begin
  if( test1 == true )
  then
    return x;
  end
  doSomethignElseThatShouldnHappenIfTest1IsTrue();
  return somethingElse();
end;

L'intention est claire, et l'algorithme est suffisamment petit et assez simple pour qu'il ne justifie pas la création d'une variable «drapeau» qui contient la valeur de retour éventuelle utilisée dans un seul point de retour. L'algorithme peut être erroné, mais sa structure est suffisamment simple pour que l'effort de détection d'une erreur soit (très probablement) négligeable.

Parfois, ce n'est pas le cas (en utilisant ici un pseudocode de type C):

switch(someVal)
{
case v1 : return x1;
case v2 : return x2:
case v3 : doSomething(); // fall-through
case v4: // fall-through
case v5: // fall-through
case v6: return someXthingie;
...
...
default:
   doSomething(); // no return statement yet
}

Ici, l'algorithme n'a pas de structure simple et l'instruction switch (de style C) permet des étapes de passage qui peuvent ou non être effectuées intentionnellement dans le cadre de l'algorithme.

Peut-être que l'algorithme est correct, mais mal écrit.

Ou peut-être, par des forces externes au-delà des capacités du programmeur, c'est la représentation réelle (et correcte) d'un algorithme légitimement nécessaire.

C'est peut-être faux.

Pour découvrir la vérité de tout cela, il faut beaucoup plus d'efforts que dans l'exemple précédent. Et là se trouve quelque chose que je crois fermement (sachez que je n'ai aucune étude formelle à l'appui):

En supposant un extrait de code supposé correct:

  1. Les instructions de retour multiples augmentent la lisibilité et la simplicité d'un tel extrait de code, si l'extrait de code représente un algorithme simple avec une structure de flux intrinsèquement simple. Par simple, je ne veux pas dire petit, mais je veux dire intrinsèquement compréhensible ou évidence , ce qui ne nécessite pas d'effort de lecture disproportionné (ni inciter les gens à vomir, à maudire la mère de quelqu'un ou à avaler une balle quand ils doivent le lire. )

  2. Une seule déclaration de retour augmente la lisibilité et la simplicité d'un tel morceau de code si la valeur de retour est calculée tout au long de l'exécution de l'algorithme ou si les étapes de l'algorithme chargé de le calculer peuvent être regroupées en un seul emplacement dans la structure de l'algorithme .

  3. Une seule instruction de retour diminue la lisibilité et la simplicité d'un tel morceau de code si elle nécessite des affectations à une ou plusieurs variables d'indicateur, les emplacements de ces affectations n'étant pas uniformément situés dans tout l'algorithme.

  4. Les instructions de retour multiples réduisent la lisibilité et la simplicité d'un tel morceau de code si les instructions de retour ne sont pas réparties uniformément dans l'algorithme et si elles délimitent des blocs de code mutuellement exclusifs qui ne sont pas de taille ou de structure uniformes entre eux.

Cela est étroitement lié à la complexité d'un extrait de code en question. Et ceci est à son tour lié aux mesures de complexité cyclomatiques et halstead. De cela, on a pu observer ce qui suit:

Plus la taille d'un sous-programme ou d'une fonction est grande, plus sa structure de flux de contrôle interne est grande et complexe, et plus vous serez confronté à la question d'utiliser ou non des instructions de retour multiples ou complexes.

La conclusion de ceci est: gardez vos fonctions petites en faisant une seule et unique chose (et en le faisant bien). S'ils présentent des métriques de complexité cyclomatique et halstead nominalement réduites, non seulement ils sont très probablement corrects et implémentent des tâches qui sont compréhensibles, mais leurs structures internes seront également relativement évidentes.

Ensuite, et seulement alors, vous pouvez assez facilement et sans perdre beaucoup de sommeil, vous pouvez décider d'utiliser un seul retour et plusieurs retours sans courir beaucoup de risques d'introduire des erreurs avec l'un ou l'autre choix.

On pourrait également examiner tout cela et suggérer que lorsque les gens sont aux prises avec la question des retours uniques ou des retours multiples, c'est parce que - soit par inexpérience, par stupidité ou par manque d'éthique du travail - ils n'écrivent pas de code propre et ont tendance à écrire fonctions monstrueuses avec mépris total des mesures cyclomatiques et halstead.


1
Le type de retour C ++ n'est pas par défaut int: il n'y a pas de type de retour par défaut, il doit donc être spécifié dans tous les cas.
Sjoerd

Avant d'écrire cette question - programmers.stackexchange.com/questions/58237/… . Fondamentalement, je préconise la prise de conscience du principe, mais pas strictement le suivre - si tous les points de sortie sont évidents, je suis heureux. Ce que je veux dire ici - ce n'est pas parce que je mentionne un principe comme exemple que je préconise ce principe, et certainement pas sous sa forme stricte. Mon opinion subjective est juste que, cependant - peut-être il y a un argument plus fort pour mon avis, ou peut-être il y a un argument fort que je me trompe.
Steve314

Qu'est-ce que "default to int"?
curiousguy

Je veux dire, et j'aurais dû le qualifier, que la plupart des compilateurs vont simplement "pousser" la valeur d'un registre d'accumulateur comme valeur de retour si le code se trouve avoir une branche d'exécution sans valeur de retour explicite. Cela signifie en fait renvoyer le résultat de la dernière opération arithmétique (quelles que soient les ordures qui pourraient être) sous forme int. Et ce serait certainement des ordures (et ergo, un comportement indéfini) indépendamment de ce que la fonction avait l'intention de faire en premier lieu. C et C ++ peuvent vous avertir, mais les compilations vous permettront de compiler à moins que vous n'utilisiez -Werror ou quelque chose de similaire.
luis.espinal
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.