Pourquoi l'encodage des noms d'arguments dans les noms de fonction n'est-il pas plus courant? [fermé]

47

Dans Clean Code, l'auteur donne un exemple de

assertExpectedEqualsActual(expected, actual)

contre

assertEquals(expected, actual)

avec l’ancien prétendu être plus clair parce que cela supprime la nécessité de se rappeler où vont les arguments et les abus qui en découlent. Pourtant, je n'ai jamais vu un exemple du premier système de nommage dans un code et je vois ce dernier tout le temps. Pourquoi les codeurs n'adoptent-ils pas le premier si, comme le prétend l'auteur, il est plus clair que le dernier?

Étudiant éternel
la source
9
Je pense que c'est une excellente question pour une discussion. Mais pas quelque chose qui peut être répondu avec une réponse objective. Donc, cette question pourrait être fermée en tant qu'opinion.
Euphoric
54
De nombreuses personnes s'opposeraient au premier système de nommage, car il est excessivement prolixe , bien au-delà du point où cela contribuerait à la clarté. Surtout parce assertEquals()que cette méthode est utilisée des centaines de fois dans une base de code, on peut donc s’attendre à ce que les lecteurs se familiarisent une fois avec la convention. Différents cadres ont différentes conventions (par exemple (actual, expected) or an agnostic (gauche, droite) `), mais d'après mon expérience, c'est tout au plus une source de confusion mineure.
amon
5
Parce que le gain est si faible, comparé à ses avantages, que n'importe quel homme sensé pourrait probablement s'en aller. Si vous souhaitez une approche plus fluide , vous devriez essayer assert(a).toEqual(b)(même si, à l’OMI, elle reste inutilement prolixe) où vous pouvez enchaîner quelques affirmations connexes.
Adriano Repetti
18
Comment savons-nous que les valeurs réelles et attendues sont? Cela devrait sûrement être assertExpectedValueEqualsActualValue? Mais attendez, comment nous rappelons-nous s'il utilise ==ou .equalsou Object.equals? Cela devrait-il être assertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter?
user253751
6
Étant donné que, pour cette méthode particulière, l'ordre des deux arguments importe peu, il semble que ce soit un mauvais exemple pour choisir de profiter des avantages de ce schéma de nommage.
Steven Rands le

Réponses:

66

Parce que c’est plus à taper et plus à lire

La raison la plus simple est que les gens aiment taper moins, et coder cette information signifie plus de dactylographie. En le lisant, je dois tout lire à chaque fois, même si je connais parfaitement l'ordre des arguments. Même si pas familier avec l'ordre des arguments ...

De nombreux développeurs utilisent des IDE

Les IDE fournissent souvent un mécanisme permettant d'afficher la documentation d'une méthode donnée en survolant ou via un raccourci clavier. Pour cette raison, les noms des paramètres sont toujours à portée de main.

Le codage des arguments introduit la duplication et le couplage

Les noms des paramètres devraient déjà documenter ce qu’ils sont. En écrivant les noms dans le nom de la méthode, nous dupliquons également ces informations dans la signature de la méthode. Nous créons également un couplage entre le nom de la méthode et les paramètres. Dites expectedet actualsont déroutants à nos utilisateurs. Aller de assertEquals(expected, actual)à assertEquals(planned, real)ne nécessite pas de changer le code client en utilisant la fonction. Passer de assertExpectedEqualsActual(expected, actual)à assertPlannedEqualsReal(planned, real)signifie un changement radical de l'API. Ou nous ne changeons pas le nom de la méthode, ce qui devient rapidement déroutant.

Utilisez des types au lieu d'arguments ambigus

Le vrai problème est que nous avons des arguments ambigus qui sont facilement interchangeables car ils sont du même type. Nous pouvons plutôt utiliser notre système de types et notre compilateur pour imposer le bon ordre:

class Expected<T> {
    private T value;
    Expected(T value) { this.value = value; }
    static Expected<T> is(T value) { return new Expected<T>(value); }
}

class Actual<T> {
    private T value;
    Actual(T value) { this.value = value; }
    static Actual<T> is(T value) { return new Actual<T>(value); }
}

static assertEquals(Expected<T> expected, Actual<T> actual) { /* ... */ }

