Ускользает понимание своего или чужого кода?

Не можете вникнуть в алгоритм?

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

Вспомните о приведенных ниже правилах и примените их!

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

Рассмотрим процесс восприятия данных, чтобы соотнести описанные правила с процессом восприятия и определить критерии простого кода.

Упрощенный процесс восприятия состоит из следующих этапов:

  1. Поступающая через рецепторы данные соотносятся с предыдущим опытом.
  2. Если соотнесения нет – это шум. Шум быстро забывается. Если есть с чем соотнести, происходит опознавание фактов.
  3. Если факт важен — запоминаем, либо обобщаем, либо действуем, например говорим или набираем код.
  4. Для сокращения объема запоминаемой и анализируемой информации используется обобщение.
  5. После обобщения, информация вновь соотносится и анализируется (этап 1).


Ученые разделяют память на кратковременную и долгосрочную. Кратковременная память мала по объему, но извлечение и сохранение информации выполняет мгновенно. Кратковременная память — кэш мозга. В нем может храниться 7+-2 слов, цифр, предметов. Долгосрочная память больше по объему, но она требует больших затрат энергии (усилий) на сохранение и извлечение информации, чем краткосрочная.

Выводы:

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

Приступим к описанию правил.

Правило 1. Используем в условиях утверждение, избавляемся от «не».

Удаление оператора «не» уменьшает количество анализируемых элементов. Также, во многих языках программирования для оператора отрицания используется «!». Данный знак легко пропустить при прочтении кода.

Сравните:

if (!entity.IsImportAvaible)
{
      //код 1
}
else
{
      //код 2
}

После:

if (entity.IsImportAvaible)
{ 
      //код 2
}
else
{
      //код 1
}

Правило 2. Уменьшение уровня вложенности.

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

Рассмотрим способы уменьшения количества уровней вложенности.

1) Возврат управления. Отсекаем часть случаев и сосредотачиваемся на оставшихся.

if (conributor != null)
{
      //код
}

Преобразуем в

if(contributor == null)
{
       return;
}
//код

или

while(условие 1)
{
       if(условие 2)
       {
              break;
       }
       //код
}

или

for(;условие 1;)
{
       if(условие 2)
       {
              continue;
       }
       //код
}

Расширенный пример приведен в правиле 5. Обратите внимание на выброс исключения.

2) Выделение метода. Имя функции — результат обобщения.

if(условие рассылки)
{
      foreach(var sender in senders)
      {
            sender.Send(message);
      }
}

в

if(условие рассылки)
{
      SendAll(senders, message);
}
 
void SendAll(IEnumerable<Sender> senders, string message)
{
      foreach(var sender in senders)
      {
            sender.Send(message);
      }
}

3) Объединение условий.

if (contributor == null)
{
      if (accessMngr == null)
      {
            //код
      }
}

в

if (contributor == null
    && accessMngr == null)
{
       //код
}

4) Вынесение переменных и разделение на смысловые блоки. В результате блоки можно изменять независимо, а главное читать и воспринимать такой код гораздо проще. Один блок – одна функциональность. Частый случай — поиск с обработкой. Необходимо разбить код на отдельные смысловые блоки: поиск элемента, обработка.

for (int i=0;i<array.length;i++)
{
      if (array[i] == findItem)
      {
            //обработка array[i]
                           	
            break;
      }
}

в

for(int i=0;i<array.length;i++)
{
      if(array[i] == findItem)
      {
            foundItem =array[i];
 
            break;
      }
}
 
if (foundItem != null)
{
      //обработка foundItem
}

Правило 3. Избавляемся от индексаторов и обращений через свойства.

Индексатор — операция обращения к элементу массива по индексу arr[index].

В процессе восприятия кода, мозг оперирует символами [] как разделителями, а индексатором как выражением.

function updateActiveColumnsSetting(fieldsGroups) {
 
      var result = {};
 
      for (var i = 0; i < fieldsGroups.length; i++) {
            var fields = fieldsGroups[i].fields;
 
            for (var j = 0; j < fields.length; j++) {
                  if (!result[fieldsGroups[i].groupName]) {
                  result[fieldsGroups[j].groupName] = {};
            }
 
            result[fieldsGroups[i].groupName][fields[j].field] 
                = createColumnAttributes(j, fields[j].isActive);
            }
      }
 
      return JSON.stringify(result);
}

Индексатор — место частых ошибок. В силу кратких имен индексов перепутать I, j или k очень легко.

