El código está vivo y muerto. Tercera parte Código como texto

Tienes que leer el código para acompañar el programa, y ​​cuanto más fácil sea hacerlo, más se parece a un lenguaje natural, entonces te meterás más rápido y te enfocarás en lo principal.


En los últimos dos artículos, mostré que las palabras cuidadosamente seleccionadas ayudan a comprender mejor la esencia de lo que está escrito, pero pensar solo en ellas no es suficiente, porque cada palabra existe en dos formas: como en sí misma y como parte de una oración. Repetir CurrentThread aún no se repite hasta que lo leamos en el contexto de Thread.CurrentThread .


Así, guiados por notas y melodías simples, ahora veremos qué es la música.


Tabla de contenidos del ciclo


  1. Los objetos
  2. Acciones y propiedades
  3. Código como texto

Código como texto


La mayoría de las interfaces fluidas están diseñadas con énfasis en lo externo en lugar de lo interno, por lo que son muy fáciles de leer. Por supuesto, no es gratis: el contenido se está debilitando en cierto sentido. Entonces, digamos, en el paquete FluentAssertions puede escribir: (2 + 2).Should().Be(4, because: "2 + 2 is 4!") , Y, en relación con la lectura, because ve elegante, pero dentro del Be() más bien, se errorMessage parámetro error o errorMessage .


En mi opinión, tales exenciones no son significativas. Cuando aceptamos que el código es un texto, sus componentes dejan de pertenecer a sí mismos: ahora forman parte de algún tipo de "Éter" universal.


Mostraré con ejemplos cómo tales consideraciones se convierten en experiencia.


Interlocked


Permítame recordarle el caso Interlocked , que cambiamos de Interlocked.CompareExchange(ref x, newX, oldX) a Atomically.Change(ref x, from: oldX, to: newX) , usando nombres claros de métodos y parámetros.


ExceptWith


El tipo ISet<> tiene un método llamado ExceptWith . Si observa una llamada como items.ExceptWith(other) , no se dará cuenta de inmediato de lo que está sucediendo. Pero solo tiene que escribir: items.Exclude(other) . items.Exclude(other) , ya que todo encaja.


GetValueOrDefault


Al trabajar con Nullable<T> llamar a x.Value arrojará una excepción si x es null . Si aún necesita obtener Value , use x.GetValueOrDefault : es Value o el valor predeterminado. Voluminoso


La expresión "o x, o el valor predeterminado" coincide con la breve y elegante x.OrDefault .


 int? x = null; var a = x.GetValueOrDefault(); // ,  .  . var b = x.OrDefault(); //  —  ,   . var c = x.Or(10); //     . 

Con OrDefault y Or hay algo que vale la pena recordar: cuando se trabaja con un operador .? no puede escribir algo como x?.IsEnabled.Or(false) , solo (x?.IsEnabled).Or(false) (en otras palabras, el operador .? cancela todo el lado derecho si null en la izquierda).


La plantilla se puede aplicar cuando se trabaja con IEnumerable<T> :


 IEnumerable<int> numbers = null; // . var x = numbers ?? Enumerable.Empty<int>(); //   . var x = numbers.OrEmpty(); 

Math.Min y Math.Max


Una idea con Or puede desarrollarse en tipos numéricos. Suponga que desea tomar el número máximo de a y b . Luego escribimos: Math.Max(a, b) o a > b ? a : b a > b ? a : b . Ambas opciones parecen bastante familiares, pero, sin embargo, no parecen un lenguaje natural.


Puede reemplazarlo con: a. a.Or(b).IfLess() : tome b si a menor . Adecuado para tales situaciones:


 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


A veces necesitas ensamblar una secuencia en una cadena, separando los elementos con un espacio o una coma. Para hacer esto, use string.Join , por ejemplo, así: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3". .


Un simple "Dividir el número de coma" puede convertirse repentinamente en "Adjuntar una coma a cada número de la lista" ; esto ciertamente no es un código como texto.


 var numbers = new [] { 1, 2, 3 }; // ""    —  . var x = string.Join(", ", numbers); //    — ! var x = numbers.Separated(with: ", "); 

Regex


Sin embargo, string.Join es bastante inofensivo en comparación con la forma en que Regex veces se usa incorrectamente y para otros fines. Donde puede sobrevivir con texto simple y legible, por alguna razón se prefiere una entrada demasiado complicada.


Comencemos con uno simple: determinar que una cadena representa un conjunto de números:


 string id = ...; // ,  . var x = Regex.IsMatch(id, "^[0-9]*$"); // . var x = id.All(x => x.IsDigit()); // ! var x = id.IsNumer(); 