// How it is used
assertEquals(Expected.is(10), Actual.is(x));

Cela peut ensuite être appliqué au niveau du compilateur et garantit que vous ne pouvez pas les récupérer à l'envers. En abordant le problème sous un angle différent, c'est essentiellement ce que la bibliothèque Hamcrest fait pour les tests.

cbojar
la source
5
Eh bien, si vous utilisez un IDE, vous avez les noms des paramètres dans l'aide de la bulle. Si vous n'en utilisez pas, le fait de mémoriser le nom de la fonction revient à mémoriser les arguments. Par conséquent, rien n'est gagné.
Peter - Réintégrez Monica le
29
Si vous vous opposez à assertExpectedEqualsActual"parce que c’est plus à taper et plus à lire", comment pouvez-vous plaider assertEquals(Expected.is(10), Actual.is(x))?
Ruakh le
9
@ruakh ce n'est pas comparable. assertExpectedEqualsActualdemande toujours au programmeur de spécifier les arguments dans le bon ordre. La assertEquals(Expected<T> expected, Actual<T> actual)signature utilise le compilateur pour imposer l'utilisation correcte, ce qui constitue une approche totalement différente. Vous pouvez optimiser cette approche par souci de concision, par exemple expect(10).equalsActual(x), mais ce n'était pas la question…
Holger
6
De plus, dans ce cas particulier (==), l'ordre des arguments est en réalité sans importance pour la valeur finale. L'ordre n'a d'importance que pour un effet secondaire (signaler l'échec). Lorsque vous commandez est important, cela peut donner (légèrement) plus de sens. Par exemple, strcpy (dest, src).
Kristian H
1
Ne pouvez pas être plus d’accord, en particulier avec la partie sur la duplication et le couplage ... Si chaque fois qu’un paramètre d’une fonction change de nom, le nom de la fonction doit également changer, vous devez suivre tous les usages de cette fonction et changez-les aussi ... Cela ferait un tas de changements radicaux pour moi, mon équipe et tous les autres en utilisant notre code comme dépendance ...
mrsmn
20

Vous demandez un débat de longue date dans la programmation. Combien de verbosité c'est bien? De manière générale, les développeurs ont constaté que la verbosité supplémentaire nommant les arguments n'en valait pas la peine.

Verbosité ne signifie pas toujours plus de clarté. Considérer

copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)

contre

copy(output, source)

Les deux contiennent le même bogue, mais avons-nous réellement rendu plus facile la recherche de ce bogue? En règle générale, la meilleure chose à faire pour le débogage est lorsque tout est extrêmement sommaire, à l'exception des quelques éléments qui présentent le bogue et qui sont suffisamment détaillés pour vous dire ce qui n'a pas fonctionné.

Il y a une longue histoire d'ajout de verbosité. Par exemple, il y a la " notation hongroise " généralement impopulaire qui nous a donné des noms merveilleux comme lpszName. Cela a généralement été mis de côté dans la foule des programmeurs. Cependant, l'ajout de caractères aux noms de variable de membre (comme mNameou m_Nameou name_) continue d'avoir une certaine popularité dans certains cercles. D'autres ont tout laissé tomber. Il m'est arrivé de travailler sur une base de code de simulation physique dont les documents de style de codage exigent que toute fonction renvoyant un vecteur spécifie la trame du vecteur dans l'appel de fonction ( getPositionECEF).

Certaines des langues rendues populaires par Apple pourraient vous intéresser. Objective-C inclut les noms d'arguments dans la signature de la fonction (la fonction [atm withdrawFundsFrom: account usingPin: userProvidedPin]est écrite dans la documentation en tant que withdrawFundsFrom:usingPin:. C'est le nom de la fonction). Swift a pris un ensemble similaire de décisions, vous obligeant à mettre les noms d'arguments dans les appels de fonction ( greet(person: "Bob", day: "Tuesday")).

