To accompany the program, you have to read the code, and in order to do it easier, the more it looks like a natural language, then you get faster and focus on the main thing.
In the last two articles, I showed that carefully selected words help to better understand the essence of what was written, but thinking about them is not enough, because every word exists in two forms: as in itself and as part of a sentence. Repeat CurrentThread
is not yet repeat until we read it in the context of Thread.CurrentThread
.
Thus, focusing on the notes and simple melodies, we now see what music is.
Most fluent interfaces are designed with a focus on the external rather than the internal, which is why they are so easy to read. Of course, not free: the content in some sense weakens. So, let's say in the FluentAssertions
package FluentAssertions
can write: (2 + 2).Should().Be(4, because: "2 + 2 is 4!")
, And, regarding reading, because
looks elegant, but inside the Be()
method Be()
it is expected, rather, the parameter error
or errorMessage
.
In my opinion, such indulgences are insignificant. When we agree that a code is a text, its components cease to belong to themselves: they are now part of some kind of universal "Ether" .
I will show by examples how such considerations become experience.
Interlocked
I recall the case of Interlocked
, which we from Interlocked.CompareExchange(ref x, newX, oldX)
turned into Atomically.Change(ref x, from: oldX, to: newX)
using clear names of methods and parameters.
ExceptWith
The type ISet<>
has a method called ExceptWith
. If you look at a call like items.ExceptWith(other)
, you will not immediately figure out what is happening. But you just have to write: items.Exclude(other)
, how things items.Exclude(other)
into place.
GetValueOrDefault
When working with Nullable<T>
call to x.Value
will throw an exception if x
is null
. If you still need to get the Value
, you use x.GetValueOrDefault
: this is either Value
or the default value. Cumbersome.
The expression "or x, or default" is appropriate for a short and elegant x.OrDefault
.
int? x = null; var a = x.GetValueOrDefault(); // , . . var b = x.OrDefault(); // — , . var c = x.Or(10); // .
With OrDefault
and Or
there is one thing that is worth remembering: when working with an operator .?
you cannot write something like x?.IsEnabled.Or(false)
, only (x?.IsEnabled).Or(false)
(in other words, the .?
operator cancels the entire right-hand side if left null
).
The template can be applied when working with IEnumerable<T>
:
IEnumerable<int> numbers = null; // . var x = numbers ?? Enumerable.Empty<int>(); // . var x = numbers.OrEmpty();
Math.Min
and Math.Max
An idea with Or
can be developed into numeric types. Suppose you want to take the maximum number of a
and b
. Then we write: Math.Max(a, b)
or a > b ? a : b
a > b ? a : b
. Both options look quite familiar, but, nevertheless, do not look like natural language.
You can replace it with: a.Or(b).IfLess()
- take a
or b
, if a
less . Suitable for such situations:
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
Sometimes you need to assemble a sequence into a string, separating the elements with a space or comma. To do this, use string.Join
, for example, like this: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
.
A simple “Separate numbers with a comma” can suddenly become “Attach a comma to each number from the list” - this is certainly not code as text.
var numbers = new [] { 1, 2, 3 }; // "" — . var x = string.Join(", ", numbers); // — ! var x = numbers.Separated(with: ", ");
Regex
However, string.Join
is quite harmless compared to how it is sometimes used incorrectly and not for its intended purpose using Regex
. Where it is possible to do with simple readable text, for some reason, the overwritten recording is preferred.
Let's start with a simple one - determining that a string represents a set of numbers:
string id = ...; // , . var x = Regex.IsMatch(id, "^[0-9]*$"); // . var x = id.All(x => x.IsDigit()); // ! var x = id.IsNumer();
Another case is to find out if there is at least one character in the string from the sequence:
string text = ...; // . var x = Regex.IsMatch(text, @"["<>[]'"); // . ( .) var x = text.ContainsAnyOf('"', '<', '>', '[', ']', '\''); // . var x = text.ContainsAny(charOf: @"["<>[]'");
The more complex the task, the more difficult the "pattern" of the solution: to break the "HelloWorld"
type of record into several words "Hello World"
, someone instead of a simple algorithm wanted a 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: " ");
Undoubtedly, regular expressions are effective and universal, but I want to understand what is happening at first glance.
Substring
and Remove
It happens, you need to remove from the line any part from the beginning or end, for example, from path
- the extension .txt
, if it exists.
string path = ...; // . var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path; // . var x = path.Without(".exe").AtEnd;
Again, the action and the algorithm are gone, and a simple string is left without the .exe extension at the end .
Since the Without
method should return a certain WithoutExpression
, they still path.Without("_").AtStart
: path.Without("_").AtStart
and path.Without("Something").Anywhere
. It is also interesting that with the same word you can build another expression: name.Without(charAt: 1)
- deletes the character at index 1 and returns a new line (useful when calculating permutations). And also readable!
Type.GetMethods
To obtain methods of a certain type using reflection, use:
Type type = ...; // `Get` , `|`. . var x = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // , . `Or` , . var x = type.Methods(_ => _.Instance.Public.Or.NonPublic);
(The same applies to GetFields
and GetProperties
.)
Directory.Copy
Any operations with folders and files are often generalized to DirectoryUtils
, FileSystemHelper
. They implement file system bypassing, cleaning, copying, etc. But here you can come up with something better!
Display the text "copy all files from 'D: \ Source' into 'D: \ Target'" to the code "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target")
. AsDirectory()
returns DirectoryInfo
from string
, and Copy()
creates an instance of CopyExpression
that describes an unambiguous API for building expressions (you cannot call Copy().Files.Files
, for example). Then there are opportunities to copy not all files, but some: Copy().Files.Where(x => x.IsNotEmpty)
.
GetOrderById
In the second article I wrote that IUsersRepository.GetUser(int id)
is redundant, and better is IUsersRepository.User(int id)
. Accordingly, in the similar IOrdersRepository
we have not GetOrderById(int id)
, but Order(int id)
. However, in another example, it was suggested that the variable of such a repository be called not _ordersRepository
, but simply _orders
.
Both changes are good in themselves, but together, in the context of reading, they do not add up: the _orders.Order(id)
call looks verbose. It would be _orders.Get(id)
, but orders fail, we just want to specify the one that has such an identifier . "That One" is One
, therefore:
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 such objects as IOrdersRepository
, other methods are often found: AddOrder
, RemoveOrder
, GetOrders
. From the first two repetitions go, and Add
and Remove
(with the corresponding _orders.Add(order)
and _orders.Remove(order)
) are _orders.Remove(order)
. With GetOrders
harder to rename to Orders
little. Let's get a look:
IOrdersRepository orders = ...; // . var x = orders.GetOrders(); // `Get`, . var x = orders.Orders(); // ! var x = orders.All();
It should be noted that with the old _ordersRepository
repetition in calls to GetOrders
or GetOrderById
not so noticeable, because we are working with the repository!
Names like One
, All
are suitable for many interfaces representing sets. For example, in the well-known GitHub API implementation, octokit
, getting all user repositories looks like gitHub.Repository.GetAllForUser("John")
, although it is more logical to gitHub.Users.One("John").Repositories.All
. In this case, getting one repository will be, respectively, gitHub.Repository.Get("John", "Repo")
instead of the obvious gitHub.Users.One("John").Repositories.One("Repo")
. The second case looks longer, but it is internally consistent and reflects the platform. In addition, with the help of extension methods it can be reduced to gitHub.User("John").Repository("Repo")
.
Dictionary.TryGetValue
Getting values ​​from the dictionary is divided into several scenarios, which differ only in what needs to be done if the key is not found:
dictionary[key]
);GetValueOrDefault
or TryGetValue
);GetValueOrOther
);GetOrAdd
is GetOrAdd
).The expressions converge at the point " take some X, or Y, if X is not ." In addition, as in the case of _ordersRepository
, we will call the dictionary variable not itemsDictionary
, but items
.
Then for the “take some X” part , a call of the type items.One(withKey: X)
ideal, returning a structure with four endings :
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
Let's look at the creation of all instances of type T
in an assembly:
// . var x = Assembly .GetAssembly(typeof(T)) .GetTypes() .Where(...) .Select(Activator.CreateInstance); // "" . var x = TypesHelper.GetAllInstancesOf<T>(); // . var x = Instances.Of<T>();
Thus, sometimes, the name of a static class is the beginning of an expression.
Something similar can be found in NUnit: Assert.That(2 + 2, Is.EqualTo(4))
- Is
and was not thought of as a self-sufficient type.
Argument.ThrowIfNull
Now take a look at the precondition check:
// . Argument.ThrowIfNull(x); Guard.CheckAgainstNull(x); // . x.Should().BeNotNull(); // , ... ? Ensure(that: x).NotNull();
Ensure.NotNull(argument)
- pretty, but not quite in English. Another thing written above is Ensure(that: x).NotNull()
. If only there it was possible ...
By the way, you can! We write Contract.Ensure(that: argument).IsNotNull()
and import the Contract
type using using static
. This Ensure(that: type).Implements<T>()
are obtained Ensure(that: type).Implements<T>()
, Ensure(that: number).InRange(from: 5, to: 10)
, etc.
The idea of ​​static import opens many doors. A beautiful example for the sake of: instead of items.Remove(x)
write Remove(x, from: items)
. But more curious is the reduction of enum
and returning functions.
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);
Find
In C # 7.1 and above, you can write not Find(1, @in: items)
, but Find(1, in items)
, where Find
is defined as Find<T>(T item, in IEnumerable<T> items)
. This example is impractical, but it shows that all means are good in the struggle for readability.
In this section, I looked at several ways to work with code readability. All of them can be summarized to:
Should().Be(4, because: "")
, Atomically.Change(ref x, from: oldX, to: newX)
.Separated(with: ", ")
, Exclude
.x.OrDefault()
, x.Or(b).IfLess()
, orders.One(with: id)
, orders.All
.path.Without(".exe").AtEnd
.Instances.Of
, Is.EqualTo
.using static
) - Ensure(that: x)
, items.All(Weapons)
.This is how the external and contemplated is brought to the fore. At first it is thought, and only then its concrete embodiments, not so significant, are thought as long as the code is read as text. From this it follows that the judge will not so much taste as language - it determines the difference between item.GetValueOrDefault
and item.OrDefault
.
What is better, understandable, but non-working method, or working, but incomprehensible? White Castle without furniture and rooms or a barn with sofas in the style of Louis IV? A luxury yacht without an engine or a groaning barge with a quantum computer that nobody knows how to use?
Polar answers are not suitable, but "somewhere in the middle" too.
In my opinion, both concepts are inseparable: carefully choosing a cover for a book, we look with doubt at errors in the text, and vice versa. I would not want the Beatles to play low-quality music, but that they were called MusicHelper too.
Another thing is that work on a word as part of the development process is an underestimated, unusual thing, and therefore some extreme measure is still needed in judgments. This cycle is an extreme form and picture.
Thank you all for your attention!
Who is interested to see more examples, they can be found on my GitHub, for example, in the Pocket.Common
library. (not for universal and universal use)
Source: https://habr.com/ru/post/447830/
All Articles