Der Code ist lebendig und tot. Teil drei. Code als Text

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


  1. Die Objekte
  2. Aktionen und Eigenschaften
  3. 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(); // ,  .  . var b = x.OrDefault(); //  —  ,   . var c = x.Or(10); //     . 

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; // . var x = numbers ?? Enumerable.Empty<int>(); //   . var x = numbers.OrEmpty(); 

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 = ...; //   . creature.Health = Math.Max(creature.Health - damage, 0); // Fluent. creature.Health = (creature.Health - damage).Or(0).IfGreater(); //   : creature.Health = (creature.Health - damage).ButNotLess(than: 0); 

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 }; // ""    —  . var x = string.Join(", ", numbers); //    — ! var x = numbers.Separated(with: ", "); 

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 = ...; // ,  . var x = Regex.IsMatch(id, "^[0-9]*$"); // . var x = id.All(x => x.IsDigit()); // ! var x = id.IsNumer(); 

Ein anderer Fall besteht darin, herauszufinden, ob die Zeichenfolge mindestens ein Zeichen aus der Sequenz enthält:


 string text = ...; //   . var x = Regex.IsMatch(text, @"["<>[]'"); //   . ( .) var x = text.ContainsAnyOf('"', '<', '>', '[', ']', '\''); //  . var x = text.ContainsAny(charOf: @"["<>[]'"); 

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 = ...; //   -   . var x = Regex.Replace(text, "([az](?=[AZ])|[AZ](?=[AZ][az]))", "$1 "); //  . var x = text.PascalCaseWords().Separated(with: " "); //   . var x = text.AsWords(eachStartsWith: x => x.IsUpper()).Separated(with: " "); 

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 = ...; //    . var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path; //   . var x = path.Without(".exe").AtEnd; 

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 = ...; //   `Get` ,   `|`.     . var x = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // ,  . `Or`   , . var x = type.Methods(_ => _.Instance.Public.Or.NonPublic); 

(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 = ...; //   . var x = orders.GetOrderById(id); //      : var x = orders.Order(id); //     ,    . var x = orders.One(id); //    : var x = orders.One(with: 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 = ...; //   . var x = orders.GetOrders(); //  `Get`,  . var x = orders.Orders(); // ! var x = orders.All(); 

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 = ...; //  ,   : var x = items.GetValueOrDefault(id); var x = items[id]; var x = items.GetOrAdd(id, () => new Item()); //    : var x = items.One(with: id).OrDefault(); var x = items.One(with: id).Or(Item.Empty); var x = items.One(with: id).OrThrow(withMessage: $"Couldn't find item with '{id}' id."); var x = items.One(with: id).OrNew(() => new Item()); 

Assembly.GetTypes


Schauen wir uns an, wie alle vorhandenen Instanzen vom Typ T in der Assembly erstellt werden:


 // . var x = Assembly .GetAssembly(typeof(T)) .GetTypes() .Where(...) .Select(Activator.CreateInstance); // "" . var x = TypesHelper.GetAllInstancesOf<T>(); // . var x = Instances.Of<T>(); 

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:


 //  . Argument.ThrowIfNull(x); Guard.CheckAgainstNull(x); // . x.Should().BeNotNull(); // ,  ...  ? Ensure(that: x).NotNull(); 

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 = ...; // . var x = items.All(where: x => x.IsWeapon); //  . // `ItemsThatAre.Weapons`  `Predicate<bool>`. var x = items.All(ItemsThatAre.Weapons); // `using static`  !  . var x = items.All(Weapons); 

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)

Source: https://habr.com/ru/post/de447830/


All Articles