Sie müssen den Code lesen, um das Programm zu begleiten. Je einfacher es ist, desto mehr sieht es aus wie eine natürliche Sprache - dann werden Sie schneller darauf zugreifen und sich auf die Hauptsache konzentrieren.
In den letzten beiden Artikeln habe ich gezeigt, dass sorgfältig ausgewählte Wörter helfen, das Wesentliche des Geschriebenen besser zu verstehen, aber nur darüber nachzudenken, reicht nicht aus, da jedes Wort in zwei Formen existiert: als für sich und als Teil eines Satzes. Das Wiederholen von CurrentThread
wird erst wiederholt, wenn wir es im Kontext von Thread.CurrentThread
gelesen Thread.CurrentThread
.
Anhand von Noten und einfachen Melodien werden wir nun sehen, was Musik ist.
Inhaltsverzeichnis durchlaufen
- Die Objekte
- Aktionen und Eigenschaften
- Code als Text
Code als Text
Die meisten fließenden Schnittstellen sind eher auf externe als auf interne Schnittstellen ausgelegt, sodass sie so einfach zu lesen sind. Natürlich nicht umsonst: Der Inhalt wird gewissermaßen schwächer. FluentAssertions
wir also an, FluentAssertions
können im FluentAssertions
Paket FluentAssertions
schreiben: (2 + 2).Should().Be(4, because: "2 + 2 is 4!")
Und im Verhältnis zum Lesen, because
elegant aussieht, aber innerhalb des Be()
vielmehr der Parameter error
oder errorMessage
.
Meiner Meinung nach sind solche Ausnahmen nicht von Bedeutung. Wenn wir uns einig sind, dass der Code ein Text ist, hören seine Komponenten auf, zu sich selbst zu gehören: Sie sind jetzt Teil einer Art universellem "Äther" .
Ich werde anhand von Beispielen zeigen, wie solche Überlegungen zu Erfahrungen werden.
Interlocked
Ich möchte Sie an den Fall Interlocked
erinnern, den wir von Interlocked.CompareExchange(ref x, newX, oldX)
in Atomically.Change(ref x, from: oldX, to: newX)
mit eindeutigen Namen von Methoden und Parametern Atomically.Change(ref x, from: oldX, to: newX)
.
ExceptWith
Typ ISet<>
hat eine Methode namens ExceptWith
. Wenn Sie sich einen Anruf wie items.ExceptWith(other)
, werden Sie nicht sofort erkennen, was passiert. Aber Sie müssen nur schreiben: items.Exclude(other)
, da alles items.Exclude(other)
.
GetValueOrDefault
Wenn Sie mit Nullable<T>
x.Value
Aufruf von x.Value
eine Ausnahme aus, wenn x
null
. Wenn Sie weiterhin Value
x.GetValueOrDefault
müssen, verwenden Sie x.GetValueOrDefault
: x.GetValueOrDefault
ist entweder Value
oder der Standardwert. Sperrig.
Der Ausdruck "oder x oder der Standardwert" entspricht dem kurzen und eleganten x.OrDefault
.
int? x = null; var a = x.GetValueOrDefault();
Bei OrDefault
und Or
ist eines zu beachten: bei der Arbeit mit einem Bediener .?
Sie können nicht so etwas wie x?.IsEnabled.Or(false)
schreiben, x?.IsEnabled.Or(false)
nur (x?.IsEnabled).Or(false)
(mit anderen Worten, der Operator .?
bricht die gesamte rechte Seite ab, wenn null
links ist).
Die Vorlage kann angewendet werden, wenn mit IEnumerable<T>
:
IEnumerable<int> numbers = null;
Math.Min
und Math.Max
Eine Idee mit Or
kann zu numerischen Typen entwickelt werden. Angenommen, Sie möchten die maximale Anzahl von a
und b
. Dann schreiben wir: Math.Max(a, b)
oder a > b ? a : b
a > b ? a : b
. Beide Optionen kommen mir recht bekannt vor, sehen aber dennoch nicht wie eine natürliche Sprache aus.
Sie können es ersetzen durch: a.Or(b).IfLess()
- nehmen Sie a
oder b
wenn a
kleiner ist . Geeignet für solche Situationen:
Creature creature = ...; int damage = ...;
string.Join
Manchmal müssen Sie eine Sequenz zu einer Zeichenfolge zusammenfügen und die Elemente durch ein Leerzeichen oder Komma trennen. Verwenden string.Join
beispielsweise string.Join
wie string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
.
Ein einfaches "Komma-Nummer teilen" kann plötzlich zu "Komma an jede Zahl aus der Liste anhängen" werden - dies ist sicherlich kein Code als Text.
var numbers = new [] { 1, 2, 3 };
Regex
string.Join
ist jedoch ziemlich harmlos im Vergleich dazu, wie Regex
manchmal falsch und für andere Zwecke verwendet wird. Wo Sie mit einfachem, lesbarem Text auskommen können, wird aus irgendeinem Grund ein überkomplizierter Eintrag bevorzugt.
Beginnen wir mit einer einfachen Bestimmung, dass eine Zeichenfolge eine Reihe von Zahlen darstellt:
string id = ...;
Ein anderer Fall besteht darin, herauszufinden, ob die Zeichenfolge mindestens ein Zeichen aus der Sequenz enthält:
string text = ...;
Je komplizierter die Aufgabe, desto schwieriger ist das Lösungsmuster: Um einen Datensatz vom "HelloWorld"
in ein paar Wörter "Hello World"
aufzuteilen, wollte jemand anstelle eines einfachen Algorithmus ein Monster:
string text = ...;
Zweifellos sind reguläre Ausdrücke effektiv und universell, aber ich möchte verstehen, was auf den ersten Blick geschieht.
Substring
und Remove
Es kommt vor, dass Sie einen Teil von Anfang oder Ende aus der Zeile entfernen müssen, z. B. aus dem path
- der Erweiterung .txt
, falls vorhanden.
string path = ...;
Wieder waren die Aktion und der Algorithmus weg und eine einfache Zeile wurde am Ende ohne die Erweiterung .exe belassen .
Da die Without
Methode einen bestimmten WithoutExpression
, bitten sie um einen anderen: path.Without("_").AtStart
und path.Without("Something").Anywhere
. Es ist auch interessant, dass ein anderer Ausdruck mit demselben Wort erstellt werden kann: name.Without(charAt: 1)
- löscht das Zeichen bei Index 1 und gibt eine neue Zeile zurück (nützlich bei der Berechnung von Permutationen). Und auch lesbar!
Type.GetMethods
Verwenden Sie:
Type type = ...;
(Gleiches gilt für GetFields
und GetProperties
.)
Directory.Copy
Alle Vorgänge mit Ordnern und Dateien werden häufig auf DirectoryUtils
, FileSystemHelper
verallgemeinert. Sie implementieren die Umgehung, Bereinigung, das Kopieren usw. des Dateisystems. Aber hier können Sie sich etwas Besseres einfallen lassen!
Wir zeigen den Text "Alle Dateien von 'D: \ Source' nach 'D: \ Target' kopieren" in den Code "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target")
AsDirectory()
- gibt DirectoryInfo
aus der string
und Copy()
- erstellt eine Instanz von CopyExpression
, die eine eindeutige API zum CopyExpression
von CopyExpression
beschreibt (Sie können beispielsweise Copy().Files.Files
nicht aufrufen). Dann öffnet sich die Möglichkeit, nicht alle Dateien zu kopieren, sondern einige: Copy().Files.Where(x => x.IsNotEmpty)
.
GetOrderById
Im zweiten Artikel habe ich geschrieben, dass IUsersRepository.GetUser(int id)
redundant ist und besser IUsersRepository.User(int id)
. Dementsprechend haben wir in einem ähnlichen IOrdersRepository
nicht GetOrderById(int id)
, sondern Order(int id)
. In einem anderen Beispiel wurde jedoch vorgeschlagen, die Variable eines solchen Repositorys nicht _ordersRepository
, sondern einfach _orders
.
Beide Änderungen sind für sich genommen gut, summieren sich jedoch im _orders.Order(id)
nicht: Der Aufruf von _orders.Order(id)
sieht ausführlich aus. Es wäre möglich, _orders.Get(id)
, aber die Bestellungen _orders.Get(id)
fehl. Wir möchten nur diejenige angeben, die eine solche Kennung hat . Einer, der One
, also:
IOrdersRepository orders = ...; int id = ...;
GetOrders
In Objekten wie IOrdersRepository
werden häufig andere Methoden gefunden: AddOrder
, RemoveOrder
, GetOrders
. Die ersten beiden Wiederholungen _orders.Add(order)
und Add
and Remove
werden erhalten (mit den entsprechenden Einträgen _orders.Add(order)
und _orders.Remove(order)
). Mit GetOrders
schwieriger, Orders
wenig umzubenennen. Mal sehen:
IOrdersRepository orders = ...;
Es sollte beachtet werden, dass mit dem alten _ordersRepository
Wiederholungen in GetOrders
oder GetOrderById
Aufrufen nicht so auffällig sind, da wir mit dem Repository arbeiten!
Namen wie One
, All
eignen sich für viele Schnittstellen, die viele darstellen. Angenommen, in der bekannten Implementierung der GitHub-API - octokit
- sieht das gitHub.Repository.GetAllForUser("John")
aller Benutzerrepositorys wie gitHub.Repository.GetAllForUser("John")
, obwohl dies logischer ist - gitHub.Users.One("John").Repositories.All
. In diesem Fall ist das gitHub.Repository.Get("John", "Repo")
eines Repositorys gitHub.Repository.Get("John", "Repo")
anstelle des offensichtlichen gitHub.Users.One("John").Repositories.One("Repo")
. Der zweite Fall sieht länger aus, ist jedoch intern konsistent und spiegelt die Plattform wider. Darüber hinaus kann es mithilfe von Erweiterungsmethoden zu gitHub.User("John").Repository("Repo")
gekürzt werden.
Dictionary.TryGetValue
Das Abrufen von Werten aus dem Wörterbuch ist in mehrere Szenarien unterteilt, die sich nur darin unterscheiden, was zu tun ist, wenn der Schlüssel nicht gefunden wird:
- einen Fehler auslösen (
dictionary[key]
); - den Standardwert zurückgeben (nicht implementiert, aber häufig
GetValueOrDefault
oder TryGetValue
schreiben); - etwas anderes zurückgeben (nicht implementiert, aber ich würde
GetValueOrOther
erwarten); - Schreiben Sie den angegebenen Wert in das Wörterbuch und geben Sie ihn zurück (nicht implementiert, aber
GetOrAdd
wird gefunden).
Die Ausdrücke konvergieren an dem Punkt " Nimm ein X oder Y, wenn X nicht ist ." Wie im Fall von _ordersRepository
rufen wir außerdem die Wörterbuchvariable nicht itemsDictionary
, sondern items
.
Dann ist für den Teil "Nimm etwas X" ein Aufruf der Formularelemente. items.One(withKey: X)
ideal und gibt eine Struktur mit vier Endungen zurück :
Dictionary<int, Item> items = ...; int id = ...;
Assembly.GetTypes
Schauen wir uns an, wie alle vorhandenen Instanzen vom Typ T
in der Assembly erstellt werden:
Daher ist der Name einer statischen Klasse manchmal der Anfang eines Ausdrucks.
Ähnliches findet sich in NUnit: Assert.That(2 + 2, Is.EqualTo(4))
- Is
und wurde nicht als autarker Typ konzipiert.
Argument.ThrowIfNull
Schauen wir uns nun die Voraussetzungsprüfung an:
Ensure.NotNull(argument)
- nett, aber nicht ganz englisch. Eine andere Sache ist das oben geschriebene Ensure(that: x).NotNull()
. Wenn es nur könnte ...
Übrigens können Sie! Wir schreiben Contract.Ensure(that: argument).IsNotNull()
und importieren den Contract
using static
. So werden alle Arten von Ensure(that: type).Implements<T>()
, Ensure(that: number).InRange(from: 5, to: 10)
usw. Ensure(that: number).InRange(from: 5, to: 10)
.
Die Idee des statischen Imports öffnet viele Türen. Ein schönes Beispiel für: anstelle von items.Remove(x)
Remove(x, from: items)
items.Remove(x)
Schreiben Remove(x, from: items)
. Aber merkwürdig ist die Reduzierung von enum
und Eigenschaften, die Funktionen zurückgeben.
IItems items = ...;
Exotischer Find
In C # 7.1 und höher können Sie nicht Find(1, @in: items)
schreiben, sondern Find(1, in items)
, wobei Find
als Find<T>(T item, in IEnumerable<T> items)
. Dieses Beispiel ist unpraktisch, zeigt aber, dass alle Mittel im Kampf um Lesbarkeit gut sind.
Insgesamt
In diesem Teil habe ich verschiedene Möglichkeiten untersucht, um mit der Lesbarkeit von Code zu arbeiten. Alle von ihnen können verallgemeinert werden auf:
- Der benannte Parameter als Teil des Ausdrucks lautet
Should().Be(4, because: "")
, Atomically.Change(ref x, from: oldX, to: newX)
. - Ein einfacher Name anstelle von technischen Details lautet
Separated(with: ", ")
, Exclude
. - Die Methode als Teil der Variablen lautet
x.OrDefault()
, x.Or(b).IfLess()
, orders.One(with: id)
, orders.All
. - Die Methode als Teil des Ausdrucks lautet
path.Without(".exe").AtEnd
. - Der Typ als Teil des Ausdrucks ist
Instances.Of
, Is.EqualTo
. - Die Methode als Teil des Ausdrucks (unter
using static
) lautet Ensure(that: x)
, items.All(Weapons)
.
So werden das Äußere und das Kontemplierte in den Vordergrund gerückt. Zuerst wird es gedacht, und dann werden seine spezifischen Inkarnationen gedacht, nicht so bedeutend, solange der Code als Text gelesen wird. Daraus folgt, dass der Richter weniger der Geschmack als die Sprache ist - er bestimmt den Unterschied zwischen item.GetValueOrDefault
und item.OrDefault
.
Nachwort
Was ist besser, klar, aber nicht funktionierend oder funktionierend, aber unverständlich? Ein schneeweißes Schloss ohne Möbel und Zimmer oder ein Schuppen mit Sofas im Stil Ludwigs XIV.? Eine Luxusyacht ohne Motor oder Stöhnen mit einem Quantencomputer, den niemand benutzen kann?
Polare Antworten passen nicht, sondern "irgendwo in der Mitte" .
Meiner Meinung nach sind beide Konzepte untrennbar miteinander verbunden: Wenn wir sorgfältig ein Cover für ein Buch auswählen, betrachten wir zweifelsohne Fehler im Text und umgekehrt. Ich möchte nicht, dass die Beatles Musik von geringer Qualität spielen, aber sie sollten auch MusicHelper heißen.
Eine andere Sache ist, dass die Arbeit an einem Wort als Teil des Entwicklungsprozesses eine unterschätzte, ungewöhnliche Sache ist und daher immer noch ein extremes Urteilsvermögen erforderlich ist. Dieser Zyklus ist das Extrem von Form und Bild.
Vielen Dank für Ihre Aufmerksamkeit!
Referenzen
Pocket.Common
, die weitere Beispiele sehen Pocket.Common
, finden Sie auf meinem GitHub, beispielsweise in der Pocket.Common
Bibliothek. (nicht für den weltweiten und universellen Gebrauch)