Превратите свой код в восхитительное произведение искусства, которое приятно читать

Целью каждого программного проекта, который предположительно будет иметь долгую жизнь, является чистая и читаемая кодовая база. Удобочитаемость — наряду с чистой архитектурой — главное требование для долгоживущего проекта. Чистый код позволит снизить затраты на поддержку проекта и повысить производительность.

Основная цель чистого кода состоит в том, чтобы

«Чистый код может быть прочитан и улучшен разработчиком, отличным от его первоначального автора». (Дэйв Томас о чистом коде)

Я считаю, что обеспечение возможности сопровождения кода не только вами, но и другими разработчиками с аналогичным опытом, является важным аспектом чистого кода. Если вы будете следовать этому правилу, полученный код уже будет в значительной степени соответствовать требованиям чистого кода. Хотя он может быть не идеальным, он будет достаточно чистым, чтобы дать результаты, сравнимые с упомянутыми выше.
Следующие рекомендации следует рассматривать как дополнение к устоявшимся правилам, упомянутым Робертом С. Мартином в его книге Чистый код.

Рекомендации для .NET и C#

Чтобы другие разработчики могли поддерживать ваш код, он должен быть простым для понимания. Вашей главной целью при написании кода всегда должна быть его читабельность. Даже одна непонятная строка кода может привести к пустой трате драгоценного времени другого разработчика, и это будет вашей ответственностью.

Какие факторы помогают сделать код более понятным для других?

В этой статье не рассматриваются приемы, описанные в книге Чистый код. Рекомендую изучить их самостоятельно. Вместо этого я сосредоточусь на конкретных рекомендациях, которые я усвоил благодаря своему 9-летнему опыту работы разработчиком .NET.

Эти предложения предназначены для того, чтобы сделать код максимально кратким и удобочитаемым. Они не заменяют принципов чистого кодирования, а скорее служат им дополнением.

Оглавление

Отступы и разрывы строк

Первое и наиболее действенное правило для получения более читаемого кода — это правильное использование отступов и разрывов строк. Visual Studio немного помогает вам с форматированием вашего кода, но ему еще многое предстоит улучшить. Если у вас есть опыт работы с Typescript с VSCode, вы можете знать расширение Prettier. Prettier отлично форматирует ваш код, сохраняет все открывающие и закрывающие скобки на одном уровне отступа и вставляет разрывы строк там, где они должны быть.

Аккуратным, но не идеальным решением является расширение CodeMaid для Visual Studio. В это расширение включена функция Форматировать при сохранении, которая уже требует от вас много работы.

Чтобы отформатировать ваш код после поддержки Visual Studio и CodeMaid, я обнаружил, что достаточно 4 конкретных эмпирических правил:

1. Уровень отступа открывающей и закрывающей скобки должен совпадать
Уровень отступа строки кода, где открывается ваша скобка, определяет уровень отступа закрывающей скобки.

enumerable.Select(e => 
  e.GetName(
    formatter: nameFormatter
  )
);

Как видите, все становится намного легче читать, так как каждая строка имеет только одно значение, и вы можете сразу увидеть, где заканчивается каждый раздел.

2. Длина одной строки кода не должна превышать половину экрана
Слишком длинные строки кода плохо читаются. Как вы видите в приведенном выше примере, его намного легче читать, когда только одна проблема получает одну строку. Также многие IDE поддерживают открытие двух документов рядом друг с другом. Когда у вас есть более длинные строки кода, они будут скрыты за следующим окном, и вам придется прокручивать их по горизонтали.

3. Вставляйте разрывы строк ПЕРЕД точкой или ПОСЛЕ запятой
Хорошее практическое правило для связанных методов заключается в том, что вы должны разрывать строку перед точкой. Таким образом, вы убедитесь, что следующая строка прямо подразумевает, что она представляет вызов другого метода или свойства. Еще одним преимуществом является то, что вы можете удалить всю строку, чтобы удалить этот метод из цепочки, и вам не нужно в конечном итоге также удалять точку на одну строку выше.