В приведенном выше примере в строчке result[fieldsGroups[j].groupName] = {}; допущена ошибка:
используется j, вместо i.

Для того, чтоб обнаружить где используется именно i-тое значение приходится:

1) визуально выделять переменную массива

function updateActiveColumnsSetting(fieldsGroups) {
 
      var result = {};
 
      for (var i = 0; i < fieldsGroups.length; i++) {
            var fields = fieldsGroups[i].fields;
 
            for (var j = 0; j < fields.length; j++) {
                  if (!result[fieldsGroups[i].groupName]) {
                  result[fieldsGroups[j].groupName] = {};
            }
 
            result[fieldsGroups[i].groupName][fields[j].field] 
                = createColumnAttributes(j, fields[j].isActive);
            }
      }
 
      return JSON.stringify(result);
}

2) анализировать каждое вхождение на использование нужного индексатора I,j, i-1, j-1 и т.д., держа в восприятии места использования индексаторов и уже отождествленные обращения.
Выделив индексатор в переменную, сократим количество опасных мест и сможем легко задействовать мозг на восприятие переменной, без необходимости запоминания.

После переработки:

function updateActiveColumnsSetting(fieldsGroups) {
 
      var columnsGroups = {};
 
      for (var i = 0; i < fieldsGroups.length; i++) {
 
            var fieldsGroup = fieldsGroups[i];
 
            var groupName = fieldsGroup.groupName;
            var columnsGroup = columnsGroups[groupName];
 
            if (!columnsGroup) {
                  columnsGroup = columnsGroups[groupName] = {};
            }
 
            var fields = fieldsGroup.fields;
            for (var j = 0; j < fields.length; j++) {
 
            var fieldInfo = fields[j];
 
            columnsGroup[fieldInfo.field] 
                = createColumnAttributes(j, field.isActive);
            }
      }
 
      return columnsGroups;
}

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

function updateActiveColumnsSetting(fieldsGroups) {
 
      var columnsGroups = {};
 
      for (var i = 0; i < fieldsGroups.length; i++) {
 
            var fieldsGroup = fieldsGroups[i];
 
            var groupName = fieldsGroup.groupName;
            var columnsGroup = columnsGroups[groupName];
 
            if (!columnsGroup) {
                  columnsGroup = columnsGroups[groupName] = {};
            }
 
            var fields = fieldsGroup.fields;
            for (var j = 0; j < fields.length; j++) {
 
                  var fieldInfo = fields[j];
 
                  columnsGroup[fieldInfo.field] 
                      = createColumnAttributes(j, field.isActive);
            }
      }
 
      return columnsGroups;
}

Правило 4. Группируем блоки по смыслу.

Используем психологический эффект восприятия — «Эффект близости»: близко расположенные фигуры при восприятии объединяются. Получить код, подготовленный для анализа и обобщения в процессе восприятия, и сократить количество информации, сохраняемой в памяти, можно расположив рядом строки, объединенные смыслом или близкие по функционалу, разделив их пустой строкой.

До:

foreach(var abcFactInfo in abcFactInfos)
{
      var currentFact = abcInfoManager.GetFact(abcFactInfo);
 
      var percentage = GetPercentage(summaryFact, currentFact);
 
      abcInfoManager.SetPercentage(abcFactInfo, percentage);
 
      accumPercentage += percentage;
 
      abcInfoManager.SetAccumulatedPercentage(abcFactInfo, accumPercentage);
 
      var category = GetAbcCategory(accumPercentage, categoryDictionary);
 
      abcInfoManager.SetCategory(abcFactInfo, category);
}

После:

foreach (var abcFactInfo in abcFactInfos)
{
      var currentFact = abcInfoManager.GetFact (abcFactInfo);
      var percentage = GetPercentage(summaryFact, currentFact);
 
      accumPercentage += percentage;
      var category = GetAbcCategory(accumPercentage, categoryDictionary);
 
      abcInfoManager.SetPercentage(abcFactInfo, percentage);
      abcInfoManager.SetAccumulatedPercentage(abcFactInfo, accumPercentage);
      abcInfoManager.SetCategory(abcFactInfo, category);
}

В верхнем примере 7 блоков, в нижнем 3: получение значений, накопление в цикле, установка свойств менеджера.

Отступами хорошо выделять места, на которые стоит обратить внимание. Так, строки

accumPercentage += percentage;
var category = GetAbcCategory(accumPercentage, categoryDictionary);

помимо зависимости от предыдущих вычислений, накапливают значения в переменной accumulatedPercentage. Для акцентирования внимания на отличии, код выделен отступом.

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

