Как улучшить эту функцию js, чтобы сделать ее быстрее, а также лучше тестируемой?

У меня есть простая программа:

function counterPattern() {
  for (let i = 1; i <= 100; i++) {
    if (i % 3 === 0 && i % 5 === 0) {
      console.log("c");
    } else if (i % 3 === 0) {
      console.log("a");
    } else if (i % 5 === 0) {
      console.log("b");
    }
  }
}

counterPattern();

Меня попросили поэтапно улучшить вышеуказанную функцию:

  1. Уменьшить время выполнения и
  2. Напишите для него юнит-тесты

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

Ниже приводится попытка в этом направлении:

function counterPattern(){
    const pattern = []; // for making it unit-testable; returning pattern will help with snapshotting/matching without losing the order;
    for ( let i=1; i<=100 ; i++ ) {
        const rem3 = i % 3; // separate calculation to + the speed; same for next-line;
        const rem5 = i % 5;
        if ( rem3 === 0 && rem5 === 0 ) {
            pattern.push('c');
        }else if ( rem3 === 0 ){
            pattern.push('a');
        }else if ( rem5 === 0){
            pattern.push('b');
        }
    }
    // returning array would allow the caller to format the pattern
    return pattern; 
}

let result = counterPattern();
console.log(result);

Мой вопрос

Как исходная программа может быть написана по-другому:

  1. чтобы он был быстрее в исполнении,
  2. и таким, чтобы его можно было тестировать;

Есть ли у вас какие-либо комментарии к моей попытке?


person H S    schedule 14.08.2020    source источник
comment
Я не думаю, что снимки необходимы для непосредственного тестирования этой функции. Поскольку, насколько я могу судить, это выглядит как чистая функция, вывод всегда будет одинаковым. Я склонен следовать парадигме тестирования KentCDodds, так что это, конечно, ваша прерогатива.   -  person Jacob    schedule 14.08.2020
comment
О более быстром выполнении: en.wikipedia.org/wiki/Fizz_buzz — это чрезвычайно хорошо известная проблема, и, вероятно, каждое решение, которое вы можете придумать на любом языке, есть где-то в Интернете.   -  person ASDFGerte    schedule 14.08.2020
comment
@Jacob - спасибо за ваш вклад; я думал о тестировании моментальных снимков для варианта использования, когда функция будет изменена в будущем, но спецификация та же; или наоборот, если у нас есть результат для начала и развивается тело функции; также проверю KentCDodds - для меня это ново.   -  person H S    schedule 14.08.2020


Ответы (1)


1. Скорость

Что касается скорости алгоритма, есть следующие соображения:

  • Примерно половина итераций цикла не приведет к результату. Это можно улучшить;
  • Существует шаблон, который повторяется каждые 15 итераций, потому что наименьшее общее кратное 3 и 5 равно 15.

Таким образом, вы можете жестко закодировать этот шаблон в свой код и выполнить вывод, соответствующий диапазону от 1 до 15, а затем повторить:

function counterPattern() {
  for (let i = 0; i < 90; i += 15) {
    console.log("a"); // 3 + i
    console.log("b"); // 5 + i
    console.log("a"); // 6 + i
    console.log("a"); // 9 + i
    console.log("b"); // 10 + i
    console.log("a"); // 12 + i
    console.log("c"); // 15 + i
  }
  // The remainder: values between 90 and 100:
  console.log("a"); // 93
  console.log("b"); // 95
  console.log("a"); // 96
  console.log("a"); // 99
  console.log("b"); // 100
}

counterPattern();

2. Тестирование

Чтобы сделать этот код пригодным для тестирования, у вас есть несколько вариантов. Использование массива, как и вы, является одним из них. Вы также можете рассмотреть возможность превращения функции в генератор, а затем использовать yield для вывода значений.

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

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

Вот как будет работать насмешка без использования внешней библиотеки:

function test() { // Wrapper to keep the mocking local

  function orig_counterPattern() { // The reference implementation
    for (let i = 1; i <= 100; i++) {
      if (i % 3 === 0 && i % 5 === 0) {
        console.log("c");
      } else if (i % 3 === 0) {
        console.log("a");
      } else if (i % 5 === 0) {
        console.log("b");
      }
    }
  }

  function counterPattern() { // Our own implementation
    for (let i = 0; i < 90; i += 15) {
      console.log("a"); // 3 + i
      console.log("b"); // 5 + i
      console.log("a"); // 6 + i
      console.log("a"); // 9 + i
      console.log("b"); // 10 + i
      console.log("a"); // 12 + i
      console.log("c"); // 15 + i
    }
    // The remainder: values between 90 and 100:
    console.log("a"); // 93
    console.log("b"); // 95
    console.log("a"); // 96
    console.log("a"); // 99
    console.log("b"); // 100
  }

  // Mock console object:
  let console = {
    log: (value) => output.push(value)
  };

  // Collect the expected output
  let output = [];
  orig_counterPattern();
  let reference = [...output];

  // Run our own implementation
  output = [];
  counterPattern();
  let result = [...output];

  // Stop mocking the console object
  console = globalThis.console;
  // Compare
  console.assert(output.length === reference.length, "incorrect number of outputs");
  console.assert(reference.every((ref, i) => ref === result[i], "mismatch"));
}

test();
console.log("Test completed.");

Более короткий код

Во-первых, вы, конечно, можете жестко закодировать вывод полностью, даже избегая цикла. Но это противоречило бы принципу, что мы не должны повторяться (СУХОЙ).

Вот вариант, который немного более СУХОЙ:

function counterPattern() {
  let pattern = "abaabac".repeat(105/15).slice(0, -2);
  for (let c of pattern) console.log(c);
}

counterPattern();

person trincot    schedule 14.08.2020