var element = enumerable
  .Where(e => Condition(e))
  .Select(e => e.Property)
  .FirstOrDefault();

var newElement = new Element(
  dependency1: dependency1,
  dependency2: dependency2
);

4. Каждая лямбда-функция получает новый уровень отступа
Когда вы часто работаете с LINQ, у вас в основном есть лямбда-функции в качестве аргументов для них. Сохранение уровня отступа для них неявно сделает их более читабельными, особенно если они длиннее.

enumerable
  .SelectMany(e => e.Items
    .Select(i => i.Name)
  );

Избегайте циклов с помощью LINQ

Хотите верьте, хотите нет, но вы можете заменить как минимум 90% ваших циклов запросами LINQ. Это происходит в гораздо более чистом и читаемом коде, поскольку вам не нужны одна или несколько дополнительных переменных, которые изменяются с каждой итерацией или что-то в этом роде. Вместо этого вы можете точно увидеть каждую связанную операцию одну за другой и просто понять, что возвращает запрос.

Взгляните на следующий пример:

var nameList = new List<string>();

foreach (user in users) {
  nameList.Add(user.Name);
}

return nameList;

С помощью запроса LINQ вы можете значительно сократить требуемый код до этого:

return users.Select(u => u.Name).ToList();

Как только вы поймете и сможете использовать запросы LINQ, я гарантирую вам, что ваш код станет намного более читабельным.

Обзор доступных методов расширения для LINQ можно найти здесь.

Извлечение методов и предоставление читаемых имен

Длинные и вложенные вызовы часто трудно читать. Большую часть времени второй разработчик или даже вы сами будете задаваться вопросом, что именно этот фрагмент кода должен здесь делать.

Чтобы избавиться от этой проблемы, просто извлеките и назовите метод так, чтобы было понятно, что здесь должно происходить. Помните: комментариев следует избегать. Если вы хотите использовать комментарий для описания происходящего, это хороший показатель того, что вы тоже можете провести этот рефакторинг.

//Bad
public List<string> Test(IEnumerable<string> someStrings)
    => someStrings
        .Where(s => s.StartsWith("a", StringComparison.OrdinalIgnoreCase))
        .Select(s => s.Replace("a", ""))
        .ToList();
 
//Good (Imagine a more complicated logic here)
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", "");

Если вы не знали, вы также можете использовать локальные методы, если вы не хотите выставлять конкретную логику в свой класс следующим образом:

public void Test(string input) {
  var str = GetFormattedStringInternal(input);
  // logic...
  string GetFormattedStringInternal(string s) {
    return s...;
  }
}

Именованные аргументы

Для второго разработчика, чтобы проанализировать, какие аргументы требуются для выполнения другого метода или конструктора, иногда бывает трудно увидеть на первый взгляд. Вы можете улучшить читаемость здесь, используя именованные аргументы.

var newElement = new Element(
  argument1: argument1,
  argument2: argument2,
  argument3: argument3
);

Даже если вы сейчас вставите более длинный вызов в качестве аргумента, второй разработчик будет сразу знать, что это должно быть, и сможет лучше понять, что там должно происходить. Конечно, имя аргумента должно быть хорошим, иначе оно вообще бесполезно.

Минимизируйте количество кода в классах

Как предполагает первый принцип SOLID, у класса должна быть только одна обязанность. Раздутый код внутри класса в большинстве случаев является хорошей подсказкой о том, что вам следует провести рефакторинг класса.

Если вам нужно расширить функциональность класса, вы можете сделать это по принципу открытого-закрытого через метод расширения.
Методы расширения добавляют функциональность к существующему классу, не затрагивая его исходную функциональность. Бонус в том, что вы можете реализовать расширение в другом проекте, где оно может быть помечено как internal или даже file. Так что только ваш проект имеет возможность использовать этот метод расширения и убедиться, что другие проекты не будут затронуты вашим расширением.

Используйте синтаксический сахар и новые возможности языка

Спорным, но, тем не менее, полезным правилом является использование так называемого синтаксического сахара там, где это возможно. Синтаксический сахар — это особенности языка, сокращающие определенные команды.