Правило 5. Следование принципу единственности ответственности.

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

В заключении комплексный пример:

private PartnerState GetPartnerStateForUpdate(
            PartnerActivityInfo partner,
            Dictionary<int, PartnerState> currentStates,
            Dictionary<int, PartnerState> prevStates)
{
      PartnerState updatingState;
 
      if (prevStates.ContainsKey(partner.id))
      {
            if (currentStates.ContainsKey(partner.id))
            {
                  var prevState = prevStates[partner.id];
 
                  updatingState = currentStates[partner.id];
 
                  //Код 1
            }

            else
            {
                  //Код 2
            }
      }

      else if (currentStates.ContainsKey(partner.id))
      {
            updatingState = currentStates[partner.id];
      }

      else
      {
             throw new Exception(string.Format("Для партнера {0} не найдено текущее и предыдущее состояние на месяц {1}", partner.id, month));
      }
 
      return updatingState;
}

После замены индексаторов ContainsKeу, инвертирования ветвления, выделения метода и уменьшения уровней вложенности получилось:

private PartnerState GetPartnerStateForUpdate(
            PartnerActivityInfo partner,
            Dictionary<int, PartnerState> currentStates,
            Dictionary<int, PartnerState> prevStates)
{
      PartnerState currentState = null;
      PartnerState prevState = null;

      prevStates.TryGetValue(partner.id, out prevState);
      currentStates.TryGetValue(partner.id, out currentState);

      currentState = CombineStates(currentState, prevState);

      return currentState;
}

private PartnerState CombineStates(
            PartnerState currentState,
            PartnerState prevState)
{
      if (currentState == null
          && prevState == null)
      {
            throw new Exception(string.Format(
                    "Для партнера {0} не найдено текущее и предыдущее состояние на месяц {1}" , partner.id, month));
      }


      if (currentState == null)
      {
            //Код 1
      }
      
      else if (prevState != null)
      {
            //Код 
      }

      return currentState;
}

Первая функция отвечает за получение состояний из словарей, вторая за их комбинирование в новое.

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

