Как говорит гуру программирования Роберт К. Мартин, несмотря на то, что чистый код может быть не идеален в своем определении, его результаты отчетливо понятны. Вы наверняка предпочтете, чтобы ваш код был читаем, подобно хорошей книге, и без проблем расширялся. Ошибки либо должны быть легко обнаружимыми, либо избегаться ввиду самой структуры программы. Чистый код позволяет поддерживать продуктивность на высоте и одновременно снижает затраты на обслуживание.
Вот цитата из книги “Чистый код. Создание, анализ, рефакторинг”:
“Чистый код может читать и улучшать любой разработчик, а не только его изначальный автор”, — Дэйв Томас.
Это одна из важнейших характеристик чистого кода. Если код можете обслуживать не только вы, но и любой другой разработчик близкого к вам уровня, то такой он автоматически впишется в большинство требований звания “чистый”. Естественно, он может быть не на все 100% таковым, но достаточно, чтобы он соответствовал свойствам, описанным в начале.
Читаемый код
Для того, чтобы другие разработчики могли обслуживать ваш код, он должен легко читаться и быть понятным. Целью при написании каждой его строки всегда должна быть читаемость. Даже одна строчка может сбить программиста с толку и привести к трате ценных минут на понимание кода.
Так что же делает код более читаемым?
В этой статье мы не будем разбирать методы, которые описаны в книге “Чистый код. Создание, анализ, рефакторинг” — с ними вы можете ознакомиться самостоятельно. Вместо этого я сосредоточусь на конкретных рекомендациях, которые выработал за время работы с .NET в течение последних 8 лет.
Эти рекомендации нацелены на то, чтобы сделать код максимально лаконичным и читаемым. Они не заменяют методики написания чистого кода, приведенные в упомянутой выше книге, но дополняют их.
Рекомендации к .NET и C#
1. Избегайте “Else”
Первый принцип прост — избегать else
. Практически всякий раз, когда вы используете else
, его можно легко избежать и сделать код более читаемым. Взгляните на следующие примеры:
//Плохо
public void Test(Test test) {
if (test == null) {
throw new ArgumentNullException(nameof(test));
} else {
//Do something
}
}
//Хорошо
public void Test(Test test) {
if (test == null)
throw new ArgumentNullException(nameof(test));
//Нужные действия
}
// ____________________
//Плохо
public Result Test(SomeEnum someEnum) {
if (someEnum == SomeEnum.A) {
//Do something
} else {
//Do the Other thing
}
}
//Хорошо
public Result Test(SomeEnum someEnum) {
return someEnum switch {
SomeEnum.A => DoSomething(),
SomeEnum.B => DoTheOtherThing(),
_ => throw new NotImplementedException()
};
Result DoSomething() { }
Result DoTheOtherThing() { }
}
Первый пример также известен как раннее возвращение. Нужно всегда отчетливо указывать в первых строках кода, чего вы ожидаете от аргументов.
Второй пример показывает откровенно новый способ переключения инструкций в C# — сопоставление шаблонов. Здесь вы можете легко заметить, что происходит при прохождении явного кейса, и какая функция этот кейс обрабатывает. Кроме того, тут была применена еще одна рекомендация: создавать дополнительную, грамотно названную, функцию для каждой вложенной логики.
2. Избегайте высокой цикломатической сложности
Еще один уместный здесь принцип заключается в избежании высокой цикломатической сложности. Это означает, что метод должен иметь минимум сторонней логики, выполняющейся при возникновении определенного состояния. Это можно представить на примере реализации интерфейсов для классов и прописывания отдельной логики в каждом классе:
interface IRequestHandler
{
Result Handle();
}
internal class Test1 : IRequestHandler
{
public Result Handle() {
//Действия
}
}
internal class Test2 : IRequestHandler
{
public Result Handle() {
//Прочие действия
}
}
public Result HandleRequest(IRequestHandler requestHandler)
=> requestHandler.Handle();
3. Избегайте излишних уровней отступов
В примере avoid_else.cs
можно заметить и еще одно отличие, вносимое исключением else
. Вы избегаете в методе одного уровня отступов. Хорошей практикой для повышения читаемости кода будет просто избежание лишних уровней отступов. Я на собственном опыте обнаружил, что лучше всего придерживаться трех-четырех. При этом они должны быть поистине необходимыми.
Например, никогда не следует вкладывать ветку if
в другую ветку if
. По правде говоря, if
вообще лучше стараться избегать.
Практически единственный случай, в котором у меня получается более одного уровня отступов — это использование запросов LINQ.
4. Избегайте циклов с помощью Linq
Вам мог встречаться подобный код:
//Плохо
public List<string> Test(string[] someStrings) {
List<string> listToFill = new();
foreach (var s in someStrings) {
listToFill.Add(s.ToUpper());
}
return listToFill;
}
//Хорошо
public List<string> Test(IEnumerable<string> someStrings)
=> someStrings
.Select(s => s.ToUpper())
.ToList();
Спросите себя: “Можно ли заменить цикл запросом Linq?”. Гарантирую, что в 90% случаев ответом будет: “Да”. Это может быть моим личным мнением, но я считаю, что хороший запрос Linq гораздо понятнее масштабного цикла for
.
Чаще всего при использовании таких циклов вы собираетесь преобразовать один или более классов Enumerable
в результат. В Linq есть все необходимое для применения проверок, трансформаций и действий для IEnumerable<T>
. Если вы с этим языком еще не работали, то настоятельно его вам рекомендую.
В Linq у вас есть выбор между синтаксисом запросов и синтаксисом методов. Лично я предпочитаю последний, но тут уже дело вкуса. Все расширяющие методы для IEnumerable
можно найти здесь.
5. Извлечение и именование методов
Извлечение методов и присваивание им понятного имени также сильно повышает читаемость.
//Плохо
public List<string> Test(IEnumerable<string> someStrings)
=> someStrings
.Where(s => s.StartsWith("a", StringComparison.OrdinalIgnoreCase))
.Select(s => s.Replace("a", ""))
.ToList();
//Хорошо(Представьте, что здесь используется более сложная логика)
public List<string> Test(IEnumerable<string> someStrings)
=> someStrings
.Where(StartsWithA)
.Select(RemoveA)
.ToList();
private static bool StartsWithA(string s)
=> s.StartsWith("a", StringComparison.OrdinalIgnoreCase);
private static string RemoveA(string s)
=> s.Replace("a", "");
Представьте, что в методах Where()
или Select()
используется более сложная логика, чем в данном простом примере.
Если вы являетесь автором этой функции, то знаете, что здесь должно происходить, но другой разработчик может затрудниться это понять. Так почему бы просто не извлечь метод и не присвоить ему удачное имя? Это сразу прояснит его назначение, и следующий программист поймет, что этот код должен делать.
6. Используйте в коде разрывы и отступы
Еще один способ повышения читаемости кода — это правильное его разделение и использование отступов для строк. В большинстве случаев правильно расставить отступы вам поможет Visual Code, хотя тоже не идеально.
Вот несколько хороших правил относительно того, что нужно разбивать и где использовать отступы.
- Строки кода не должны выступать за середину экрана. Это упростит его чтение и позволит использовать разделение экрана.
- Уровень отступов открывающих и закрывающих скобок должен совпадать. Я знаю, что это займет несколько дополнительных секунд, но раскрытие аргументов и добавление им имен упрощает чтение метода и его редактирование (см. второй пример).
- Делать разрыв перед точкой или после запятой. Таким образом, вы можете удалить целую строку для перемещения части кода.
- Каждая лямбда-функция должна отделяться дополнительным уровнем отступа .
Разрыв перед точкой:
//Плохо
public Task<bool> BlobExistsAsync(string containerName, string blobName)
=> client.GetBlobContainerClient(containerName).GetBlobClient(blobName).ExistsAsync().GetValueAsync();
//Хорошо
public Task<bool> BlobExistsAsync(string containerName, string blobName)
=> client
.GetBlobContainerClient(containerName)
.GetBlobClient(blobName)
.ExistsAsync()
.GetValueAsync();
Разрыв аргументов и добавление имен:
public void Test() {
//Плохо
var test1 = new Test("a", "b", "c", "d", "e");
//Хорошо
var test2 = new Test(
argument1: "a",
argument2: "b",
argument3: "c",
argument4: "d",
argument5: "e"
);
}
7. Используйте краткий синтаксис
Думаю, вы можете поспорить, насколько использование краткого синтаксиса окажется оправданным. Однако, если вы к этому привыкните, то получите лучшую читаемость, одновременно избавившись от ненужного шума в коде.
C# постоянно обновляется, и каждый год в нем появляется много новых возможностей. Нередко свежие версии предлагают более сжатый вариант написания некоторых инструкций, которые вы уже используете повседневно. Вот некоторые примеры для C#10:
- Краткая
if
:
public void Test(bool predicate) {
//Плохо
string s;
if (predicate) {
s = "Hello World";
} else {
s = "Bye World";
}
//Хорошо
var s = predicate
? "Hello World"
: "Bye World";
}
- Краткие проверки на
null
:
public void Test() {
//Проверяет левое значение на null, и если это так, использует //правое значение;
var a = null;
var b = a ?? new Xyz();
//Выбрасывает исключение, если значение null
var c = a ?? throw new Exception();
//Если d равно null, создает new D();
var d = null;
d ??= new D();
}
- А вот, на мой взгляд, самая краткая интерполяция строк:
public void Test() {
var a = "Name"
var s = $"Hello {a}"
// s - это "Hello Name"
}
- Сопоставление шаблонов. По началу покажется сложным, но в итоге окажется самым удобочитаемым механизмом различения кейсов:
public void Test() {
var a = 1;
var b = a switch {
1 => "a is 1",
2 => "a is 2",
_ => "a is not 1 or 2"
}
// b = "a is 1"
}
- Тела выражений. Лучший вариант для однострочных функций или свойств:
public string Test(bool predicate) {
return predicate ? "true" : "false";
}
public string Test(bool predicate)
=> predicate ? "true" : "false";
- Типы записей. Лучше всего подходят для Plain Old Class Object (POCO):
//Старый
public class Xyz() {
public string Test { get; }
public string Test2 { get; }
public Xyz(string test, string test2){
Test = test;
Test2 = test2
}
}
//Новый
public record Xyz(string Test, string Test2);
В этом списке я перечислил некоторые примеры краткого синтаксиса, которые использую изо дня в день. Кому-то они поначалу покажутся сложноватыми для восприятия, но по своему опыту скажу, что при меньшем количестве символов читать становится проще.
8. Global Usings и FileScopedNamespaces
В C#10 появилось две интересных возможности: file-scoped namespaces и global usings.
Пространства имен в масштабах файла избавляют нас от одного уровня отступов на протяжении всего файла, что немного повышает читаемость кода. При добавлении новых классов или методов у вас будет на одну }
меньше.
//Старый вариант
namespace This.Is.A.Test.Namespace {
public class Test {
}
}
//Новый вариант
namespace This.Is.A.Test.Namespace;
public class Test {
}
Global usings
работают в масштабе проекта и экономят кучу места в начале файлов. Некоторые usings
окажутся очень полезны для последующих разработчиков, помогая понять, на какие пакеты и пространства имен вы ссылаетесь в коде, тем не менее, использовать их нужно осторожно. Добавляйте global usings
только для тех пространств имен, которые используются в проекте часто.
Результат global usings
и implicit usings
:
//Старый вариант
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace xyz;
class Abc {
}
//Новый вариант
namespace xyz;
class Abc {
}
В этом примере usings
также можно импортировать, включив в проекте функционал implicit usings
.
Читайте также:
- 5 простейших приемов работы на C#
- Чем отличается C++ от C#?
- Использование методов расширения в C# для элегантного и плавного кода
Читайте нас в Telegram, VK и Дзен
Перевод статьи Tobias Streng: 8 Guidelines to Write Readable Code in C# With .NET 6