WSD logo Понимание как основа устойчивости к ошибкам

на примерах из жизни

Красная Шапочка Красной Шапочки

и пирожочков Пирожок Пирожок

В главных ролях

Докладчик
Докладчик
Мария Гришкевич
из Exadel
Красная Шапочка
Красная шапочка
Программист Петя
Разработчик Петя
Бабушка
Бабушка
Волк Волк Волк
Стая волков (голодных)
Хинкаль
Случайный
Хинкаль
Пирожок
Пирожочек
Чебурек
Чебурек
(а то пирожочки надоели!)

Как улучшить код?

  • Рефакторить
  • Оптимизировать
  • Добавить обработку ошибок
  • Написать тесты
  • Декомпозиция
  • Написать уже документацию

Ожидание Реальность

  • Количество
    кода
  • Сложность
  • Устойчивость
    к ошибкам
Ракета с Красной Шапочкой

Процесс решения задачи

Мир

Представление

мир Красной Шапочки

задача: доставить бабушке пирожок

Красная шапочка Пирожок Бабушка Волк

мозг программиста Пети

задача: что-то кому-то доставить

Красная шапочка Хинкаль Бабушка Волк
Разработчик Петя

Объект (условный программист Петя)

Решение задач в команде

Мое представление

Представление Пети

Есть ли разница между
пирожком и чебуреком?

Красная шапочка Чебурек Бабушка Волк в костюмчике

Вот бы пельмешек съесть...

Красная шапочка Хинкаль Бабушка Злой Волк Злой Волк
Докладчик
Коллега Петя

Оба представления содержат ошибки, что не мешает коду работать

Когнитивная модель

субъективное представление о том, как что-то в реальном мире работает

код — формальное описание такой модели

Изменения

Изменения

Изменения

Изменения

Иcкажение

Исправление ошибок

Добавление ошибок

Иcкажение

Сложность

Сложность

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

Задачи

  • Как измерить сложность?
  • Как минимизировать сложность при написании кода?

Формальные характеристики сложности

  • Количество строк кода: SLOC (LOC) и LLOC
  • Глубина вложенности
  • Цикломатическая сложность
  • Когнитивная сложность
  • Метрики Холстеда
  • Комплексный показатель качества

Цикломатическая сложность (Маккейб, 1976)

Число линейно независимых маршрутов выполнения
                  function packPies(redRidingHood, packA, packB, pies) {
                    const parcel = [];/*1*/
                    for (/*2*/let i = 0; /*3*/i < pies.length;/*4*/i++) {
                      let pie = null;/*5*/
                      switch (pies[i].type) {/*6*/
                        case "HINKAL"/*7*/: pie = packA(pies[i]); break;/*8*/
                        case "CHEBUREK"/*9*/:
                        case "PIE"/*10*/: pie = packB(pies[i]); break;/*11*/
                      }
                      if (pie /*12*/) { parcel.push(pie);/*13*/ }
                    }
                    if (parcel.length /*14*/) {
                      redRidingHood.carry(parcel);/*15*/
                    }
                  }
                

Цикломатическая сложность

1
1
2
3
2
5
6
7
3
4
9
10
5
8
12
11
13
6
4
14
15
7
  • P = 1 компоненты связности
  • E = 22 ребра
  • P = 17 узлы
= N regions
= E − N + 2P
= 22 − 17 + 2
= 7
Граф
потока управления

Цикломатическая сложность

G = π + 1
π — число следующих конструкций
  • if
  • for
  • while
  • case
  • ?
  • &&
  • ||

Цикломатическая сложность, < 10 на функцию

G = π + 1 = 7
            function packPies(redRidingHood, packA, packB, pies) { // 1
              const parcel = [];                                   // 0
              for (let i = 0; i < pies.length; i++) {              // 1
                let pie = null;                                    // 0
                switch (pies[i].type) {                            // 0
                  case "HINKAL": pie = packA(pies[i]); break;        // 1
                  case "CHEBUREK":                                     // 1
                  case "PIE": pie = packB(pies[i]); break;        // 1
                }                                                  // 0
                if (pie) { parcel.push(pie); }                     // 1
              }                                                    // 0
              if (parcel.length) {                                 // 1
                redRidingHood.carry(parcel);                       // 0
              }                                                    // 0
            }                                                      // 0
          

Когнитивная сложность, Энн Кэмпбелл, 2018