Otro caso es averiguar si hay al menos un carácter en la cadena de la secuencia:


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

Cuanto más complicada es la tarea, más difícil es el "patrón" de solución: dividir un registro del "HelloWorld" en unas pocas palabras "Hello World" , alguien en lugar de un algoritmo simple quería un monstruo:


 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: " "); 

Sin duda, las expresiones regulares son efectivas y universales, pero quiero entender lo que está sucediendo a primera vista.


Substring y Remove


Sucede que necesita eliminar alguna parte de la línea desde el principio o el final, por ejemplo, de la path , la extensión .txt , si la hay.


 string path = ...; //    . var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path; //   . var x = path.Without(".exe").AtEnd; 

Nuevamente, la acción y el algoritmo desaparecieron, y se dejó una línea simple sin la extensión .exe al final .


Dado que el método Without debe devolver un cierto Sin WithoutExpression , piden otro: path.Without("_").AtStart . path.Without("_").AtStart y path.Without("Something").Anywhere path.Without("_").AtStart path.Without("Something").Anywhere . También es interesante que se pueda construir otra expresión con la misma palabra: name.Without(charAt: 1) . name.Without(charAt: 1) - elimina el carácter en el índice 1 y devuelve una nueva línea (útil para calcular permutaciones). ¡Y también legible!


Type.GetMethods


Para obtener métodos de cierto tipo usando la reflexión, use:


 Type type = ...; //   `Get` ,   `|`.     . var x = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); // ,  . `Or`   , . var x = type.Methods(_ => _.Instance.Public.Or.NonPublic); 

(Lo mismo ocurre con GetFields y GetProperties ).


Directory.Copy


Todas las operaciones con carpetas y archivos a menudo se generalizan a DirectoryUtils , FileSystemHelper . Implementan bypass del sistema de archivos, limpieza, copia, etc. ¡Pero aquí puedes encontrar algo mejor!


Mostramos el texto “copie todos los archivos de 'D: \ Source' a 'D: \ Target'” al código "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target") AsDirectory() : devuelve DirectoryInfo de la string y Copy() crea una instancia de CopyExpression que describe una API única para construir expresiones (por ejemplo, no puede llamar a Copy().Files.Files ). Entonces se abre la oportunidad de copiar no todos los archivos, sino algunos: Copy().Files.Where(x => x.IsNotEmpty) .


GetOrderById


En el segundo artículo, escribí que IUsersRepository.GetUser(int id) es redundante, y mejor, IUsersRepository.User(int id) . En consecuencia, en un IOrdersRepository similar IOrdersRepository no tenemos GetOrderById(int id) , sino Order(int id) . Sin embargo, en otro ejemplo, se sugirió que la variable de dicho repositorio se llame no _ordersRepository , sino simplemente _orders .


Ambos cambios son buenos por sí solos, pero no se suman en el contexto de lectura: llamar a _orders.Order(id) parece detallado. Sería posible _orders.Get(id) , pero las órdenes fallan, solo queremos especificar la que tenga dicho identificador . Uno que es One , por lo tanto:


 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


En objetos como IOrdersRepository , a menudo se encuentran otros métodos: AddOrder , RemoveOrder , GetOrders . Las dos primeras repeticiones desaparecen, y se obtienen Add y Remove (con las entradas correspondientes _orders.Add(order) y _orders.Remove(order) ). Con GetOrders más difícil cambiar el nombre de los Orders poco. A ver:


 IOrdersRepository orders = ...; //   . var x = orders.GetOrders(); //  `Get`,  . var x = orders.Orders(); // ! var x = orders.All(); 

Cabe señalar que con el antiguo _ordersRepository repeticiones en las llamadas GetOrders o GetOrderById no son tan notables, ¡porque estamos trabajando con el repositorio!


Nombres como One , All son adecuados para muchas interfaces que representan muchas. Digamos, en la conocida implementación de la API de GitHub - octokit - obtener todos los repositorios de usuarios se parece a gitHub.Repository.GetAllForUser("John") , aunque es más lógico: gitHub.Users.One("John").Repositories.All . En este caso, la obtención de un repositorio será, respectivamente, gitHub.Repository.Get("John", "Repo") lugar del obvio gitHub.Users.One("John").Repositories.One("Repo") . El segundo caso parece más largo, pero es internamente consistente y refleja la plataforma. Además, utilizando métodos de extensión, se puede acortar a gitHub.User("John").Repository("Repo") .


Dictionary.TryGetValue


