In Clean Code gibt der Autor ein Beispiel für
assertExpectedEqualsActual(expected, actual)
vs
assertEquals(expected, actual)
Ersteres soll klarer sein, weil es die Notwendigkeit beseitigt, sich daran zu erinnern, wohin die Argumente führen, und den möglichen Missbrauch, der daraus resultiert. Dennoch habe ich in keinem Code ein Beispiel für das frühere Benennungsschema gesehen und sehe das letztere die ganze Zeit. Warum nehmen Programmierer das erstere nicht an, wenn es, wie der Autor behauptet, klarer ist als das letztere?
clean-code
EternalStudent
quelle
quelle
assertEquals()
wird diese Methode hunderte Male in einer Codebasis verwendet, so dass zu erwarten ist, dass sich die Leser einmal mit der Konvention vertraut machen. Unterschiedliche Frameworks haben unterschiedliche Konventionen (z. B.(actual, expected) or an agnostic
(links, rechts)), aber meiner Erfahrung nach ist dies höchstens eine geringfügige Ursache für Verwirrung.assert(a).toEqual(b)
(auch wenn IMO immer noch unnötig ausführlich ist), einige verwandte Behauptungen zu verketten.assertExpectedValueEqualsActualValue
? Aber warte, wie können wir uns erinnern, ob es verwendet==
oder.equals
oderObject.equals
? Sollte es seinassertExpectedValueEqualsMethodReturnsTrueWithActualValueParameter
?Antworten:
Weil es mehr zu tippen und mehr zu lesen ist
Der einfachste Grund ist, dass die Leute gerne weniger tippen, und das Kodieren dieser Informationen bedeutet mehr Tippen. Wenn ich es lese, muss ich jedes Mal das Ganze lesen, auch wenn ich mit der Reihenfolge der Argumente vertraut bin. Auch wenn Sie mit der Reihenfolge der Argumente nicht vertraut sind ...
Viele Entwickler verwenden IDEs
IDEs bieten häufig einen Mechanismus zum Anzeigen der Dokumentation für eine bestimmte Methode durch Bewegen des Mauszeigers oder über eine Tastenkombination. Aus diesem Grund sind die Namen der Parameter immer verfügbar.
Das Kodieren der Argumente führt zu Duplizierung und Kopplung
Die Namen der Parameter sollten bereits dokumentieren, was sie sind. Indem wir die Namen in den Methodennamen schreiben, duplizieren wir diese Informationen auch in der Methodensignatur. Wir erstellen auch eine Kopplung zwischen dem Methodennamen und den Parametern. Sagen Sie
expected
undactual
verwirren Sie zu unseren Benutzern. Das Wechseln vonassertEquals(expected, actual)
zuassertEquals(planned, real)
erfordert keine Änderung des Client-Codes mithilfe der Funktion. Das Wechseln vonassertExpectedEqualsActual(expected, actual)
zuassertPlannedEqualsReal(planned, real)
bedeutet eine grundlegende Änderung an der API. Oder wir ändern den Methodennamen nicht, was schnell verwirrend wird.Verwenden Sie Typen anstelle mehrdeutiger Argumente
Das eigentliche Problem ist, dass wir mehrdeutige Argumente haben, die leicht gewechselt werden können, weil sie vom gleichen Typ sind. Wir können stattdessen unser Typensystem und unseren Compiler verwenden, um die richtige Reihenfolge zu erzwingen:
Dies kann dann auf Compilerebene erzwungen werden und garantiert, dass Sie sie nicht rückwärts erhalten können. Aus einem anderen Blickwinkel betrachtet ist dies im Wesentlichen das, was die Hamcrest- Bibliothek für Tests tut.
quelle
assertExpectedEqualsActual
"weil es mehr um das Schreiben und mehr um das Lesen geht", wie können Sie dann dafür eintretenassertEquals(Expected.is(10), Actual.is(x))
?assertExpectedEqualsActual
Der Programmierer muss weiterhin darauf achten, die Argumente in der richtigen Reihenfolge anzugeben. DieassertEquals(Expected<T> expected, Actual<T> actual)
Signatur verwendet den Compiler, um die korrekte Verwendung zu erzwingen. Dies ist ein völlig anderer Ansatz. Sie können diesen Ansatz für die Kürze optimierenexpect(10).equalsActual(x)
, aber das war nicht die Frage ...Sie fragen nach einer langjährigen Debatte in der Programmierung. Wie viel Ausführlichkeit ist gut? Generell haben die Entwickler festgestellt, dass sich die zusätzliche Ausführlichkeit, mit der die Argumente benannt werden, nicht lohnt.
Ausführlichkeit bedeutet nicht immer mehr Klarheit. Erwägen
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
gegen
copy(output, source)
Beide enthielten den gleichen Fehler, aber haben wir es tatsächlich einfacher gemacht, diesen Fehler zu finden? In der Regel ist das Debuggen am einfachsten, wenn alles maximal knapp ist, außer den wenigen Dingen, die den Fehler aufweisen, und die ausführlich genug sind, um Ihnen mitzuteilen, was schief gelaufen ist.
Das Hinzufügen von Ausführlichkeit hat eine lange Tradition. Zum Beispiel gibt es die allgemein unpopuläre " ungarische Notation ", die uns wunderbare Namen wie gab
lpszName
. Das ist in der allgemeinen Programmierer-Bevölkerung in der Regel auf der Strecke geblieben. Das Hinzufügen von Zeichen zu Mitgliedsvariablennamen (wiemName
oderm_Name
odername_
) ist jedoch in einigen Kreisen weiterhin beliebt. Andere ließen das gänzlich fallen. Ich arbeite gerade an einer Physiksimulations-Codebasis, deren Codierungsstil-Dokumente erfordern, dass jede Funktion, die einen Vektor zurückgibt, den Rahmen des Vektors im Funktionsaufruf (getPositionECEF
) spezifizieren muss .Möglicherweise interessieren Sie sich für einige der von Apple verbreiteten Sprachen. Objective-C enthält die Argumentnamen als Teil der Funktionssignatur (Die Funktion
[atm withdrawFundsFrom: account usingPin: userProvidedPin]
ist in der Dokumentation als geschriebenwithdrawFundsFrom:usingPin:
. Das ist der Name der Funktion). Swift traf ähnliche Entscheidungen und forderte Sie auf, die Argumentnamen in die Funktionsaufrufe (greet(person: "Bob", day: "Tuesday")
) einzufügen .quelle
copyFromSourceStreamToDestinationStreamWithoutBlocking(fileStreamFromChoosePreferredOutputDialog, heuristicallyDecidedSourceFileHandle)
es geschrieben wärecopy_from_source_stream_to_destination_stream_without_blocking(file_stream_from_choose_preferred_output_dialog, heuristically_decided_source_file_handle)
. Sehen Sie, wie viel einfacher das war ?! Das liegt daran, dass es zu leicht ist, kleine Änderungen in der Mitte dieses Salats mit ungebrochenen Worten zu übersehen, und dass es länger dauert, herauszufinden, wo die Wortgrenzen liegen. Zertrümmern verwirrt.withdrawFundsFrom: account usingPin: userProvidedPin
ist tatsächlich von SmallTalk entlehnt.Addingunderscoresnakesthingseasiertoreadnotharderasyousee
manipuliert das Argument. Die Antwort hier verwendete Großschreibung, die Sie weglassen.AddingCapitalizationMakesThingsEasyEnoughToReadAsYouCanSeeHere
. Zweitens sollte ein Name 9-mal von 10 niemals[verb][adjective][noun]
ReadSimpleName
Der Autor von "Clean Code" weist auf ein legitimes Problem hin, sein Lösungsvorschlag ist jedoch eher unelegant. Es gibt normalerweise bessere Möglichkeiten, um unklare Methodennamen zu verbessern.
Er hat Recht, dass
assertEquals
(aus Unit-Test-Bibliotheken im xUnit-Stil) nicht klar ist, welches Argument das erwartete und welches das tatsächliche ist. Das haben mich auch gebissen! Viele Komponententestbibliotheken haben das Problem festgestellt und alternative Syntaxen eingeführt, z.Oder ähnliches. Das ist sicherlich viel klarer als
assertEquals
aber auch viel besser alsassertExpectedEqualsActual
. Und es ist auch viel komponierbarer.quelle
fun(x)
5 ist, was dann schief gehen könnte, wenn die Reihenfolge umgekehrt wird -assert(fun(x), 5)
? Wie hat es dich gebissen?expected
und erstellt. Wenn Sieactual
diese also umkehren, wird die Meldung möglicherweise nicht korrekt. Aber ich bin damit einverstanden, dass es sich natürlicher anhört :)assert(expected, observed)
oderassert(observed, expected)
. Ein besseres Beispiel wäre so etwas wielocateLatitudeLongitude
- wenn Sie die Koordinaten umkehren, wird es ernsthaft durcheinander bringen.Sie versuchen, Ihren Weg zwischen Scylla und Charybdis zur Klarheit zu lenken, um unnötige Ausführlichkeit (auch als zielloses Wandern bezeichnet) sowie übermäßige Kürze (auch als kryptische Knappheit bezeichnet) zu vermeiden.
Wir müssen uns also die Schnittstelle ansehen, die Sie auswerten möchten, um Debug-Aussagen zu machen, dass zwei Objekte gleich sind.
Nein, der Name selbst ist also klar genug.
Nein, also ignorieren wir sie. Hast du das schon gemacht? Gut.
Fast im Fehlerfall setzt die Nachricht jede Argumentdarstellung an ihre eigene Stelle.
Lassen Sie uns also sehen, ob dieser kleine Unterschied von Bedeutung ist und nicht durch bestehende strenge Konventionen abgedeckt wird.
Ist das beabsichtigte Publikum belästigt, wenn die Argumente unbeabsichtigt ausgetauscht werden?
Nein, die Entwickler bekommen auch einen Stack-Trace und müssen den Quellcode trotzdem überprüfen, um den Fehler zu beheben.
Auch ohne einen vollständigen Stack-Trace löst die Assertions-Position diese Frage. Und wenn selbst das fehlt und aus der Botschaft nicht ersichtlich ist, welches ist, verdoppelt es höchstens die Möglichkeiten.
Entspricht die Reihenfolge der Argumente der Konvention?
Scheint der Fall zu sein. Es scheint jedoch allenfalls eine schwache Konvention zu sein.
Daher sieht der Unterschied ziemlich unbedeutend aus, und die Argumentreihenfolge unterliegt einer Konvention, die stark genug ist, dass jeder Versuch, sie in den Funktionsnamen zu codieren, einen negativen Nutzen hat.
quelle
expected
und erstelltactual
(zumindest bei Strings)assertEquals("foo", "doo")
gibt die FehlermeldungComparisonFailure: expected:<[f]oo> but was:<[d]oo>
... die Werte Swapping die Bedeutung der Nachricht umkehren würde, das klingt mehr anti mir symmetrisch. Wie auch immer, wie Sie sagten, ein Entwickler hat andere Indikatoren, um den Fehler zu beheben, aber es kann meiner Meinung nach irreführend sein und ein bisschen mehr Debugging-Zeit in Anspruch nehmen.Oft fügt es keine logische Klarheit hinzu.
Vergleichen Sie "Add" mit "AddFirstArgumentToSecondArgument".
Wenn Sie eine Überladung benötigen, die beispielsweise drei Werte hinzufügt. Was wäre sinnvoller?
Ein weiteres "Hinzufügen" mit drei Argumenten?
oder
"AddFirstAndSecondAndThirdArgument"?
Der Name der Methode sollte ihre logische Bedeutung vermitteln. Es sollte zeigen, was es tut. Auf Mikroebene zu sagen, welche Schritte erforderlich sind, macht es dem Leser nicht leichter. Die Namen der Argumente liefern bei Bedarf zusätzliche Details. Wenn Sie noch mehr Details benötigen, ist der Code genau richtig für Sie.
quelle
Add
schlägt eine kommutative Operation vor. Das OP befasst sich mit Situationen, in denen es auf die Reihenfolge ankommt.sum
ist ein perfektes cromulent Verb . Es ist besonders häufig in der Phrase "zusammenzufassen".Ich möchte noch etwas hinzufügen, das durch andere Antworten angedeutet ist, aber ich glaube, es wurde nicht explizit erwähnt:
@puck sagt "Es gibt immer noch keine Garantie, dass das erste Argument im Funktionsnamen wirklich der erste Parameter ist."
@cbojar sagt "Verwenden Sie Typen anstelle von mehrdeutigen Argumenten"
Das Problem ist, dass Programmiersprachen keine Namen verstehen: Sie werden nur als undurchsichtige, atomare Symbole behandelt. Wie bei Codekommentaren besteht daher nicht notwendigerweise eine Korrelation zwischen dem Namen einer Funktion und ihrer tatsächlichen Funktionsweise.
Vergleichen Sie
assertExpectedEqualsActual(foo, bar)
mit einigen Alternativen (von dieser Seite und anderswo), wie:Diese haben alle mehr Struktur als der wörtliche Name, wodurch die Sprache nicht undurchsichtig wirkt. Die Definition und Verwendung der Funktion hängt auch von dieser Struktur ab, so dass sie nicht mit dem synchronisiert werden kann, was die Implementierung tut (wie ein Name oder ein Kommentar).
Wenn ich auf ein Problem wie dieses stoße oder es sehe, nehme ich mir einen Moment Zeit, um zu fragen, ob es „fair“ ist, der Maschine überhaupt die Schuld zu geben, bevor ich frustriert meinen Computer anschreie. Mit anderen Worten, hat die Maschine genügend Informationen erhalten, um zu unterscheiden, was ich wollte und wonach ich gefragt habe?
Ein Anruf wie
assertEqual(expected, actual)
macht genauso viel Sinn wieassertEqual(actual, expected)
, daher ist es für uns einfach, sie durcheinander zu bringen und die Maschine vorwärts zu pflügen und das Falsche zu tun. Wenn wirassertExpectedEqualsActual
stattdessen verwenden, ist es möglicherweise weniger wahrscheinlich, dass wir einen Fehler machen, aber die Maschine erhält keine weiteren Informationen (sie kann kein Englisch verstehen und die Wahl des Namens sollte sich nicht auf die Semantik auswirken).Was die "strukturierten" Ansätze bevorzugter macht, wie Schlüsselwortargumente, beschriftete Felder, unterschiedliche Typen usw., ist, dass die zusätzlichen Informationen auch maschinenlesbar sind , so dass wir die Maschine falsche Verwendungen erkennen und uns dabei helfen können, die Dinge richtig zu machen. Der
assertEqual
Fall ist nicht so schlimm, da das einzige Problem ungenaue Nachrichten wären. Ein unheimlicheres Beispiel könnte seinString replace(String old, String new, String content)
, das leicht zu verwechselnString replace(String content, String old, String new)
ist und eine ganz andere Bedeutung hat. Ein einfaches Mittel wäre, ein Paar zu nehmen[old, new]
, bei dem Fehler sofort einen Fehler auslösen (auch ohne Typen).Beachten Sie, dass wir selbst bei Typen möglicherweise nicht sagen, was wir wollen. Zum Beispiel behandelt das Anti-Pattern "stringly typed programming" alle Daten als Strings, was es leicht macht, Argumente zu verwechseln (wie in diesem Fall), einen Schritt zu vergessen (zB Escape), um Invarianten versehentlich zu brechen (zB unparseable JSON machen), etc.
Dies hängt auch mit der "Booleschen Blindheit" zusammen, bei der wir eine Reihe von Booleschen Werten (oder Zahlen usw.) in einem Teil des Codes berechnen, aber wenn wir versuchen, sie in einem anderen Teil zu verwenden, ist nicht klar, was sie tatsächlich darstellen, ob Wir haben sie durcheinander gebracht, usw. Vergleichen Sie dies zB mit verschiedenen Aufzählungen, die beschreibende Namen haben (zB
LOGGING_DISABLED
anstattfalse
) und die eine Fehlermeldung verursachen, wenn wir sie durcheinander bringen.quelle
Wirklich? Es gibt noch keine Garantie, dass das zuerst genannte Argument im Funktionsnamen wirklich der erste Parameter ist. Schlagen Sie es also besser nach (oder lassen Sie Ihre IDE das tun) und bleiben Sie bei vernünftigen Namen, als sich blind auf einen ziemlich dummen Namen zu verlassen.
Wenn Sie den Code lesen, sollten Sie leicht sehen, was passiert, wenn die Parameter so benannt werden, wie sie sein sollten.
copy(source, destination)
ist viel leichter zu verstehen als soemthing wiecopyFromTheFirstLocationToTheSecondLocation(placeA, placeB)
.Weil es unterschiedliche Sichtweisen auf verschiedene Stile gibt und Sie x Autoren anderer Artikel finden können, in denen das Gegenteil festgestellt wird. Du würdest verrückt werden, wenn du versuchst, allem zu folgen, was jemand irgendwo schreibt ;-)
quelle
Ich stimme zu, dass das Codieren von Parameternamen in Funktionsnamen das Schreiben und Verwenden von Funktionen intuitiver macht.
Die Reihenfolge der Argumente in Funktionen und Shell-Befehlen kann leicht vergessen werden, und viele Programmierer verlassen sich aus diesem Grund auf IDE-Features oder Funktionsreferenzen. Die im Namen beschriebenen Argumente wären eine beredte Lösung für diese Abhängigkeit.
Einmal geschrieben, wird die Beschreibung der Argumente jedoch für den nächsten Programmierer, der die Anweisung lesen muss, überflüssig, da in den meisten Fällen benannte Variablen verwendet werden.
Die Knappheit wird die meisten Programmierer überzeugen und ich persönlich finde es einfacher zu lesen.
BEARBEITEN: Wie @Blrfl bereits betont hat, ist die Codierung von Parametern nicht so intuitiv, da Sie sich zunächst den Namen der Funktion merken müssen. Dies erfordert das Nachschlagen von Funktionsreferenzen oder das Anfordern von Hilfe von einer IDE, die wahrscheinlich ohnehin Informationen zur Parameterreihenfolge liefert.
quelle
copyFromSourceToDestination
oder nichtcopyToDestinationFromSource
, können Sie diese durch Ausprobieren oder Lesen des Referenzmaterials finden. IDEs, die Teilnamen vervollständigen können, sind nur eine automatisierte Version der letzteren.copyFromSourceToDestination
ist, dasscopyToDestinationFromSource
der Compiler Ihren Fehler findet, wenn Sie glauben, dass er es ist , aber wenn er aufgerufen wurdecopy
, wird er es nicht tun . Es ist einfach, die Parameter einer Kopierroutine in die falsche Richtung zu lenken, da strcpy, strcat usw. einen Präzedenzfall setzen. Und ist die knappe leichter zu lesen? Erstellt mergeLists (listA, listB, listC) listA aus listB & listC oder liest listA & listB und schreibt listC?dir1.copy(dir2)
Arbeit? Keine Ahnung. Was istdir1.copyTo(dir2)
?