Комментарии (25)


  1. tFirma
    15.01.2019 15:50

    5 простых правил удобного для восприятия кода
    От меня ускользает понимание заголовка — плохое начало.


  1. ivanych
    15.01.2019 22:30
    +1

    Первое "правило" вы неправильно излагаете.


    Не следует использовать отрицательные формы условных операторов, типа unless:


    unless( 2*2 == 4 ) {
      print "наступил конец света"
    }

    а вот сами отрицательные условия можно и нужно использовать. Пример выше следует переписать так:


    if( 2*2 != 4 ) {
      print "наступил конец света"
    }


    1. herodream Автор
      16.01.2019 00:38

      Если есть условие вида:

      if( 2*2 != 4 )
      {
          print "наступил конец света"
      }
      else
      {
          print "наступил мир во всем мире"
      }
      


      То лучше поменять ветки местами:
      if( 2*2 == 4 )
      {
          print "наступил мир во всем мире"
      }
      else
      {
          print "наступил конец света"
      }
      


      1. ivanych
        16.01.2019 01:01
        +1

        Вы приводите плохой пример, с двумя вариантами выбора. В таком примере конечно лучше поменять местами, но дело в том, что такой пример слишком узкий и слишком подогнан под вашу формулировку "правила". Возьмите пример без второго варианта выбора. Тут как-раз перекликается со вторым "правилом" — типа, если конец света, то сразу выйти из функции.


        1. herodream Автор
          16.01.2019 01:13

          Если вы про второе правило, то в коде должен присутствоват возврат управления, например return.


          1. ivanych
            16.01.2019 01:30

            Да, вместо print — return. А ещё лучше die.


      1. c0f04
        16.01.2019 19:17

        На самом деле не всегда такое правило применимо. Я пришёл ровно к таким же принципам, что Вы изложили, собственно, удивился увидев стиль программирования один в один как у себя, только на другом языке.

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

        char *str = malloc(str_size);
        if (!str) {
            // ... сохранение errno, если требуется
            perror("malloc");
            // ...
        } else {
            // в невероятно крайне редких случаях этот блок может присутствовать
        }
        

        Но в то же время для условных строк условие будет обратным:
        if (str) {
            // работа с str
        } else {
            // какая-либо другая логика
        }
        


        Пример плохой, но это первое, что пришло в голову.


    1. rjhdby
      16.01.2019 11:01

      Это двойное отрицание. Оно в два раза хуже, чем обычное. :)


  1. worldmind
    16.01.2019 09:29
    -3

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


  1. wxmaper
    16.01.2019 13:42

    Стойкое ощущение дежавю.
    Где-то я уже читал всё это, при чем на русском языке, слово в слово. Вырезка из книги? Или из другой статьи на хабре?


    1. herodream Автор
      16.01.2019 23:08

      Авторское, но все хорошее изобретено до нас. Друзья сказали что подобные правила есть в «Совершенном» или в «Чистом» коде. Возможно видели там.


  1. Crimso
    16.01.2019 17:33

    Никогда не понимал Правила 2. Когда нормальные вложенные условия, нормально выделенные отступами, то видна вся логика процедуры, видно при каких условиях куда попадет программа. А если в тексте рандомно рассыпаны return'ы, то тщательно выискивать их по всему тексту становится неудобно.


    1. wxmaper
      16.01.2019 18:51

      Чем больше уровень вложенности, тем меньше остаётся свободного места для кода.
      Читать длинный участак кода (например, большую формулу), разбитый в несколько строк не очень-то просто.


  1. almunt
    16.01.2019 17:33

    Стараюсь избегать возврата управления через return (2). Мне кажется, что такой вот выход из условного оператора где-то в середине метода, может быть не очевидным при беглом чтении кодом. Поэтому стараюсь return делать только в конце метода.


    1. herodream Автор
      16.01.2019 17:37
      -1

      Решения имеют свои + и -. Это не догма. Если цель упростить за счет снижения уровня вложенности — return помогает. Если нет условий, которые можно явно отделить, лучше один return.


    1. rjhdby
      17.01.2019 10:52

      Возврат управления через return хорошо работает для пред-проверок, т.е. в самом начале метода.
      С другой стороны, если возникает ситуация, когда необходимо вернуть управление из середины метода, то, с большой долей вероятности, либо с методом что-то не так и нужна декомпозиция, либо, на самом деле, нужен не return, а throw.


    1. SbWereWolf
      17.01.2019 16:57

      +1, именно.


  1. SbWereWolf
    16.01.2019 22:56

    Холливарная статья. Направление мысли конечно правильное, с большинством предложений согласен и всегда на код ревью требую соблюдения.
    Но я сильно не согласен с рандомными return`ами в коде, возврат должен быть один, в остальных местах надо поднимать флаг и на каждом этапе алгоритма его анализировать.
    Я всю жизнь работаю с легаси, и что бы понять как оно устроено живу с дебагером в обнимку, и меня очень очень вымораживает когда я поставлю брейк поинт до интересного места, а отладчик до него не доходит потому что где то был спрятан return.
    В знакомом коде нет проблем с return`ами, но мы же код не для себя пишем? его другие люди читать будут, им всё должно быть понятно, для них поведение должно быть предсказуемо, логично.
    По поводу if-then-else, на практике пришёл к тому что писать
    var = expression(a,b,c);
    if(var) method1();
    if(!var)method2();

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


    1. rjhdby
      17.01.2019 10:56

      в остальных местах надо поднимать флаг и на каждом этапе алгоритма его анализировать.
      Вот уж воистину лекарство хуже болезни.
      Разбросанные по методу return'ы — это конечно зло в разрезе анализа потока выполнения, но флаги — это зло в квадрате.


      1. SbWereWolf
        17.01.2019 14:15

        Я когда первый раз увидел тоже ругался на индусов, но потом (через пару лет) сам к этому пришёл. Флаг же обычно один и код метода не такой длинный что бы сбиться со счёта.
        Кто к чему привык. Для меня множественные ретурны — это предельная степень ада.


        1. rjhdby
          17.01.2019 14:31

          Вы сравниваете разные вещи. Флаг у вас один, а ретурны, почему-то, множественные.
          Там, где достаточно одного флага — там достаточно и одного возврата. К тому же, оператор возврата однозначен — т.е. если он появился в коде, то это однозначное и единственное место возврата управления. В случае с флагом необходимо держать его в уме при анализе всего оставшегося кода, гадая, где же вы его могли еще проглядеть.


  1. pprometey
    17.01.2019 05:40

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


    1. rjhdby
      17.01.2019 10:58

      И сильно JNZ проигрывает в производительности JZ?


      1. pprometey
        17.01.2019 19:17

        Некорректно так сравнивать. Из разряда «с какого конца я съем быстрее бутерброд?»


        1. SbWereWolf
          18.01.2019 07:30

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

          Не надо тратить время на преждевременную оптимизацию.