Не можете вникнуть в алгоритм?
Проводите кучу время в отладке, но найти место неверной инициализации не получается, а хочется получать удовольствие от кодирования?
Вспомните о приведенных ниже правилах и примените их!
В статье не рассматриваются базовые правила именования переменных и функций, синтаксические отступы и масштабная тема рефакторинга. Рассматриваются 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)
ivanych
15.01.2019 22:30+1Первое "правило" вы неправильно излагаете.
Не следует использовать отрицательные формы условных операторов, типа unless:
unless( 2*2 == 4 ) { print "наступил конец света" }
а вот сами отрицательные условия можно и нужно использовать. Пример выше следует переписать так:
if( 2*2 != 4 ) { print "наступил конец света" }
herodream Автор
16.01.2019 00:38Если есть условие вида:
if( 2*2 != 4 ) { print "наступил конец света" } else { print "наступил мир во всем мире" }
То лучше поменять ветки местами:
if( 2*2 == 4 ) { print "наступил мир во всем мире" } else { print "наступил конец света" }
ivanych
16.01.2019 01:01+1Вы приводите плохой пример, с двумя вариантами выбора. В таком примере конечно лучше поменять местами, но дело в том, что такой пример слишком узкий и слишком подогнан под вашу формулировку "правила". Возьмите пример без второго варианта выбора. Тут как-раз перекликается со вторым "правилом" — типа, если конец света, то сразу выйти из функции.
c0f04
16.01.2019 19:17На самом деле не всегда такое правило применимо. Я пришёл ровно к таким же принципам, что Вы изложили, собственно, удивился увидев стиль программирования один в один как у себя, только на другом языке.
Первым в операторе условия должен идти не истинный блок, а логически ожидаемый. Например, у нас в Си, если произошла ошибка, мы должны сначала явно обработать ошибку:
char *str = malloc(str_size); if (!str) { // ... сохранение errno, если требуется perror("malloc"); // ... } else { // в невероятно крайне редких случаях этот блок может присутствовать }
Но в то же время для условных строк условие будет обратным:
if (str) { // работа с str } else { // какая-либо другая логика }
Пример плохой, но это первое, что пришло в голову.
worldmind
16.01.2019 09:29-3Примеры кода с индексатором не стал читать — слишком сложные на первый взгляд, да и в нормальных языках индексаторы давно не используются.
wxmaper
16.01.2019 13:42Стойкое ощущение дежавю.
Где-то я уже читал всё это, при чем на русском языке, слово в слово. Вырезка из книги? Или из другой статьи на хабре?herodream Автор
16.01.2019 23:08Авторское, но все хорошее изобретено до нас. Друзья сказали что подобные правила есть в «Совершенном» или в «Чистом» коде. Возможно видели там.
Crimso
16.01.2019 17:33Никогда не понимал Правила 2. Когда нормальные вложенные условия, нормально выделенные отступами, то видна вся логика процедуры, видно при каких условиях куда попадет программа. А если в тексте рандомно рассыпаны return'ы, то тщательно выискивать их по всему тексту становится неудобно.
wxmaper
16.01.2019 18:51Чем больше уровень вложенности, тем меньше остаётся свободного места для кода.
Читать длинный участак кода (например, большую формулу), разбитый в несколько строк не очень-то просто.
almunt
16.01.2019 17:33Стараюсь избегать возврата управления через return (2). Мне кажется, что такой вот выход из условного оператора где-то в середине метода, может быть не очевидным при беглом чтении кодом. Поэтому стараюсь return делать только в конце метода.
herodream Автор
16.01.2019 17:37-1Решения имеют свои + и -. Это не догма. Если цель упростить за счет снижения уровня вложенности — return помогает. Если нет условий, которые можно явно отделить, лучше один return.
rjhdby
17.01.2019 10:52Возврат управления через return хорошо работает для пред-проверок, т.е. в самом начале метода.
С другой стороны, если возникает ситуация, когда необходимо вернуть управление из середины метода, то, с большой долей вероятности, либо с методом что-то не так и нужна декомпозиция, либо, на самом деле, нужен не return, а throw.
SbWereWolf
16.01.2019 22:56Холливарная статья. Направление мысли конечно правильное, с большинством предложений согласен и всегда на код ревью требую соблюдения.
Но я сильно не согласен с рандомными return`ами в коде, возврат должен быть один, в остальных местах надо поднимать флаг и на каждом этапе алгоритма его анализировать.
Я всю жизнь работаю с легаси, и что бы понять как оно устроено живу с дебагером в обнимку, и меня очень очень вымораживает когда я поставлю брейк поинт до интересного места, а отладчик до него не доходит потому что где то был спрятан return.
В знакомом коде нет проблем с return`ами, но мы же код не для себя пишем? его другие люди читать будут, им всё должно быть понятно, для них поведение должно быть предсказуемо, логично.
По поводу if-then-else, на практике пришёл к тому что писать
var = expression(a,b,c);
if(var) method1();
if(!var)method2();
сильно удобней и ветки местами менять, и от веток отказываться, вот это всё удобней, и более модульно код смотрится.
Про код в котором строчки сгруппированы правильно сказано — такой код глотаешь, жевать его не приходиться. И опять же шаги алгоритма местами менять удобней.rjhdby
17.01.2019 10:56в остальных местах надо поднимать флаг и на каждом этапе алгоритма его анализировать.
Вот уж воистину лекарство хуже болезни.
Разбросанные по методу return'ы — это конечно зло в разрезе анализа потока выполнения, но флаги — это зло в квадрате.SbWereWolf
17.01.2019 14:15Я когда первый раз увидел тоже ругался на индусов, но потом (через пару лет) сам к этому пришёл. Флаг же обычно один и код метода не такой длинный что бы сбиться со счёта.
Кто к чему привык. Для меня множественные ретурны — это предельная степень ада.rjhdby
17.01.2019 14:31Вы сравниваете разные вещи. Флаг у вас один, а ретурны, почему-то, множественные.
Там, где достаточно одного флага — там достаточно и одного возврата. К тому же, оператор возврата однозначен — т.е. если он появился в коде, то это однозначное и единственное место возврата управления. В случае с флагом необходимо держать его в уме при анализе всего оставшегося кода, гадая, где же вы его могли еще проглядеть.
pprometey
17.01.2019 05:40Уже тут писали. В условиях, проверка должна идти первую очередь на наиболее вероятностный вариант. И цепочку условий строить так, чтобы в конце был менее вероятностный. Это влияет на быстродействие.
rjhdby
17.01.2019 10:58И сильно
JNZ
проигрывает в производительностиJZ
?pprometey
17.01.2019 19:17Некорректно так сравнивать. Из разряда «с какого конца я съем быстрее бутерброд?»
SbWereWolf
18.01.2019 07:30Вам намекают что овчинка не стоит выделки. Вероятность того или иного варианта зависит от использования кода, от пользовательских привычек, от характера данных, со временем это меняется.
Не надо тратить время на преждевременную оптимизацию.
tFirma