Строится на цикломатической сложности,
но учитывает глубину вложенности
  • По умолчанию для функции — ноль
  • Выражения, которые можно объединить, считаем за единицу
                          a || b || c
                          a && b && c
                          switch + case + default
                          try + catch + finally
                        
  • К каждой конструкции прибавляем единицу и уровень вложенности
                        if for while switch ? && || catch
                      
  • Добавляем единицу, если нарушается линейная последовательность

Когнитивная сложность

C = 6
                  function packPies(redRidingHood, packA, packB, pies) { // 0
                    const parcel = [];                                   // 0
                    for (let i = 0; i < pies.length; i++) {              // 1
                      let pie = null;                                    // 0
                      switch (pies[i].type) {                            // 1 + 1
                        case "HINKAL": pie = packA(pies[i]); break;        // 0
                        case "CHEBUREK":                                     // 0
                        case "PIE": pie = packB(pies[i]); break;        // 0
                      }                                                  // 0
                      if (pie) { parcel.push(pie); }                     // 1 + 1
                    }                                                    // 0
                    if (parcel.length) {                                 // 1
                      redRidingHood.carry(parcel);                       // 0
                    }                                                    // 0
                  }                                                      // 0
                

Метрики Холстеда

Операторы

служебные слова и операторы, например

              ! != === !=== && || * *= 
              + += - -= / /= % 
              () {} [] let const var
              for if while catch case
            
Операнды

идентификаторы, константы, типы, свойства объектов

              1 'hello' true testName length
              null undefined
            
n1 — количество уникальных операторов
n2 — количество уникальных операндов
N1 — общее количество операторов
N2 — общее количество операндов

Метрики Холстеда

              function packPies(redRidingHood, packA, packB, pies) {
                const parcel = [];
                for (let i = 0; i < pies.length; i++) {
                  let pie = null;
                  switch (pies[i].type) {
                    case "HINKAL": pie = packA(pies[i]); break;
                    case "CHEBUREK":
                    case "PIE": pie = packB(pies[i]); break;
                  }
                  if (pie) { parcel.push(pie); }
                }
                if (parcel.length) {
                  redRidingHood.carry(parcel);
                }
              }
            

Метрики Холстеда

оператор N оператор N операнд N операнд N
const 1 . 5 parcel 4 Хинкаль 1
= 5 ++ 1 i 6 Чебурек 1
[] 3 switch 1 pies 4 Пирожок 1
; 10 case 3 length 2 packA 1
for 1 break 2 null 1 packB 1
{} 4 () 6 type 1 push 1
let 2 if 1 pie 5 redRidingHood 1
< 1 0 1 carry 1
n1=15
N1=46
n2=16
N2=32

Метрики Холстеда, 20 < V < 1000 на функцию

Уникальные операторы
n1 = 15
Операторы
N1 = 46
Уникальные операнды
n2 = 16
Операнды
N2 = 32
Длина программы N = N1 + N2 = 46 + 32 = 78
Размер словаря n = n1 + n2 = 15 + 16 = 31
Объем программы V = N * log2n = 386,43
Сложность D = (n1 * N2) / (2 * n2) = 15
Усилия программиста E = V * D = 5796,43
Число багов B = E2/3/3000 = 0,107

Комплексный
показатель качества , 20 < MI% < 100

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

MI = 171 - ln(V) - 0.23G - 16.2ln(LOC) =
= 171 - ln(386,43) - 0.23*7 - 16.2 * ln(15) = 119.56
MI% = MAX(0, MI * 100/171) = 69.92

И что с этим делать? Использовать инструменты!

Eslint

Tslint

Инструменты для получения аналитики.

Эмпирические характеристики сложности

Динамизм
Часы
Пирожок Пирожок Чебурек Чебурек Хинкаль
Множество частей (переменных)
и связи между ними
Бабушка Пирожок Чебурек Хинкаль Чебурек Хинкаль Хинкаль Чебурек Хинкаль Пирожок Волк Петя
Неопределенность
Пирожок Хинкаль Чебурек Морковка
Риск
Пирожок Волк Испуганный Чебурек Испуганный Хинкаль

Динамизм

Решение задачи строится на событиях. События происходят в произвольное время.

Проблема

