Vous devez lire le code pour accompagner le programme, et plus il est facile de le faire, plus il ressemble à un langage naturel - alors vous y entrerez plus rapidement et vous vous concentrerez sur l'essentiel.
Dans les deux derniers articles, j'ai montré que des mots soigneusement sélectionnés aident à mieux comprendre l'essence de ce qui est écrit, mais y penser ne suffit pas, car chaque mot existe sous deux formes: comme en soi et comme partie d'une phrase. Répéter CurrentThread
ne se répète pas encore tant que nous ne l'avons pas lu dans le contexte de Thread.CurrentThread
.
Ainsi, guidés par des notes et des mélodies simples, nous allons maintenant voir ce qu'est la musique.
Table des matières du cycle
- Les objets
- Actions et propriétés
- Code comme texte
Code comme texte
La plupart des interfaces fluides sont conçues en mettant l'accent sur l'extérieur plutôt que sur l'intérieur, de sorte qu'elles sont si faciles à lire. Bien sûr, pas gratuitement: le contenu s'affaiblit dans un sens. Donc, disons, dans le package FluentAssertions
, FluentAssertions
pouvez écrire: (2 + 2).Should().Be(4, because: "2 + 2 is 4!")
, Et, par rapport à la lecture, because
air élégant, mais à l'intérieur du Be()
, le paramètre error
ou errorMessage
est plutôt errorMessage
.
À mon avis, ces exemptions ne sont pas importantes. Lorsque nous convenons que le code est un texte, ses composants cessent de s’appartenir: ils font maintenant partie d’une sorte d’ Ether universel.
Je vais montrer par des exemples comment de telles considérations deviennent une expérience.
Interlocked
Permettez-moi de vous rappeler le cas Interlocked
, que nous sommes Interlocked.CompareExchange(ref x, newX, oldX)
d' Interlocked.CompareExchange(ref x, newX, oldX)
à Atomically.Change(ref x, from: oldX, to: newX)
, en utilisant des noms clairs de méthodes et de paramètres.
ExceptWith
Type ISet<>
a une méthode appelée ExceptWith
. Si vous regardez un appel comme des éléments, items.ExceptWith(other)
, vous ne réaliserez pas immédiatement ce qui se passe. Mais il suffit d'écrire: items.Exclude(other)
, car tout se met en place.
GetValueOrDefault
Lorsque vous travaillez avec Nullable<T>
appel de x.Value
une exception si x
est null
. Si vous devez toujours obtenir Value
, utilisez x.GetValueOrDefault
: c'est soit Value
soit la valeur par défaut. Encombrant.
L'expression «ou x, ou la valeur par défaut» correspond au x.OrDefault
court et élégant.
int? x = null; var a = x.GetValueOrDefault();
Avec OrDefault
et Or
il y a une chose à retenir: lorsque vous travaillez avec un opérateur .?
vous ne pouvez pas écrire quelque chose comme x?.IsEnabled.Or(false)
, seulement (x?.IsEnabled).Or(false)
(en d'autres termes, l'opérateur .?
annule tout le côté droit si null
à gauche).
Le modèle peut être appliqué lorsque vous travaillez avec IEnumerable<T>
:
IEnumerable<int> numbers = null;
Math.Min
et Math.Max
Une idée avec Or
peut être développée en types numériques. Supposons que vous vouliez prendre le nombre maximum de a
et b
. Puis on écrit: Math.Max(a, b)
ou a > b ? a : b
a > b ? a : b
. Les deux options semblent assez familières, mais ne ressemblent néanmoins pas à un langage naturel.
Vous pouvez le remplacer par: a.Or(b).IfLess()
- prenez a
ou b
si a
inférieur . Convient à de telles situations:
Creature creature = ...; int damage = ...;
string.Join
Parfois, vous devez assembler une séquence en chaîne, en séparant les éléments par un espace ou une virgule. Pour ce faire, utilisez string.Join
, par exemple, comme ceci: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
.
Un simple "Diviser le nombre de virgules" peut soudainement devenir "Attacher une virgule à chaque numéro de la liste" - ce n'est certainement pas un code sous forme de texte.
var numbers = new [] { 1, 2, 3 };
Regex
Cependant, string.Join
est assez inoffensif par rapport à la façon dont Regex
parfois utilisé de manière incorrecte et à d'autres fins. Où vous pouvez vous débrouiller avec un texte simple et lisible, pour une raison quelconque, une entrée trop compliquée est préférable.
Commençons par un simple - déterminer qu'une chaîne représente un ensemble de nombres:
string id = ...;
Un autre cas est de savoir s'il y a au moins un caractère dans la chaîne de la séquence:
string text = ...;
Plus la tâche est compliquée, plus la «configuration» de la solution est difficile: pour diviser un enregistrement de "HelloWorld"
en quelques mots "Hello World"
, quelqu'un au lieu d'un simple algorithme voulait un monstre:
string text = ...;
Sans aucun doute, les expressions régulières sont efficaces et universelles, mais je veux comprendre ce qui se passe à première vue.
Substring
- Substring
et Remove
Il arrive que vous deviez supprimer une partie de la ligne depuis le début ou la fin, par exemple, du path
- l'extension .txt
, le cas échéant.
string path = ...;
Encore une fois, l' action et l' algorithme ont disparu, et une ligne simple a été laissée sans l'extension .exe à la fin .
Étant donné que la méthode Without
doit renvoyer un certain WithoutExpression
, ils en demandent un autre: path.Without("_").AtStart
et path.Without("Something").Anywhere
. Il est également intéressant qu'une autre expression puisse être construite avec le même mot: name.Without(charAt: 1)
- supprime le caractère à l'index 1 et renvoie une nouvelle ligne (utile dans le calcul des permutations). Et aussi lisible!
Type.GetMethods
Pour obtenir des méthodes d'un certain type à l'aide de la réflexion, utilisez:
Type type = ...;
(Il en va de même pour GetFields
et GetProperties
.)
Directory.Copy
Toutes les opérations avec des dossiers et des fichiers sont souvent généralisées à DirectoryUtils
, FileSystemHelper
. Ils implémentent le contournement du système de fichiers, le nettoyage, la copie, etc. Mais ici, vous pouvez trouver quelque chose de mieux!
Affichez le texte "copiez tous les fichiers de 'D: \ Source' vers 'D: \ Target'" dans le code "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target")
AsDirectory()
- renvoie DirectoryInfo
partir d'une string
et Copy()
- crée une instance de CopyExpression
qui décrit une API unique pour la construction d'expressions (vous ne pouvez pas appeler Copy().Files.Files
, par exemple). L'opportunité s'ouvre alors de copier non pas tous les fichiers, mais certains: Copy().Files.Where(x => x.IsNotEmpty)
.
GetOrderById
Dans le deuxième article, j'ai écrit que IUsersRepository.GetUser(int id)
est redondant, et mieux, IUsersRepository.User(int id)
. Par conséquent, dans un IOrdersRepository
similaire IOrdersRepository
nous n'avons pas GetOrderById(int id)
, mais Order(int id)
. Cependant, dans un autre exemple, il a été suggéré que la variable d'un tel référentiel soit appelée non pas _ordersRepository
, mais simplement _orders
.
Les deux modifications sont bonnes en elles-mêmes, mais elles ne s'additionnent pas dans le contexte de lecture: l'appel de _orders.Order(id)
semble verbeux. Il serait possible de _orders.Get(id)
, mais les commandes échouent, nous voulons seulement spécifier celui qui a un tel identifiant . Celui qui est One
, donc:
IOrdersRepository orders = ...; int id = ...;
GetOrders
Dans les objets tels que IOrdersRepository
, d'autres méthodes sont souvent trouvées: AddOrder
, RemoveOrder
, GetOrders
. Les deux premières répétitions disparaissent, et Add
et Remove
sont obtenus (avec les entrées correspondantes _orders.Add(order)
et _orders.Remove(order)
). Avec GetOrders
plus difficile de renommer Orders
peu les commandes. Voyons voir:
IOrdersRepository orders = ...;
Il convient de noter qu'avec l'ancien _ordersRepository
répétitions dans GetOrders
ou GetOrderById
ne sont pas si visibles, car nous travaillons avec le référentiel!
Des noms comme One
, All
conviennent à de nombreuses interfaces qui en représentent plusieurs. Dites, dans l'implémentation bien connue de l'API GitHub - octokit
- obtenir tous les référentiels utilisateur ressemble à gitHub.Repository.GetAllForUser("John")
, bien qu'il soit plus logique - gitHub.Users.One("John").Repositories.All
. Dans ce cas, l'obtention d' un référentiel sera, respectivement, gitHub.Repository.Get("John", "Repo")
au lieu de l'évident gitHub.Users.One("John").Repositories.One("Repo")
. Le deuxième cas semble plus long, mais il est cohérent en interne et reflète la plate-forme. De plus, en utilisant des méthodes d'extension, il peut être raccourci en gitHub.User("John").Repository("Repo")
.
Dictionary.TryGetValue
L'obtention de valeurs à partir du dictionnaire est divisée en plusieurs scénarios qui ne diffèrent que par ce qui doit être fait si la clé n'est pas trouvée:
- lancer une erreur (
dictionary[key]
); - retourner la valeur par défaut (non implémentée, mais souvent écrire
GetValueOrDefault
ou TryGetValue
); - retourner quelque chose d'autre (non implémenté, mais je m'attendrais à
GetValueOrOther
); - écrire la valeur spécifiée dans le dictionnaire et la renvoyer (non implémentée, mais
GetOrAdd
est GetOrAdd
).
Les expressions convergent au point " prenez un peu de X, ou Y si X ne l'est pas ." De plus, comme dans le cas de _ordersRepository
, nous appellerons la variable de dictionnaire non itemsDictionary
, mais items
.
Ensuite, pour la partie "take some X" , un appel des éléments du formulaire. items.One(withKey: X)
idéal, renvoyant une structure avec quatre terminaisons :
Dictionary<int, Item> items = ...; int id = ...;
Assembly.GetTypes
Examinons la création de toutes les instances existantes de type T
dans l'assembly:
Ainsi, parfois, le nom d'une classe statique est le début d'une expression.
Quelque chose de similaire peut être trouvé dans NUnit: Assert.That(2 + 2, Is.EqualTo(4))
- Is
et n'a pas été conçu comme un type autosuffisant.
Argument.ThrowIfNull
Jetons maintenant un œil à la vérification des conditions préalables:
Ensure.NotNull(argument)
- sympa, mais pas tout à fait en anglais. Une autre chose est l' Ensure(that: x).NotNull()
écrit ci-dessus. Si seulement il pouvait ...
Au fait, vous le pouvez! Nous écrivons Contract.Ensure(that: argument).IsNotNull()
et importons le type de Contract
using static
. On Ensure(that: type).Implements<T>()
donc toutes sortes de Ensure(that: type).Implements<T>()
, Ensure(that: number).InRange(from: 5, to: 10)
, etc.
L'idée de l'importation statique ouvre de nombreuses portes. Un bel exemple pour: à la place des items.Remove(x)
écrire Remove(x, from: items)
. Mais curieux est la réduction des enum
et des propriétés qui renvoient des fonctions.
IItems items = ...;
Find
exotique
En C # 7.1 et supérieur, vous pouvez écrire non pas Find(1, @in: items)
, mais Find(1, in items)
, où Find
est défini comme Find<T>(T item, in IEnumerable<T> items)
. Cet exemple n'est pas pratique, mais montre que tous les moyens sont bons dans la lutte pour la lisibilité.
Total
Dans cette partie, j'ai examiné plusieurs façons de travailler avec la lisibilité du code. Tous peuvent être généralisés pour:
- Le paramètre nommé dans le cadre de l'expression est
Should().Be(4, because: "")
, Atomically.Change(ref x, from: oldX, to: newX)
. - Un nom simple au lieu de détails techniques est
Separated(with: ", ")
, Exclude
. - La méthode faisant partie de la variable est
x.OrDefault()
, x.Or(b).IfLess()
, orders.One(with: id)
, orders.All
. - La méthode faisant partie de l'expression est
path.Without(".exe").AtEnd
. - Le type faisant partie de l'expression est
Instances.Of
, Is.EqualTo
. - La méthode faisant partie de l'expression (en
using static
) est Ensure(that: x)
, items.All(Weapons)
.
Ainsi, l'externe et le contemplé sont mis en avant. Au début, il est pensé, puis ses incarnations spécifiques sont pensées, pas si significatives, tant que le code est lu comme du texte. Il s'ensuit que le juge n'est pas tant le goût que la langue - il détermine la différence entre item.GetValueOrDefault
et item.OrDefault
.
Épilogue
Quelle est la meilleure méthode, claire, mais qui ne fonctionne pas ou qui fonctionne, mais incompréhensible? Un château blanc comme neige sans meubles ni chambres ou un cabanon avec des canapés dans le style de Louis XIV? Un yacht de luxe sans moteur ni barge gémissante avec un ordinateur quantique que personne ne peut utiliser?
Les réponses polaires ne correspondent pas, mais "quelque part au milieu" aussi.
À mon avis, les deux concepts sont inextricables: en choisissant soigneusement la couverture d'un livre, nous examinons sans doute les erreurs dans le texte, et vice versa. Je ne voudrais pas que les Beatles jouent de la musique de mauvaise qualité, mais ils devraient aussi s’appeler MusicHelper .
Une autre chose est que travailler sur un mot dans le cadre du processus de développement est une chose inhabituelle sous-estimée, et donc une sorte de jugement extrême est encore nécessaire. Ce cycle est l'extrême de la forme et de l'image.
Merci à tous pour votre attention!
Les références
Toute personne intéressée à voir plus d'exemples peut être trouvée sur mon GitHub, par exemple, dans la bibliothèque Pocket.Common
. (pas pour une utilisation mondiale et universelle)