La obtención de valores del diccionario se divide en varios escenarios que difieren solo en lo que debe hacerse si no se encuentra la clave:


  • arrojar un error ( dictionary[key] );
  • devuelve el valor predeterminado (no implementado, pero a menudo escribe GetValueOrDefault o TryGetValue );
  • devolver algo más (no implementado, pero esperaría GetValueOrOther );
  • escribe el valor especificado en el diccionario y lo devuelve (no implementado, pero se GetOrAdd ).

Las expresiones convergen en el punto " tome algo de X o Y si X no lo es ". Además, como en el caso de _ordersRepository , llamaremos a la variable del diccionario no itemsDictionary , sino items .


Luego, para la parte "tomar algo de X" , una llamada de los elementos del formulario. items.One(withKey: X) ideal, devolviendo una estructura con cuatro terminaciones :


 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


Veamos cómo crear todas las instancias existentes de tipo T en el ensamblaje:


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

Por lo tanto, a veces, el nombre de una clase estática es el comienzo de una expresión.


Algo similar se puede encontrar en NUnit: Assert.That(2 + 2, Is.EqualTo(4)) - Is y no fue concebido como un tipo autosuficiente.


Argument.ThrowIfNull


Ahora echemos un vistazo a la verificación de precondición:


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

Ensure.NotNull(argument) : agradable, pero no del todo inglés. Otra cosa es el Ensure(that: x).NotNull() escrito anteriormente. Si tan solo pudiera ...


Por cierto, puedes! Escribimos Contract.Ensure(that: argument).IsNotNull() e importamos el tipo de Contract using static . Por lo tanto, se Ensure(that: number).InRange(from: 5, to: 10) todo tipo de Ensure(that: type).Implements<T>() , Ensure(that: number).InRange(from: 5, to: 10) , etc.


La idea de la importación estática abre muchas puertas. Un hermoso ejemplo por el bien de: en lugar de items.Remove(x) escribir Remove(x, from: items) . Pero curioso es la reducción de enum y propiedades que devuelven funciones.


 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 exótica


En C # 7.1 y superior, puede escribir no Find(1, @in: items) , sino Find(1, in items) , donde Find se define como Find<T>(T item, in IEnumerable<T> items) . Este ejemplo no es práctico, pero muestra que todos los medios son buenos en la lucha por la legibilidad.


Total


En esta parte, analicé varias formas de trabajar con la legibilidad del código. Todos ellos pueden generalizarse para:


  • El parámetro con nombre como parte de la expresión es Should().Be(4, because: "") , Atomically.Change(ref x, from: oldX, to: newX) .
  • Un nombre simple en lugar de detalles técnicos es Separated(with: ", ") , Exclude .
  • El método como parte de la variable es x.OrDefault() , x.Or(b).IfLess() , orders.One(with: id) , orders.All .
  • El método como parte de la expresión es path.Without(".exe").AtEnd .
  • El tipo como parte de la expresión es Instances.Of , Is.EqualTo .
  • El método como parte de la expresión ( using static ) es Ensure(that: x) , items.All(Weapons) .

Por lo tanto, lo externo y lo contemplado se ponen en primer plano. Al principio se piensa, y luego se piensan sus encarnaciones específicas, no tan significativas, siempre que el código se lea como texto. De ello se deduce que el juez no es tanto el gusto como el idioma : determina la diferencia entre item.GetValueOrDefault y item.OrDefault .


Epílogo


¿Cuál es mejor, claro, pero no es un método de trabajo, o funciona, pero es incomprensible? ¿Un castillo blanco como la nieve sin muebles y habitaciones o un cobertizo con sofás al estilo de Luis XIV? ¿Un yate de lujo sin motor o una barcaza que gruñe con una computadora cuántica que nadie puede usar?


Las respuestas polares no encajan, pero "en algún lugar en el medio" también.


En mi opinión, ambos conceptos son inextricables: al elegir cuidadosamente una portada para un libro, dudamos de ver los errores en el texto y viceversa. No quisiera que los Beatles tocasen música de baja calidad, pero también deberían llamarse MusicHelper .


Otra cosa es que trabajar en una palabra como parte del proceso de desarrollo es una cosa inusual y subestimada y, por lo tanto, todavía se necesita algún tipo de juicio extremo. Este ciclo es el extremo de la forma y la imagen.


¡Gracias a todos por su atención!


Referencias


Cualquier persona interesada en ver más ejemplos se puede encontrar en mi GitHub, por ejemplo, en la biblioteca Pocket.Common . (no para uso mundial y universal)

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


All Articles