Если волк съел пирожок, попытаться еще N раз

                phone
                 .addEventListener("callFromGrandma", makePie);
                function makePie(event, counter = 0) {
                  bakePie(event)
                   .then(sendPie)
                   .then(grandmaGetsPie)
                   .catch(() => eatenByWolf(event, counter++));
                }
                function eatenByWolf(event, counter) {
                  if (counter < N) {
                    return makePie(event, counter);
                  } else {
                    return excuse();
                  }
                }
            

Динамизм

Решение задачи строится на событиях. События происходят в произвольное время.

Проблема

Если волк съел пирожок, попытаться еще N раз

Рецепт: реактивное программирование

Работа с потоками событий в декларативном стиле.

                fromEvent("callFromGrandma", phone)
                .pipe(
                  concatMap(event => of(event).pipe(
                    concatMap(bakePies),
                    concatMap(sendPies),
                    retry(N)
                  )),
                  catchError(excuse)
                )
                .subscribe(grandma);
              

Множество частей и связи между ними

Как сделать так, чтобы код быстрее читался?

Проблема

Что делает эта функция? Можно ли понять, что происходит, не читая каждое условие?
              function eatenByWolf(event, counter) {
                if (counter < N) {
                  return makePie(event, counter);
                } else {
                  return excuse();
                }
              }
          
Чебурек Волк в костюмчике

Множество частей и связи между ними

Как сделать так, чтобы код быстрее читался?

Проблема

Что делает эта функция? Можно ли понять, что происходит, не читая каждое условие?

Рецепт: линия видимости ( line of sight )

  • "Счастливый путь" — по левому краю
  • Самый "успешный" return — последний
  • В случаи неудачи — завершайте
  • Сложную логику — выносите
  • Нет return-ам внутри else
  • Инвертируйте условия для выхода
                function eatenByWolf(event, counter) {
                  if (counter > N) {
                    return excuse();
                  } 
                  return makePie(event, counter);
                }
            

Множество частей и связи между ними

Проблема

В коде есть "сквозные" (pass-through) методы
                class PieFactory {
                  constructor() {
                   this.maker = new RedRidingHood();
                  }
                  bakePie(pie) { this.maker.bakePie(pie); }
                  sendPie(pie) { this.maker.sendPie(pie); }
                }
                
                const factory = new PieFactory();
                fromEvent("callFromGrandma", phone)
                  .pipe(
                    concatMap(factory.bakePie),
                    concatMap(factory.sendPie)
                  ).subscribe(grandmaGetsPie);
            

Множество частей и связи между ними

Проблема

В коде есть "сквозные" (pass-through) методы

Рецепт: пересмотрите архитектуру

Возможно, чего-то не хватает
                class PieFactory {
                    constructor() {
                     this.maker = new RedRidingHood();
                     this.friend = new Friend();
                    }
                    bakePie(pie) { return pie.type === 'PIE'
                       ? this.maker.bakePie(pie)
                       : this.friend.buyPie(pie)
                    }
                  }
            
Или же разделение ошибочно
              const factory = new RedRidingHood();
            

Множество частей и связи между ними

Проблема

Смесь общего и частного
Хинкаль Морковка Пирожок
Коробка
                function basePresentPack(item) {
                  const box = putInBox(item);
                  const pack = addCard(box);
                  return item.type && item.type === 'HINKAL'
                   ? addFork(pack)
                   : pack;
                }
                
                /*где-то в коде*/
                const pie = bakePie(pieType);
                const box = basePresentPack(pie);
            

Множество частей и связи между ними

Проблема

Смесь общего и частного

Рецепт: разделить

Не засоряйте универсальные методы спецификой
                function basePresentPack(item) {
                  const box = putInBox(item);
                  const pack = addCard(box);
                  return pack;
                }
                
                function piePack(pie) {
                  const pack = basePresentPack(item);
                  return pie.type === 'HINKAL'
                    ? addFork(pack)
                    : pack;
                }
            

Множество частей и связи между ними

Проблема: сильная связь

Один метод не понять без другого
                function cook(recipe, ingredients) {
                  const mixture = mix(recipe, ingredients);
                  const cookedItem = bake(mixture);
                  const result = someCookingMagic(cookedItem);
                  return result;
                }
                
                function someCookingMagic(item) {
                  const result = decorate(item);
                  return sayAbracadabra(result);
                }
            

Множество частей и связи между ними

Проблема: сильная связь

Один метод не понять без другого

Рецепт: объединить

