Você precisa ler o código para acompanhar o programa e, quanto mais fácil, mais ele parecerá uma linguagem natural - então você entrará mais rápido e se concentrará no principal.
Nos últimos dois artigos, mostrei que palavras cuidadosamente selecionadas ajudam a entender melhor a essência do que está escrito, mas pensar apenas nelas não é suficiente, porque cada palavra existe em duas formas: como ela própria e como parte de uma frase. Repetir CurrentThread
ainda não está se repetindo até a lermos no contexto de Thread.CurrentThread
.
Assim, guiados por notas e melodias simples, agora veremos o que é música.
Sumário do ciclo
- Os objetos
- Ações e propriedades
- Código como texto
Código como texto
A maioria das interfaces fluentes é projetada com ênfase em externa e não interna, de modo que é muito fácil de ler. Obviamente, não de graça: o conteúdo está enfraquecendo. Então, digamos, no pacote FluentAssertions
, FluentAssertions
pode escrever: (2 + 2).Should().Be(4, because: "2 + 2 is 4!")
E, em relação à leitura, because
parece elegante, mas dentro do Be()
, o parâmetro error
ou errorMessage
é errorMessage
.
Na minha opinião, essas isenções não são significativas. Quando concordamos que o código é um texto, seus componentes deixam de pertencer a si mesmos: agora fazem parte de algum tipo de "Éter" universal.
Mostrarei por exemplos como essas considerações se tornam experiência.
Interlocked
Deixe-me lembrá-lo do caso Interlocked
, que Interlocked.CompareExchange(ref x, newX, oldX)
de Interlocked.CompareExchange(ref x, newX, oldX)
para Atomically.Change(ref x, from: oldX, to: newX)
, usando nomes claros de métodos e parâmetros.
ExceptWith
O tipo ISet<>
possui um método chamado ExceptWith
. Se você olhar para uma chamada como items.ExceptWith(other)
, você não perceberá imediatamente o que está acontecendo. Mas você só precisa escrever: items.Exclude(other)
, pois tudo se encaixa.
GetValueOrDefault
Ao trabalhar com Nullable<T>
chamar x.Value
lançará uma exceção se x
for null
. Se você ainda precisar obter Value
, use x.GetValueOrDefault
: é Value
ou o valor padrão. Volumoso.
A expressão "ou x, ou o valor padrão" corresponde ao x.OrDefault
curto e elegante.
int? x = null; var a = x.GetValueOrDefault();
Com OrDefault
e Or
há uma coisa que vale a pena lembrar: ao trabalhar com um operador .?
você não pode escrever algo como x?.IsEnabled.Or(false)
, somente (x?.IsEnabled).Or(false)
(em outras palavras, o operador .?
cancela todo o lado direito se null
à esquerda).
O modelo pode ser aplicado ao trabalhar com IEnumerable<T>
:
IEnumerable<int> numbers = null;
Math.Min
e Math.Max
Uma ideia com Or
pode ser desenvolvida em tipos numéricos. Suponha que você queira pegar o número máximo de a
e b
. Então escrevemos: Math.Max(a, b)
ou a > b ? a : b
a > b ? a : b
. Ambas as opções parecem bastante familiares, mas, no entanto, não parecem uma linguagem natural.
Você pode substituí-lo por: a.Or(b).IfLess()
- a.Or(b).IfLess()
a
ou b
se a
menor . Adequado para tais situações:
Creature creature = ...; int damage = ...;
string.Join
Às vezes, você precisa montar uma sequência em uma sequência, separando os elementos com um espaço ou vírgula. Para fazer isso, use string.Join
, por exemplo, assim: string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
string.Join(", ", new [] { 1, 2, 3 }); // "1, 2, 3".
.
Um simples "Dividir o número da vírgula" pode repentinamente se tornar "Anexar uma vírgula a cada número da lista" - esse certamente não é um código como texto.
var numbers = new [] { 1, 2, 3 };
Regex
No entanto, string.Join
é bastante inofensivo em comparação com como o Regex
às vezes Regex
usado incorretamente e para outros fins. Onde você pode se familiarizar com texto simples e legível, por algum motivo, é preferível uma entrada complicada.
Vamos começar com um simples - determinando que uma string representa um conjunto de números:
string id = ...;
Outro caso é descobrir se há pelo menos um caractere na sequência da sequência:
string text = ...;
Quanto mais complicada a tarefa, mais difícil é o “padrão” da solução: dividir um registro do "HelloWorld"
em poucas palavras "Hello World"
, alguém em vez de um algoritmo simples queria um monstro:
string text = ...;
Sem dúvida, expressões regulares são eficazes e universais, mas quero entender o que está acontecendo à primeira vista.
Substring
e Remove
Acontece que você precisa remover alguma parte da linha do início ou do fim, por exemplo, do path
- a extensão .txt
, se houver.
string path = ...;
Novamente, a ação e o algoritmo se foram e uma linha simples foi deixada sem a extensão .exe no final .
Como o método Without
deve retornar um determinado WithoutExpression
, eles pedem outro: path.Without("_").AtStart
e path.Without("Something").Anywhere
. Também é interessante que outra expressão possa ser construída com a mesma palavra: name.Without(charAt: 1)
- exclui o caractere no índice 1 e retorna uma nova linha (útil no cálculo de permutações). E também legível!
Type.GetMethods
Para obter métodos de um determinado tipo usando reflexão, use:
Type type = ...;
(O mesmo vale para GetFields
e GetProperties
.)
Directory.Copy
Todas as operações com pastas e arquivos são geralmente generalizadas para DirectoryUtils
, FileSystemHelper
. Eles implementam desvio do sistema de arquivos, limpeza, cópia, etc. Mas aqui você pode criar algo melhor!
Exibimos o texto “copie todos os arquivos de 'D: \ Source' para 'D: \ Target'” para o código "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target")
AsDirectory()
- retorna DirectoryInfo
da string
e Copy()
- cria uma instância de CopyExpression
que descreve uma API exclusiva para construir expressões (você não pode chamar Copy().Files.Files
, por exemplo). Em seguida, é aberta a oportunidade de copiar não todos os arquivos, mas alguns: Copy().Files.Where(x => x.IsNotEmpty)
.
GetOrderById
No segundo artigo, escrevi que IUsersRepository.GetUser(int id)
é redundante e, melhor, IUsersRepository.User(int id)
. Assim, em um IOrdersRepository
semelhante IOrdersRepository
não temos GetOrderById(int id)
, mas Order(int id)
. No entanto, em outro exemplo, sugeriu-se que a variável desse repositório fosse chamada não _ordersRepository
, mas simplesmente _orders
.
Ambas as alterações são boas por si só, mas elas não se somam no contexto de leitura: chamar _orders.Order(id)
parece detalhado. Seria possível _orders.Get(id)
, mas os pedidos falham, queremos apenas especificar aquele que possui esse identificador . Um que é One
, portanto:
IOrdersRepository orders = ...; int id = ...;
GetOrders
Em objetos como IOrdersRepository
, outros métodos são frequentemente encontrados: AddOrder
, RemoveOrder
, GetOrders
. As duas primeiras repetições desaparecem, e Add
and Remove
são obtidos (com as entradas correspondentes _orders.Add(order)
e _orders.Remove(order)
). Com GetOrders
mais difícil renomear Orders
pouco os Orders
. Vamos ver:
IOrdersRepository orders = ...;
Deve-se notar que, com o antigo _ordersRepository
repetições nas chamadas GetOrderById
ou GetOrderById
não são tão perceptíveis, porque estamos trabalhando com o repositório!
Nomes como One
, All
são adequados para muitas interfaces que representam muitos. Por exemplo, na conhecida implementação da API do GitHub - octokit
- obter todos os repositórios de usuários se parece com gitHub.Repository.GetAllForUser("John")
, embora seja mais lógico - gitHub.Users.One("John").Repositories.All
. Nesse caso, a obtenção de um repositório será, respectivamente, gitHub.Repository.Get("John", "Repo")
vez do óbvio gitHub.Users.One("John").Repositories.One("Repo")
. O segundo caso parece mais longo, mas é consistente internamente e reflete a plataforma. Além disso, usando métodos de extensão, ele pode ser reduzido para gitHub.User("John").Repository("Repo")
.
Dictionary.TryGetValue
A obtenção de valores do dicionário é dividida em vários cenários que diferem apenas no que precisa ser feito se a chave não for encontrada:
- lançar um erro (
dictionary[key]
); - retornar o valor padrão (não implementado, mas muitas vezes escreva
GetValueOrDefault
ou TryGetValue
); - retornar outra coisa (não implementada, mas eu esperaria
GetValueOrOther
); - escreva o valor especificado no dicionário e retorne-o (não implementado, mas
GetOrAdd
foi GetOrAdd
).
As expressões convergem no ponto " pegue algum X ou Y se X não estiver ". Além disso, como no caso de _ordersRepository
, chamaremos a variável de dicionário de não itemsDictionary
, mas items
.
Em seguida, para a parte "pegue um X" , uma chamada dos items.One(withKey: X)
do formulário. items.One(withKey: X)
ideal, retornando uma estrutura com quatro finais :
Dictionary<int, Item> items = ...; int id = ...;
Assembly.GetTypes
Vejamos a criação de todas as instâncias existentes do tipo T
na montagem:
Assim, às vezes, o nome de uma classe estática é o começo de uma expressão.
Algo semelhante pode ser encontrado em NUnit: Assert.That(2 + 2, Is.EqualTo(4))
- Is
e não foi concebido como um tipo auto-suficiente.
Argument.ThrowIfNull
Agora vamos dar uma olhada na verificação de pré-condição:
Ensure.NotNull(argument)
- bom, mas não muito inglês. Outra coisa é o Ensure(that: x).NotNull()
escrito acima. Se ao menos pudesse ...
A propósito, você pode! Escrevemos Contract.Ensure(that: argument).IsNotNull()
e importamos o tipo de Contract
using static
. Portanto, todos os tipos de Ensure(that: type).Implements<T>()
, Ensure(that: number).InRange(from: 5, to: 10)
etc. são Ensure(that: number).InRange(from: 5, to: 10)
.
A idéia de importação estática abre muitas portas. Um belo exemplo para: em vez de items.Remove(x)
write Remove(x, from: items)
. Mas curioso é a redução de enum
e propriedades que retornam funções.
IItems items = ...;
Find
exótico
No C # 7.1 e superior, você não pode escrever Find(1, @in: items)
, mas Find(1, in items)
, onde Find
é definido como Find<T>(T item, in IEnumerable<T> items)
. Este exemplo é impraticável, mas mostra que todos os meios são bons na luta pela legibilidade.
Total
Nesta parte, observei várias maneiras de trabalhar com legibilidade de código. Todos eles podem ser generalizados para:
- O parâmetro nomeado como parte da expressão é
Should().Be(4, because: "")
, Atomically.Change(ref x, from: oldX, to: newX)
. - Um nome simples em vez de detalhes técnicos é
Separated(with: ", ")
, Exclude
. - O método como parte da variável é
x.OrDefault()
, x.Or(b).IfLess()
, orders.One(with: id)
, orders.All
. - O método como parte da expressão é
path.Without(".exe").AtEnd
. - O tipo como parte da expressão é
Instances.Of
, Is.EqualTo
. - O método como parte da expressão (
using static
) é Ensure(that: x)
, items.All(Weapons)
.
Assim, o externo e o contemplado são trazidos à tona. A princípio, pensa-se, e então suas encarnações específicas são pensadas, não tão significativas, desde que o código seja lido como texto. Daqui resulta que o juiz não tem tanto gosto quanto a linguagem - ele determina a diferença entre item.GetValueOrDefault
e item.OrDefault
.
Epílogo
Qual é o método melhor, claro, mas não está funcionando, ou está funcionando, mas é incompreensível? Um castelo branco como a neve, sem móveis e quartos ou um barracão com sofás no estilo de Luís XIV? Um iate de luxo sem motor ou barcaça com um computador quântico que ninguém pode usar?
Respostas polares não se encaixam, mas "em algum lugar no meio" também.
Na minha opinião, ambos os conceitos são inextricáveis: escolhendo cuidadosamente uma capa para um livro, examinamos duvidosamente os erros no texto e vice-versa. Eu não gostaria que os Beatles tocassem músicas de baixa qualidade, mas eles também deveriam ser chamados de MusicHelper .
Outra coisa é que trabalhar em uma palavra como parte do processo de desenvolvimento é algo subestimado e incomum e, portanto, ainda é necessário algum tipo de julgamento extremo. Esse ciclo é o extremo da forma e da imagem.
Obrigado a todos pela atenção!
Referências
Qualquer pessoa interessada em ver mais exemplos pode ser encontrada no meu GitHub, por exemplo, na biblioteca Pocket.Common
. (não para uso mundial e universal)