Разработчик, который не знаком с соответствующим синтаксисом, может сначала быть ошеломлен, но, по моему опыту, он будет искать синтаксис в Интернете и шаг за шагом реализовывать синтаксис в своем собственном стиле кодирования.

Мое правило: чем короче, тем лучше. Таким образом, каждый бит кода, который слишком длинный для чтения и может быть сокращен, должен быть сокращен.

Для справки, вот наиболее часто используемые языковые функции текущей версии языка C#:

Укороченная проверка null

//Checks if left value is null and if it is null, uses right value;
var a = null;
var b = a ?? new Xyz();

//Throws exception if value is null
var c = a ?? throw new Exception();

//If d is null, create new D();
var d = null;
d ??= new D();

Укороченные предложения If

//Bad
string s;
if (predicate) {
    s = "Hello World";
} else {
    s = "Bye World";
}

//Good
var s = predicate
    ? "Hello World"
    : "Bye World";

Интерполяция строк

var a = "Name"
var s = $"Hello {a}"
// s is "Hello Name"

Сопоставление с образцом

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

Записи

//Old
public class Xyz() {
    public string Test { get; }
    
    public string Test2 { get; }
    
    public Xyz(string test, string test2){
        Test = test;
        Test2 = test2
    }
}

//New
public record Xyz(string Test, string Test2);

Пространства имен файлов

//Old
namespace This.Is.A.Test.Namespace {
  public class Test {
  }
}

//New
namespace This.Is.A.Test.Namespace;
  
public class Test {
}

Глобальное и неявное использование

//Old
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace xyz;

class Abc {
}

//New
namespace xyz;

class Abc {
}

Вы можете быть в курсе последних возможностей языка здесь.

Избегайте цикломатической сложности

Термин цикломатическая сложность используется для описания сложности программы. Сложность в основном определяется количеством возможных путей, которые может пройти программа. Увеличение цикломатической сложности напрямую ведет к снижению тестируемости.

Во время кодирования вы создадите новый путь для каждого возможного пути, по которому может идти выполнение. Например. если вы реализуете предложение if, вы создадите 2 возможных новых пути для выполнения.

Среди других методов вы можете минимизировать цикломатическую сложность, избегая условно-условных выражений и используя интерфейсы для разделения логики:

interface IRequestHandler
{
    Result Handle();
}

internal class Test1 : IRequestHandler
{
    public Result Handle() {
        //Do something
    }
}

internal class Test2 : IRequestHandler
{
    public Result Handle() {
        //Do the other thing
    }
}

public Result HandleRequest(IRequestHandler requestHandler)
    => requestHandler.Handle();

Здесь коду не нужно выбирать два разных пути внутри HandleRequest. Неявно дано, что код различает две реализации, однако они не вызывают увеличения цикломатической сложности и остаются чрезвычайно хорошо тестируемыми.

В Visual Studio вы можете отобразить цикломатическую сложность своего решения, нажав Анализ/вычисление метрик кода. Помимо сложности, вы также получите представление о глубине наследования и связи классов. Все эти значения должны быть сведены к минимуму.

Разделите свои проекты в соответствии с рекомендациями по чистой архитектуре

Если вы не слышали о шаблоне чистой архитектуры Джейсона Тейлора, я настоятельно рекомендую вам ознакомиться с ним на его Github:



Хорошее представление принципа и шаблона можно найти здесь:

TL;DR

Основная идея этого пакета состоит в том, чтобы разделить ваши проекты в соответствии с луковичной архитектурой, первоначально предложенной Робом С. Мартином.
Вы должны создать проект для каждого из следующих слоев, упорядоченных от самого внутреннего к внешнему. слои:

  • Домен: этот уровень содержит все сущности, перечисления, исключения, интерфейсы, типы и логику, характерные для уровня домена.
  • Приложение: здесь вы разместите всю логику приложения. Его единственной зависимостью является слой Domain.
  • Инфраструктура. Здесь вы можете создать несколько проектов, каждый из которых содержит реализацию интерфейсов, объявленных на внутренних уровнях.
  • UI: здесь будут находиться ваши пользовательские интерфейсы. Этот уровень зависит от всех внутренних слоев, так как его задачей также является загрузка всего приложения.