Или разделить как-нибудь по-другому
                function cook(recipe, ingredients) {
                    const mixture = mix(recipe, ingredients);
                    const cookedItem = bake(mixture);
                    const result = decorate(cookedItem);
                    return sayAbracadabra(result);
                }
            

Множество частей и связи между ними

Проблема

Перенасыщенность переменными
Каскад условий, вытекающих друг из друга
                fromEvent("callFromGrandma", phone).pipe(
                  .pipe(
                    concatMap(event => {
                      const needPie = !!event.pie;
                      const pie = needPie ? event.pie : null;
                      const type = needPie ? pie.type : null;
                      const pieType = type || 'DEFAULT_TYPE';
                      return type ? bakePie(pieType) : null;
                    }),
                    concatMap(sendPie)
                  ).subscribe(grandmaGetsPie);
            

Множество частей и связи между ними

Проблема

Перенасыщенность переменными
Каскад условий, вытекающих друг из друга

Рецепт: перечитайте и удалите лишнее

Иногда лучше написать условие подлиннее, чем создать переменную
                fromEvent("callFromGrandma", phone).pipe(
                    concatMap(event => event.pie
                      ? bakePie(event.pie.type || 'DEFAULT_TYPE')
                      : null
                    ),
                    concatMap(sendPie)
                  ).subscribe(grandmaGetsPie);
            

Множество частей и связей между ними

Самодокументируемый код. Миф или Реальность?

Проблема: комментарии

Тратят время и засоряют код?

                  // спечь пирожок по просьбе бабушки
                  function bakePie(grandmaRequest, resources) {
                    let pie = grandmaRequest.pie;
                    if (pie.ingredients.some(x => !resources[x])) {
                      pie = X.availablePie(resourses) /*1*/;
                    }
                    switch (pie.type /*2*/) {
                      case "PIE": return makeA();
                      case "HINKAL": return makeB();
                      case "CHEBUREK": return makeC();
                      ..../*3*/
                    }
                  }
              

Рецепт: комментарии нужны, но не любые

Два золотых правила

  1. Удалите все, что дублирует код. Добавьте то, чего в нём нет

Множество частей и связей между ними

Рецепт: комментарии нужны, но не любые

Два золотых правила

  1. Удалите все, что дублирует код. Добавьте то, чего в нём нет
                    // 1: используем X для оптимизации (Y работало плохо)
                    // 2: не проверяем на null, потому что Z
                    // 3: список из JIRA-XXX
                  
  2. Конкретика лучше абстракции
                      // bakePie(
                      //   { type: 'PIE', ingredients: ['flour','oil','meat'] },
                      //   { 'flour': true, 'oil': true, 'meat' true }
                      // )
                  

Множество частей и связей между ними

Советы от вашего капитана

  • По возможности пишите чистые функции без побочных эффектов
  • При сложной бизнес-логике — используйте TypeScript
  • Не дублируйте код
  • Когда рефакторите и удаляете что-то — удаляйте до конца (если кажется, что удалили всё — проверьте еще раз)
  • Не оставляйте закомментаренный код (он есть в истории и путает других)
Капитан Очевидность

Неопределенность

Проблема

Слишком много условий в коде

                function cookForGrandma(recipeNames) {
                  getRecipesFromServer(recipeNames)
                    .then(cookByRecipe)
                    .catch(handleErr);
                }
                function cookByRecipe(recipes) {
                   if (!recipes) return; 
                   recipes.forEach(recipe => {
                     if (recipe.ingredients) {
                         /* mixIngedients */
                     }
                     if (recipe.type === 'soup') {
                       /* Cook soup */
                     } else { /* Bake pie */ }
                   });
                }
            

Рецепт: возможно они вам не нужны?

Удалите лишнее

  • Не проверяйте на true то, что и так существует (массивы)
  • Проверьте типы/контракты/моменты вызова. Не надо проверять, что свойство существует, если оно точно есть в объекте, а объект точно есть на момент вызова.
  • Не проверяйте то, чего не может быть.

Неопределенность

Проблема

Слишком много условий, и они нужны

              function packPies(courier, packA, packB, pies) {
                const parcel = [];
                for (let i = 0; i < pies.length;i++) {
                  let pie = null;
                  switch (pies[i].type) {
                    case "HINKAL": pie = packA(pies[i]); break;
                    case "CHEBUREK":
                    case "PIE": pie = packB(pies[i]); break;
                  }
                  if (pie ) { parcel.push(pie); }
                }
                if (parcel.length ) {
                  courier.send(parcel);
                }
              }
          

Неопределенность

Проблема

Слишком много условий, и они нужны

Рецепт: сделать так, чтобы они исчезли

Измените поведение/архитектуру, чтобы проверки больше не требовались

  • Параметры по умолчанию у методов
  • Дефолтные значения вместо null
  • Объекты вместо условий
  • ФП и монады
            const pack = {'HINKAL': packA,'CHEBUREK': packB,'PIE': packB };
            function packPies(courier, pies, packMap = pack) {
              courier.send(pies
                .filter(pie => packMap[pie.type])
                .map(pie => packMap[pie.type](pie)));
            }
          

Неопределенность

Проблема

Слишком много условий, и они точно нужны!!!

                function getRecipe(pieType) {
                  return getHttpRecipe(pieType).then(extract);
                )
                /*где-то в коде*/
                getRecipe('CHEBUREK')
                  .catch(error => {
                    if (error && error.message) {
                      notify(error);
                    }
                    return DEFAULT_RECIPE;
                  });
                /*где-то еще в коде*/
                getRecipe('HINKAL')
                  .catch(error => {
                    if (error) { console.log(error.message); }
                    return DEFAULT_RECIPE;
                  });
              

Неопределенность

Проблема

Слишком много условий, и они точно нужны!!!

Рецепт: маскирование и агрегация

Перенесите условие туда, где оно меньше всего мешает (на более базовый уровень).

            function getRecipe(pieType) {
              return getHttpRecipe(pieType).then(extract).catch(error => {
                if (error.message) { notify(error.message); }
                return DEFAULT_RECIPE;
              });
            )
          

Или объедините и обрабатывайте разом.

            function bake(pieType) {
              return getRecipe(pieType).then(mix).then(bake).catch(handleBakeError);
            )
          

Риск

Проблема: риск — благородное дело

Задача не сложная и не очень важная, сделаем потом

              // добавить loader
              fromEvent("callFromGrandma", phone)
              .pipe(
                concatMap(event => of(event).pipe(
                    concatMap(bakePies),
                    concatMap(sendPies),
                    retry(N)
                  )),
                catchError(excuse)
              )
              .subscribe(grandma);
            

Риск

Проблема: риск — благородное дело

Задача не сложная и не очень важная, сделаем потом

Рецепт: на самом деле нет

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

  • Отслеживание состояний HTTP запросов
  • Добавление прелоадера
  • Сообщение об ошибках в приложении
                concatMap(event => of(event).pipe(
                  tap(loaderIncrement),
                  concatMap(bakePies),
                  concatMap(sendPies),
                  retry(N),
                  finally(loaderDecrement),
                )),
              

Риск

Проблема

Не надо обсуждать код, который работает

Ошибочная общность взглядов
(fundamental common-ground breakdown)

А
группа А - докладчик
B
группа B - коллега Петя
  • A думает, что B знает про X
  • B про X не знает и не знает, что должен знать
  • Поэтому B ничего не спрашивает
  • Поэтому A считает, что B всё знает

Риск

Проблема

Не надо обсуждать код, который работает

Рецепт: на самом деле надо

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

  • Грамотное код-ревью
  • Ретроспектива с разбором багов
  • Программирование хаоса:
    • Bыбор "ошибочной" ситуации
    • Построение гипотезы
    • Намеренное моделирование этой ситуации
    • Сбор данных и сравнение с гипотезой
Например: что будет, если волк съест
только половину пирожка?
Половина пирожка

Выводы

Прогресс
Время
Тактическое программирование
Стратегическое программирование
Осознанное написание понятного кода в перспективе делает кодовую базу более поддерживаемой и расширяемой
Пирамида потребностей программного кода
Понятный
Отлаживаемый
Тестируемый
Расширяемый
Поддерживаемый

Выводы

Не верьте себе, код всегда понятен сразу после написания!

  • Пользуйтесь метриками
  • Спрашивайте мнения коллег
  • Пишите комментарии
  • Думайте о понимании на каждом этапе разработке

И тогда пирожочки будут реже теряться на пути к бабушке!

Красная Шапочка Пирожок Бабушка

Полезные ссылки

Сссылка на слайды на гитхабе