Cort Ammon
la source
13
Tous les autres points mis à part, ce serait beaucoup plus facile à lire si ils copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)étaient écrits copy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle). Vous voyez comme c'était plus facile ?! En effet, il est trop facile de rater de petits changements au milieu de cet humungousunbrokenwordsalad, et il faut plus de temps pour déterminer où se trouvent les limites de mots. Smashing confuse.
tchrist
1
La syntaxe obj-C withdrawFundsFrom: account usingPin: userProvidedPinest en fait empruntée à SmallTalk.
joH1
14
@tchrist soyez prudent lorsque vous êtes certain d'avoir raison sur des sujets liés aux guerres saintes. L'autre côté n'a pas toujours tort.
Cort Ammon
3
@tchrist Addingunderscoresnakesthingseasiertoreadnotharderasyouseemanipule l'argument. La réponse ici a utilisé la capitalisation, que vous omettez. AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere. Deuxièmement, 9 fois sur 10, un nom ne devrait jamais dépasser [verb][adjective][noun](où chaque bloc est facultatif), un format qui est bien lisible avec une simple capitalisation:ReadSimpleName
Flater
5
@tchrist - la science de votre étude ( lien en texte intégral gratuit ) montre simplement que les programmeurs formés à utiliser le style de soulignement sont plus rapides à lire que le style chameau. Les données montrent également que la différence est moins grande pour les matières les plus expérimentées (et la plupart des matières étudiées suggèrent que même celles-ci n'étaient probablement pas spécialement expérimentées). Cela ne signifie pas que les programmeurs qui ont passé plus de temps à utiliser chamel case donneront également le même résultat.
Jules
8

L'auteur de "Clean Code" signale un problème légitime, mais sa solution suggérée est plutôt inélégante. Il existe généralement de meilleurs moyens d'améliorer les noms de méthodes peu clairs.

Il a raison de dire que assertEquals(à partir des bibliothèques de tests d'unités de style xUnit), il n'est pas précisé quel argument est attendu et quel est l'argument réel. Cela m'a aussi mordu! De nombreuses bibliothèques de tests unitaires ont noté le problème et ont introduit des syntaxes alternatives, telles que:

actual.Should().Be(expected);

Ou similaire. Ce qui est certainement beaucoup plus clair que assertEqualsmais aussi beaucoup mieux que assertExpectedEqualsActual. Et c'est aussi beaucoup plus composable.

JacquesB
la source
1
Je suis anal et je respecte l'ordre recommandé, mais il me semble que si je m'attends à ce que le résultat fun(x)soit égal à 5, alors qu'est-ce qui pourrait mal tourner si je renverse l'ordre - assert(fun(x), 5)? Comment ça vous a mordu?
Emory
3
@emory Je sais que jUnit (au moins) construit un message d'erreur détaillé à partir des valeurs de expectedand actual, donc les inverser peut entraîner un message inexact. Mais je suis d'accord pour dire que cela semble plus naturel :)
joH1
@ joH1 ça me semble faible. le code défaillant échouera et le code de passage sera transmis que vous le fassiez assert(expected, observed)ou non assert(observed, expected). Un meilleur exemple serait quelque chose comme locateLatitudeLongitude: si vous inversez les coordonnées, cela gâchera sérieusement.
Emory
1
@emory Les personnes qui ne se soucient pas des messages d'erreur sensibles dans les tests unitaires sont la raison pour laquelle je dois traiter avec "Assert.IsTrue failed" dans certaines anciennes bases de code. Ce qui est extrêmement amusant à déboguer. Mais oui dans ce cas, le problème pourrait ne pas être essentiel (sauf si nous faisons des comparaisons floues où l'ordre des arguments est généralement important). Les assertions Fluent sont en effet un excellent moyen d’éviter ce problème et de rendre le code plus expressif (et de fournir un message d’erreur beaucoup mieux à démarrer).
Voo le
@emory: Inverser l'argument rendra les messages d'erreur trompeurs et vous enverra le mauvais chemin lors du débogage.
JacquesB
5

Vous essayez d’orienter votre chemin entre Scylla et Charybde vers la clarté, en essayant d’éviter une verbosité inutile (aussi connue sous le nom de balade sans but), ainsi qu’une brièveté excessive (également connue sous le nom de terseness cryptique).

Nous devons donc examiner l’interface que vous voulez évaluer, une façon de faire des assertions de débogage que deux objets sont égaux.

  1. Y a-t-il une autre fonction qu'il pourrait être de considérer l'arité et le nom?
    Non, le nom lui-même est assez clair.
  2. Les types sont-ils importants?
    Non, alors ignorons-les. Vous l'avez déjà fait? Bien.
  3. Est-ce symétrique dans ses arguments?
    Presque, en cas d'erreur, le message met chaque représentation d'arguments à sa place.

Voyons donc si cette petite différence est importante et n’est pas couverte par les conventions fortes existantes.

L'audience visée est-elle gênée si les arguments sont échangés involontairement?
Non, les développeurs reçoivent également une trace de pile et ils doivent quand même examiner le code source pour corriger le bogue.
Même sans trace de pile complète, la position des assertions résout cette question. Et même si cela manque et que le message n’est pas une évidence, il double tout au plus les possibilités.

Est-ce que l'ordre des arguments suit la convention?
Semble être le cas. Bien que cela semble au mieux une convention faible.

Ainsi, la différence semble assez insignifiante et l'ordre des arguments est couvert par une convention suffisamment forte pour que tout effort visant à le coder dans le nom de la fonction ait une utilité négative.

Déduplicateur
la source
bien que l'ordre puisse avoir de l'importance avec jUnit, qui génère un message d'erreur spécifique à partir des valeurs de expectedet actual(au moins avec Strings)
joH1
Je pense avoir couvert cette partie ...
Déduplicateur
vous l'avez mentionné, mais considérez: assertEquals("foo", "doo")donne le message d'erreur est ComparisonFailure: expected:<[f]oo> but was:<[d]oo>... Permuter les valeurs inverserait le sens du message, cela me semble plus anti symétrique. Quoi qu'il en soit, comme vous l'avez dit, un développeur a d'autres indicateurs pour résoudre l'erreur, mais cela peut être trompeur à mon humble avis et prendre un peu plus de temps pour le débogage.
joH1
L'idée qu'il existe une "convention" pour les ordres d'arguments est drôle, étant donné que les deux camps (dest, src contre src, dest) ont argumenté à ce sujet depuis au moins aussi longtemps que la syntaxe AT & T vs. Intel a existé. Et les messages d'erreur inutiles dans les tests unitaires sont un fléau qui devrait être éradiqué et non imposé. C’est presque aussi mauvais que "Assert.IsTrue failed" ("Hé, vous devez exécuter le test unitaire de toute façon pour le déboguer, alors il suffit de le réexécuter et de mettre un point d’arrêt là", "hé vous devez regarder le code de toute façon, alors il suffit de vérifier si la commande est correcte ").
Voo le
@Voo: Le fait est que le "dommage" de l'erreur est minime (la logique n'en dépend pas, et l'utilitaire de messagerie n'est pas altéré de manière significative), et lors de l'écriture de l'EDI, le nom du paramètre vous sera montré. et tapez quand même.
Déduplicateur
3

Souvent, cela n’ajoute aucune clarté logique.

Comparez "Ajouter" à "AddFirstArgumentToSecondArgument".

Si vous avez besoin d'une surcharge, par exemple, ajoute trois valeurs. Qu'est-ce qui aurait plus de sens?

Un autre "Ajouter" avec trois arguments?

ou

"AddFirstAndSecondAndThirdArgument"?

Le nom de la méthode doit transmettre sa signification logique. Il devrait dire ce qu'il fait. Dire, au niveau micro, quelles sont les étapes à suivre ne facilite pas le lecteur. Les noms des arguments fourniront des détails supplémentaires si nécessaire. Si vous avez encore besoin de plus de détails, le code sera là pour vous.

Martin Maat
la source
4
Addsuggère une opération commutative. Le PO s'intéresse aux situations dans lesquelles l'ordre est important.
Rosie F
Dans Swift, vous appelez par exemple add (5, à: x) ou add (5, plus: 7, à: x) ou add (5, plus: 7, donnant: x) si vous définissez la fonction add () en conséquence.
gnasher729
La troisième surcharge devrait être nommée "Sum"
StingyJack
@StringyJack Hmm .. Sum n'est pas une instruction, c'est un nom qui le rend moins approprié pour un nom de méthode. Mais si vous vous sentez ainsi et si vous voulez être un puriste à ce sujet, la version à deux arguments devrait également s'appeler Sum. Si vous deviez avoir une méthode Add, elle devrait avoir un argument ajouté à l'instance d'objet lui-même (qui devrait être un type numérique ou vectoriel). Les 2 ou plusieurs variétés d'argument (quel que soit le nom que vous leur donneriez) seraient statiques. Alors les versions à 3 arguments ou plus seraient redondantes et nous aurions implémenté un opérateur plus: - |
Martin Maat
1
@ Martin Attendez quoi? sumest un verbe parfaitement cromulent . Il est particulièrement courant dans la phrase "résumer".
Voo le
2

J'aimerais ajouter quelque chose d'autre qui est suggéré par d'autres réponses, mais je ne pense pas que cela ait été mentionné explicitement:

@puck dit "Il n'y a toujours aucune garantie que le premier argument mentionné dans le nom de la fonction soit réellement le premier paramètre."

@cbojar dit "Utilisez des types au lieu d'arguments ambigus"

Le problème est que les langages de programmation ne comprennent pas les noms: ils sont simplement traités comme des symboles opaques et atomiques. Ainsi, à l'instar des commentaires de code, il n'y a pas nécessairement de corrélation entre le nom d'une fonction et son fonctionnement réel.

Comparez assertExpectedEqualsActual(foo, bar)avec certaines alternatives (à partir de cette page et ailleurs), comme:

# Putting the arguments in a labelled structure
assertEquals({expected: foo, actual: bar})

# Using a keyword arguments language feature
assertEquals(expected=foo, actual=bar)

# Giving the arguments different types, forcing us to wrap them
assertEquals(Expected(foo), Actual(bar))

# Breaking the symmetry and attaching the code to one of the arguments
bar.Should().Be(foo)

Celles-ci ont toutes plus de structure que le nom commenté, ce qui donne au langage un aspect non opaque à regarder. La définition et l'utilisation de la fonction dépendent également de cette structure. Elle ne peut donc pas être désynchronisée par rapport à ce que fait l'implémentation (comme un nom ou un commentaire).

Lorsque je rencontre ou prévois un problème de ce type, avant de crier sur mon ordinateur de frustration, je prends d'abord un moment pour demander s'il est «juste» de blâmer la machine. En d'autres termes, la machine a-t-elle reçu suffisamment d'informations pour distinguer ce que je voulais de ce que je demandais?

Un appel de ce type a assertEqual(expected, actual)tout autant de sens que nous assertEqual(actual, expected), il est donc facile pour nous de les mélanger et pour que la machine avance et fasse la mauvaise chose. Si nous avons utilisé à la assertExpectedEqualsActualplace, il pourrait faire nous moins susceptibles de faire une erreur, mais il ne donne pas plus d' informations à la machine (il ne peut pas comprendre l' anglais, et le choix du nom ne devrait pas affecter la sémantique).

Ce qui rend les approches "structurées" plus préférables, telles que les arguments de mots clés, les champs libellés, les types distincts, etc., est que les informations supplémentaires sont également lisibles par machine , de sorte que la machine détecte les utilisations incorrectes et nous aide à bien faire les choses. Le assertEqualcas n’est pas trop grave, le seul problème étant des messages inexacts. Un exemple plus sinistre pourrait être String replace(String old, String new, String content), qui est facile à confondre avec String replace(String content, String old, String new)qui a un sens très différent. Un remède simple serait de prendre une paire [old, new], ce qui ferait des erreurs déclencher une erreur immédiatement (même sans types).

Notez que même avec les types, nous pouvons nous retrouver à ne pas "dire à la machine ce que nous voulons". Par exemple, l'anti-motif appelé "programmation très typée" traite toutes les données en tant que chaînes, ce qui permet de mélanger facilement les arguments (comme dans ce cas), d'oublier l'exécution d'une étape (par exemple l'échappement), de casser accidentellement des invariants (par exemple rendant JSON imparable), etc.

Ceci est également lié à la "cécité booléenne", où nous calculons un groupe de booléens (ou de nombres, etc.) dans une partie du code, mais lorsque vous essayez de les utiliser dans une autre, vous ne savez pas exactement ce qu'ils représentent, nous les avons mélangés, etc. Comparez ceci par exemple à des énumérations distinctes qui ont des noms descriptifs (par exemple LOGGING_DISABLEDplutôt que false) et qui provoquent un message d'erreur si nous les mélangeons.

Warbo
la source
1

parce que cela évite d'avoir à se rappeler où vont les arguments

Est-ce vraiment? Il n'y a toujours aucune garantie que le premier argument mentionné dans le nom de la fonction soit réellement le premier paramètre. Il est donc préférable de regarder (ou de laisser votre IDE le faire) et de conserver des noms raisonnables plutôt que de vous fier aveuglément à un nom assez stupide.

Si vous lisez le code, vous devriez facilement voir ce qui se passe lorsque les paramètres sont nommés comme il se doit. copy(source, destination)est beaucoup plus facile à comprendre que quelque chose comme copyFromTheFirstLocationToTheSecondLocation(placeA, placeB).

Pourquoi les codeurs n'adoptent-ils pas le premier si, comme le prétend l'auteur, il est plus clair que le dernier?

Parce qu'il existe différents points de vue sur différents styles et que vous pouvez trouver x auteurs d'autres articles affirmant le contraire. Tu deviendrais fou en essayant de suivre tout ce que quelqu'un écrit quelque part ;-)

palet
la source
0

Je conviens que le codage des noms de paramètres en noms de fonctions rend l'écriture et l'utilisation de fonctions plus intuitive.

copyFromSourceToDestination( // "...ahh yes, the source directory goes first"

Il est facile d'oublier l'ordre des arguments dans les fonctions et les commandes du shell et de nombreux programmeurs s'appuient sur les fonctionnalités IDE ou les références de fonctions pour cette raison. Avoir les arguments décrits dans le nom serait une solution éloquente à cette dépendance.

Cependant, une fois écrite, la description des arguments devient redondante pour le prochain programmeur qui doit lire l'instruction, car dans la plupart des cas, des variables nommées seront utilisées.

copy(sourceDir, destinationDir); // "...makes sense"

La brièveté de ceci va gagner la plupart des programmeurs et je trouve personnellement cela plus facile à lire.

EDIT: Comme l'a souligné @Blrfl, les paramètres de codage ne sont pas si intuitifs après tout, car vous devez vous rappeler le nom de la fonction. Cela nécessite de rechercher des références de fonction ou d'obtenir de l'aide d'un IDE qui fournira probablement des informations sur le classement des paramètres.

Josh Taylor
la source
9
Donc, si je peux me faire l'avocat du diable pendant une minute: c'est intuitif quand vous connaissez le nom complet de la fonction. Si vous savez qu'il existe une fonction de copie et que vous ne vous rappelez pas si c'est copyFromSourceToDestinationou copyToDestinationFromSource, vos choix la trouveront par essais et erreurs ou en lisant le matériel de référence. Les IDE pouvant compléter des noms partiels ne sont qu'une version automatisée de cette dernière.
Blrfl
@Blrfl L'intérêt de l'appeler copyFromSourceToDestinationest que si vous le pensez copyToDestinationFromSource, le compilateur trouvera votre bogue, mais s'il l'a appelé copy, il ne le fera pas. Obtenir les paramètres d'une routine de copie dans le mauvais sens est facile, car strcpy, strcat, etc. créent un précédent. Et le résumé est-il plus facile à lire? MergeLists (listA, listB, listC) crée-t-il listA à partir de listB & listC, ou lit-t-il listA & listB et écrit-il listC?
Rosie F
4
@RosieF Si je n'étais pas certain de la signification des arguments, je lirais la documentation avant d'écrire le code. En outre, même avec les noms de fonction plus verbeux, il est toujours possible d’interpréter l’ordre réel. Une personne qui examine le code de façon lointaine ne sera pas en mesure de comprendre que vous avez établi la convention selon laquelle le nom du nom de la fonction reflète l'ordre des arguments. Ils devaient toujours le savoir à l'avance ou lire la documentation.
Blrfl
OTOH, destinationDir.copy (sourceDir); // "... a plus de sens"
Kristian H
1
@KristianH Quelle direction dir1.copy(dir2)fonctionne? Aucune idée. Qu'en est- il dir1.copyTo(dir2)?
Maaartinus