Поддерживайте чистую структуру папок

Помимо структуры вашего проекта, вы также должны поддерживать чистую структуру папок внутри ваших проектов. Папки должны быть хорошо названы, а второй разработчик должен уметь догадываться, где какие классы находятся. Кроме того, если ему нужно добавить больше классов, он должен сразу же понять, где они находятся.

Вот соглашение об именах, которое я в основном придерживаюсь для своих папок:

Project
├── Abstractions // interface definitions
    ├──ServiceName
        ├──IServiceName.cs
        └──IServiceNameFactory.cs
    └──...
├── Attributes
├── Behaviors // Implementations of IPipelineBehavior<,>
├── Commands // Implementations of IRequest<> & IRequestHandler<,>
├── Constants
├── Enums
├── Events
├── Exceptions
├── Extensions
├── Services
    └──ServiceName
        ├──ServiceName.cs
        └──ServiceNameFactory.cs
├── Transformations
├── Utility // Utility classes, that do not belong to services
├── AssemblyAttributes.cs // Make internals visible to Test project
├── GlobalUsings.cs
├── ServiceCollectionExtensions.cs // IServiceCollection Extensions
├── Program.cs // Startup logic
├── settings.json
└──...

Конечно, большинство из этих папок не требуются везде, но для полноты картины я упомянул их все здесь.

Если у вас есть много классов, принадлежащих одной папке, настоятельно рекомендуется создать подпапки для классов, которые принадлежат друг другу, как я сделал с ServiceName.

Держите связанные классы вместе

Еще одна вещь, которую я могу настоятельно порекомендовать, это то, что вы проводите классы, которые принадлежат друг другу, рядом друг с другом. Хороший пример можно найти в шаблоне чистой архитектуры от Джейсона Тейлора (см. выше).

Там у него реализация IRequest И обработчик IRequestHandler в той же папке или даже в одном файле. Это упрощает поиск кода, который, вероятно, будет изменен вместе.

Заключение

Это руководство содержит, на мой взгляд, наиболее важные принципы, которые, если вы включите их в свой стиль кодирования, могут значительно улучшить читабельность вашего кода. Усвоив и адаптировав эти рекомендации, вы можете сделать свой код более понятным, удобным в сопровождении и легко модифицируемым.

Следуя этим принципам, вы не только получите личную выгоду, но и ваши коллеги и будущие разработчики, которым может понадобиться работать с вашим кодом, также будут вам благодарны. Принятие передовых методов кодирования может способствовать созданию совместной рабочей среды и свести к минимуму вероятность ошибок и неэффективности.

Помните, что программирование — это не только написание инструкций, которые могут выполнять компьютеры. Речь также идет о создании всеобъемлющего и четкого средства коммуникации, которое может передать ваши намерения и позволить другим понять ход ваших мыслей. Отдавая приоритет удобочитаемости и ремонтопригодности, вы можете сделать свой код более доступным и удобным для пользователя.

Спасибо, что нашли время прочитать эту статью. Надеюсь, вы нашли его информативным, познавательным и интересным.
Мы очень ценим вашу поддержку и участие.

Если вы заинтересованы в том, чтобы быть в курсе последних тенденций, советов и приемов для чистой архитектуры, чистого кодирования и новейших технологических стеков, особенно в контексте C #, .NET и Angular, я был бы признателен. если вы решили следовать за мной.

Удачного дня!

Если вы еще не используете Medium для ежедневного расширения своих знаний, сейчас самое время начать! С Medium вы можете легко получить больше знаний по высокопрофессиональным темам, публиковать качественный контент и охватить более широкую аудиторию. Чтобы начать, просто создайте учетную запись Medium, используя эту ссылку:

Присоединиться к Medium сейчас

Таким образом, вы получите доступ к мощной платформе, которая поможет вам общаться с новыми писателями и читателями и каждый день узнавать что-то новое.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу