Рефакторинг салона видеопроката на JavaScript

в 9:11, , рубрики: java, javascript, классы, объектный подход, Проектирование и рефакторинг, рефакторинг, стиль программирования

Рефакторинг салона видеопроката на JavaScript - 1Моя книга по рефакторингу в 1999 году начиналась с простого примера расчёта и форматирования чека для видеомагазина. На современном JavaScript есть несколько вариантов рефакторинга того кода. Здесь я изложу четыре из них: рефакторинг функций верхнего уровня; переход к вложенной функции с диспетчером; используя классы; трансформация с применением промежуточной структуры данных.

Много лет назад, когда я писал книгу по рефакторингу, я начал с (очень) простого примера рефакторинга кода, который вычислял счёт для клиента за прокат видеофильмов (в те дни нам нужно было ходить в салон для этого). Недавно я размышлял об этом примере, в частности, как бы он выглядел на современном JavaScript.

Любой рефакторинг подразумевает улучшение кода в определённом направлении, в том, которое соответствует стилю программирования команды разработчиков. Пример в книге был на Java, а Java (именно в то время) подразумевала определённый стиль программирования, объектно-ориентированный стиль. Однако с JavaScript есть гораздо больше вариантов, какой стиль выбрать. Хотя вы можете придерживаться Java-подобного объектно-ориентированного стиля, особенно с ES6 (Ecmascript 2015), не все сторонники JavaScript одобряют этот стиль. Многие действительно считают, что использовать классы Очень Плохо.

Первоначальный код салона видеопроката

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

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      let movie = movies[r.movieID];
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movie.code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `t${movie.title}t${thisAmount}n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;
  }

Здесь я использую ES6. Код работает на двух структурах данных, обе из которых представляют собой просто списки записей json. Запись клиента выглядит так:

{
  "name": "martin",
  "rentals": [
    {"movieID": "F001", "days": 3},
    {"movieID": "F002", "days": 1},
  ]
}

Структура списка фильмов выглядит следующим образом:

{
  "F001": {"title": "Ran",                     "code": "regular"},
  "F002": {"title": "Trois Couleurs: Bleu",     "code": "regular"},
  // etc
}

В оригинальной книге фильмы были просто представлены как объекты в структуре объектов Java. Для этой статьи я предпочёл перейти на структуру json. Предполагается, что какой-то вид глобального поиска вроде Repository не подходит для этого приложения

Метод выдаёт простое текстовое сообщение о прокате видеофильма.

Rental Record for martin
Ran 3.5
Trois Couleurs: Bleu 2
Amount owed is 5.5
You earned 2 frequent renter points

Такая выдача довольно грубая, даже для примера. Как я мог не потрудиться хотя бы пристойно отформатировать цифры? Но помните, что книга была написана во времена Java 1.1, до добавления в язык формата String. Это частично оправдывает мою лень

Функция statement пахнет как Long Method. Один лишь её размер уже наводит на подозрения. Но один дурной запах — не причина для рефакторинга. Плохо факторизованный код является проблемой, потому что его трудно понять. Если код трудно понять, то его трудно изменить, чтобы добавить новые функции или исправить ошибки. Так что если вам не нужно читать или понимать какой-то код, то его плохая структура никак не повредит вам и вы с радостью оставите его в покое на какое-то время. Поэтому, чтобы пробудить у нас интерес к этому коду, должна быть какая-то причина для его изменения. Наша причина, которую я указал в книге, — это создание HTML-версии отчёта statement примерно с такой выдачей:

<h1>Rental Record for <em>martin</em></h1>
<table>
  <tr><td>Ran</td><td>3.5</td></tr>
  <tr><td>Trois Couleurs: Bleu</td><td>2</td></tr>
</table>
<p>Amount owed is <em>5.5</em></p>
<p>You earned <em>2</em> frequent renter points</p>

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

Разбиение на несколько функций

Каждый раз, когда я работаю со слишком длинной функцией вроде этой, моя первая мысль — попробовать разбить её на логические куски кода и сделать из них отдельные функции с помощью Extract Method. [1]. Первый кусок, который привлёк моё внимание, — это оператор switch.

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      let movie = movies[r.movieID];
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movie.code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movie.code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `t${movie.title}t${thisAmount}n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;
  }

Моя среда разработки (IntelliJ) предлагает сама сделать рефакторинг автоматически, но некорректно проводит его — её способности в JavaScript не настолько продвинуты, как в рефакторинге Java. Так что я сделаю это вручную. Нужно посмотреть, какие данные использует этот кандидат на извлечение. Там три фрагмента данных:

  • Значение thisAmount вычисляется извлечённым кодом. Я могу инициировать его внутри функции и вернуть в конце.
  • Значение r на количество дней проката проверяется в цикле, я могу передать его как параметр.
  • Переменная movie — это фильм, который взяли напрокат. Временные переменные вроде этой обычно мешают во время рефакторинга процедурного кода, так что я предпочту сначала запустить Replace Temp with Query для преобразования её в функцию, которую могу вызвать из любого извлечённого кода.

Когда я закончил с Replace Temp with Query, код выглядит так:

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movieFor(r).code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `t${movieFor(r).title}t${thisAmount}n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;
  
    function movieFor(rental) {return movies[rental.movieID];}
  }

Теперь извлекаем оператор switch.

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
  
    for (let r of customer.rentals) {
      const thisAmount = amountFor(r);
  
      //add frequent renter points
      frequentRenterPoints++;
      // add bonus for a two day new release rental
      if(movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  
      //print figures for this rental
      result += `t${movieFor(r).title}t${thisAmount}n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;
  
    function movieFor(rental) {return movies[rental.movieID];}
  
    function amountFor(r) {
      let thisAmount = 0;
  
      // determine amount for each movie
      switch (movieFor(r).code) {
        case "regular":
          thisAmount = 2;
          if (r.days > 2) {
            thisAmount += (r.days - 2) * 1.5;
          }
          break;
        case "new":
          thisAmount = r.days * 3;
          break;
        case "childrens":
          thisAmount = 1.5;
          if (r.days > 3) {
            thisAmount += (r.days - 3) * 1.5;
          }
          break;
      }
      return thisAmount;
    }
  }

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

function statement(customer, movies) {
  let totalAmount = 0;
  let frequentRenterPoints = 0;
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    const thisAmount = amountFor(r);
    frequentRenterPointsFor(r);

    //print figures for this rental
    result += `t${movieFor(r).title}t${thisAmount}n` ;
    totalAmount += thisAmount;
  }
  // add footer lines
  result += `Amount owed is ${totalAmount}n`;
  result += `You earned ${frequentRenterPoints} frequent renter pointsn`;

  return result;
…
  function frequentRenterPointsFor(r) {
   //add frequent renter points
    frequentRenterPoints++;
    // add bonus for a two day new release rental
    if (movieFor(r).code === "new" && r.days > 2) frequentRenterPoints++;
  }

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

function statement(customer, movies) {
  let totalAmount = 0;
  let frequentRenterPoints = 0;
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    const thisAmount = amountFor(r);
    frequentRenterPoints += frequentRenterPointsFor(r);

    //print figures for this rental
    result += `t${movieFor(r).title}t${thisAmount}n` ;
    totalAmount += thisAmount;
  }
  // add footer lines
  result += `Amount owed is ${totalAmount}n`;
  result += `You earned ${frequentRenterPoints} frequent renter pointsn`;

  return result;
…
  function frequentRenterPointsFor(r) {
    let result = 1;
    if (movieFor(r).code === "new" && r.days > 2) result++;
    return result;
  }

Воспользуюсь шансом слегка почистить две извлечённые функции, пока я их понимаю.

  function amountFor(rental) {
    let result = 0;
    switch (movieFor(rental).code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }
  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
   }

С этими функциями я мог бы сделать кое-что ещё, особенно с amountFor, и я действительно кое-что сделал в книге. Но для этой статьи я больше не буду углубляться в исследование тела этих функций

Готово, теперь возвращаюсь к телу функции.

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      const thisAmount = amountFor(r);
      frequentRenterPoints += frequentRenterPointsFor(r);
  
      //print figures for this rental
      result += `t${movieFor(r).title}t${thisAmount}n` ;
      totalAmount += thisAmount;
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;

Общий подход, который я люблю использовать, заключается в устранении mutable-переменных. Здесь их три, одна собирает финальную строку, ещё две вычисляют значения, которые используются в этой строке. Не имею ничего против первой, но хотелось бы уничтожить две остальные. Для начала следует разбить цикл. Сначала упрощаем цикл и встраиваем постоянную величину.

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      frequentRenterPoints += frequentRenterPointsFor(r);
      result += `t${movieFor(r).title}t${amountFor(r)}n` ;
      totalAmount += amountFor(r);
    }
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;

Затем разбиваем цикл на три части.

  function statement(customer, movies) {
    let totalAmount = 0;
    let frequentRenterPoints = 0;
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      frequentRenterPoints += frequentRenterPointsFor(r);
    }
    for (let r of customer.rentals) {
      result += `t${movieFor(r).title}t${amountFor(r)}n`;
    }
    for (let r of customer.rentals) {
      totalAmount += amountFor(r);
    }
  
    // add footer lines
    result += `Amount owed is ${totalAmount}n`;
    result += `You earned ${frequentRenterPoints} frequent renter pointsn`;
  
    return result;

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

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

  function statement(customer, movies) {
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      result += `t${movieFor(r).title}t${amountFor(r)}n`;
    }
    result += `Amount owed is ${totalAmount()}n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
    return result;
  
    function totalAmount() {
      let result = 0;
      for (let r of customer.rentals) {
        result += amountFor(r);
      }
      return result;
    }
    function totalFrequentRenterPoints() {
      let result = 0;
      for (let r of customer.rentals) {
        result += frequentRenterPointsFor(r);
      }
      return result;
    }

Как фанат цепочек последовательного сбора данных типа collection pipeline я также отрегулирую циклы в такую цепочку.

  function totalFrequentRenterPoints() {
    return customer.rentals
      .map((r) => frequentRenterPointsFor(r))
      .reduce((a, b) => a + b)
      ;
  }
  function totalAmount() {
    return customer.rentals
      .reduce((total, r) => total + amountFor(r), 0);
  }

Не уверен, какой из этих двух типов цепочек мне больше нравится

Исследование скомпонованной функции

Теперь посмотрим, что у нас получилось. Вот весь код.

function statement(customer, movies) {
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    result += `t${movieFor(r).title}t${amountFor(r)}n`;
  }
  result += `Amount owed is ${totalAmount()}n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
  return result;

  function totalFrequentRenterPoints() {
    return customer.rentals
      .map((r) => frequentRenterPointsFor(r))
      .reduce((a, b) => a + b)
      ;
  }
  function totalAmount() {
    return customer.rentals
      .reduce((total, r) => total + amountFor(r), 0);
  }
  function movieFor(rental) {
    return movies[rental.movieID];
  }
  function amountFor(rental) {
    let result = 0;
    switch (movieFor(rental).code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }
  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
  }
}

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

Но я всё ещё не готов писать функцию для выдачи html. Все функции после разбиения вложены внутри общей функции statement. Так легче извлекать функции, так как они могут ссылаться на имена внутри области видимости функции, в том числе друг на друга (как amountFor вызывает movieFor) и соответствующие параметры customer и movie. Но я не могу написать простую функцию htmlStatement, которая ссылается на эти функции. Чтобы поддерживать какие-то другие форматы выдачи с использованием тех же вычислений, нужно продолжить рефакторинг. Теперь я дстиг точки, когда появляются разные варианты рефакторинга в зависимости от того, как я хочу преобразовать код. Далее я опробую каждый из этих вариантов, объясню, как работает каждый из них, а когда все четыре будут готовы, мы их сравним.

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

Один из вариантов — определить формат выдачи как аргумент функции statement. Я хотел бы начать такой рефакторинг с использования Add Parameter, извлечь существующий код для форматирования текста и дописать код в начале для отсылки к извлечённой функции, когда параметр указывает на это.

function statement(customer, movies, format = 'text') {
  switch (format) {
    case "text":
      return textStatement();
  }
  throw new Error(`unknown statement format ${format}`);
  function textStatement() {
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      result += `t${movieFor(r).title}t${amountFor(r)}n`;
    }
    result += `Amount owed is ${totalAmount()}n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
    return result;
  }

Затем я могу написать функцию генерации html и добавить условие для диспетчера.

  function statement(customer, movies, format = 'text') {
    switch (format) {
      case "text":
        return textStatement();
      case "html":
        return htmlStatement();
    }
    throw new Error(`unknown statement format ${format}`);
  
    function htmlStatement() {
      let result = `<h1>Rental Record for <em>${customer.name}</em></h1>n`;
      result += "<table>n";
      for (let r of customer.rentals) {
        result += `  <tr><td>${movieFor(r).title}</td><td>${amountFor(r)}</td></tr>n`;
      }
      result += "</table>n";
      result += `<p>Amount owed is <em>${totalAmount()}</em></p>n`;
      result += `<p>You earned <em>${totalFrequentRenterPoints()}</em> frequent renter points</p>n`;
      return result;
    }

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

function statement(customer, movies, format = 'text') {
  const dispatchTable = {
    "text": textStatement,
    "html": htmlStatement
  };
  if (undefined === dispatchTable[format]) throw new Error(`unknown statement format ${format}`);
  return dispatchTable[format].call();

Использование функций верхнего уровня

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

Чтобы сделать это, я начал с поиска функции, которая не ссылается ни на какие другие, в нашем случае это movieFor.

Всякий раз, когда я перемещаю функции, мне нравится сначала скопировать функцию в новый контекст, встроить её в этот контекст, а затем заменить тело оригинальной функции вызовом на перемещённую функцию.

function topMovieFor(rental, movies) {
  return movies[rental.movieID];
}
function statement(customer, movies) {
  // [snip]
  function movieFor(rental) {
    return topMovieFor(rental, movies);
  }
  function frequentRenterPointsFor(rental) {
    return (movieFor(rental).code === "new" && rental.days > 2) ? 2 : 1;
  }

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

function movieFor(rental, movies) {
  return movies[rental.movieID];
}
function statement(customer, movies) {
  // [snip]
  function frequentRenterPointsFor(rental) {
    return (movieFor(rental, movies).code === "new" && rental.days > 2) ? 2 : 1;
  }

Похожие изменения сделаны внутри amountFor

Одновременно со встраиванием я также переименовал функцию верхнего уровня для соответствия старому имени, так что единственным отличием сейчас остался параметр movies.

Затем проделаем это со всеми вложенными функциями.

function statement(customer, movies) {
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    result += `t${movieFor(r, movies).title}t${amountFor(r, movies)}n`;
  }
  result += `Amount owed is ${totalAmount(customer, movies)}n`;
  result += `You earned ${totalFrequentRenterPoints(customer, movies)} frequent renter pointsn`;
  return result;
}
function totalFrequentRenterPoints(customer, movies) {
  return customer.rentals
    .map((r) => frequentRenterPointsFor(r, movies))
    .reduce((a, b) => a + b)
    ;
}
function totalAmount(customer, movies) {
  return customer.rentals
    .reduce((total, r) => total + amountFor(r, movies), 0);
}
function movieFor(rental, movies) {
  return movies[rental.movieID];
}
function amountFor(rental, movies) {
  let result = 0;
  switch (movieFor(rental, movies).code) {
    case "regular":
      result = 2;
      if (rental.days > 2) {
        result += (rental.days - 2) * 1.5;
      }
      return result;
    case "new":
      result = rental.days * 3;
      return result;
    case "childrens":
      result = 1.5;
      if (rental.days > 3) {
        result += (rental.days - 3) * 1.5;
      }
      return result;
  }
  return result;
}
function frequentRenterPointsFor(rental, movies) {
  return (movieFor(rental, movies).code === "new" && rental.days > 2) ? 2 : 1;
}

Теперь я могу легко написать функцию htmlStatement.

function htmlStatement(customer, movies) {
  let result = `<h1>Rental Record for <em>${customer.name}</em></h1>n`;
  result += "<table>n";
  for (let r of customer.rentals) {
    result += `  <tr><td>${movieFor(r, movies).title}</td><td>${amountFor(r, movies)}</td></tr>n`;
  }
  result += "</table>n";
  result += `<p>Amount owed is <em>${totalAmount(customer, movies)}</em></p>n`;
  result += `<p>You earned <em>${totalFrequentRenterPoints(customer, movies)}</em> frequent renter points</p>n`;
  return result;
}

Объявление некоторых локальных функций с частичным применением

Когда глобальная функция используется таким образом, списки параметров могут довольно сильно растянуться. Так что иногда может быть полезно объявить локальную функцию, которая вызывает глобальную функцию с некоторыми или всеми параметрами внутри. Эта локальная функция, которая является частичным применением глобальной функции, может пригодиться для последующего использования. Есть различные способы сделать такое в JavaScript. Один из них — присвоить локальные функции переменным.

  function htmlStatement(customer, movies) {
    const amount = () => totalAmount(customer, movies);
    const frequentRenterPoints = () => totalFrequentRenterPoints(customer, movies);
    const movie = (aRental) => movieFor(aRental, movies);
    const rentalAmount = (aRental) =>  amountFor(aRental, movies);
    let result = `<h1>Rental Record for <em>${customer.name}</em></h1>n`;
    result += "<table>n";
    for (let r of customer.rentals) {
      result += `  <tr><td>${movie(r).title}</td><td>${rentalAmount(r)}</td></tr>n`;
    }
    result += "</table>n";
    result += `<p>Amount owed is <em>${amount()}</em></p>n`;
    result += `<p>You earned <em>${frequentRenterPoints()}</em> frequent renter points</p>n`;
    return result;
  }

Другой способ — объявить их как вложенные функции.

  function htmlStatement(customer, movies) {
    let result = `<h1>Rental Record for <em>${customer.name}</em></h1>n`;
    result += "<table>n";
    for (let r of customer.rentals) {
      result += `  <tr><td>${movie(r).title}</td><td>${rentalAmount(r)}</td></tr>n`;
    }
    result += "</table>n";
    result += `<p>Amount owed is <em>${amount()}</em></p>n`;
    result += `<p>You earned <em>${frequentRenterPoints()}</em> frequent renter points</p>n`;
    return result;
  
    function amount() {return totalAmount(customer, movies);}
    function frequentRenterPoints() {return totalFrequentRenterPoints(customer, movies);}
    function rentalAmount(aRental) {return amountFor(aRental, movies);}
    function movie(aRental) {return movieFor(aRental, movies);}
  }

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

Использование классов

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

Первым делом обернём данные в объекты, начав с customer.

customer.es6…

  export default class Customer {
    constructor(data) {
      this._data = data;
    }
  
    get name() {return this._data.name;}
    get rentals() { return this._data.rentals;}
  }

statement.es6…

  import Customer from './customer.es6';
  
  function statement(customerArg, movies) {
    const customer = new Customer(customerArg);
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      result += `t${movieFor(r).title}t${amountFor(r)}n`;
    }
    result += `Amount owed is ${totalAmount()}n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
    return result;

До сих пор класс является простой обёрткой вокруг оригинального объекта JavaScript. Дальше сделаем такую же для rental.

rental.es6…

  export default class Rental {
    constructor(data) {
      this._data = data;
    }
    get days() {return this._data.days}
    get movieID() {return this._data.movieID}
  }

customer.es6…

  import Rental from './rental.es6'
  
  export default class Customer {
    constructor(data) {
      this._data = data;
    }
  
    get name() {return this._data.name;}
    get rentals() { return this._data.rentals.map(r => new Rental(r));}
  }

Теперь, когда классы созданы вокруг моих простых объектов json, появилась работа для Move Method. Как и во время переноса функций на верхний уровень, первым делом возьмём ту функцию, которая не обращается ни к каким другим — movieFor. Но этой функции нужен список фильмов в качестве контекста, который нужно будет сделать доступным для создаваемых объектов rental.

statement.es6…

  function statement(customerArg, movies) {
    const customer = new Customer(customerArg, movies);
    let result = `Rental Record for ${customer.name}n`;
    for (let r of customer.rentals) {
      result += `t${movieFor(r).title}t${amountFor(r)}n`;
    }
    result += `Amount owed is ${totalAmount()}n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
    return result;

class Customer...

  constructor(data, movies) {
    this._data = data;
    this._movies = movies
  }
  get rentals() { return this._data.rentals.map(r => new Rental(r, this._movies));}

class Rental...

  constructor(data, movies) {
    this._data = data;
    this._movies = movies;
  }

Когда у меня на месте все поддерживающие данные, можно перенести функцию.

statement.es6…

  function movieFor(rental) {
    return rental.movie;
  }

class Rental...

class Rental...
  get movie() {
    return this._movies[this.movieID];
  }

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

statement.es6…

function statement(customerArg, movies) {
  const customer = new Customer(customerArg, movies);
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    result += `t${r.movie.title}t${amountFor(r)}n`;
  }
  result += `Amount owed is ${totalAmount()}n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
  return result;
  function amountFor(rental) {
    let result = 0;
    switch (rental.movie.code) {
      case "regular":
        result = 2;
        if (rental.days > 2) {
          result += (rental.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = rental.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (rental.days > 3) {
          result += (rental.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }
  function frequentRenterPointsFor(rental) {
    return (rental.movie.code === "new" && rental.days > 2) ? 2 : 1;
  }

Можно использовать ту же базовую последовательность для перемещения в rental и двух вычислений.

statement.es6…

function statement(customerArg, movies) {
  const customer = new Customer(customerArg, movies);
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    result += `t${r.movie.title}t${r.amount}n`;
  }
  result += `Amount owed is ${totalAmount()}n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
  return result;
  function totalFrequentRenterPoints() {
    return customer.rentals
      .map((r) => r.frequentRenterPoints)
      .reduce((a, b) => a + b)
      ;
  }
  function totalAmount() {
    return customer.rentals
      .reduce((total, r) => total + r.amount, 0);
  }

class Rental...

  get frequentRenterPoints() {
    return (this.movie.code === "new" && this.days > 2) ? 2 : 1;
  }
  get amount() {
    let result = 0;
    switch (this.movie.code) {
      case "regular":
        result = 2;
        if (this.days > 2) {
          result += (this.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = this.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (this.days > 3) {
          result += (this.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }

Затем я могу переместить в customer две функции вычисления суммы.

statement.es6…

function statement(customerArg, movies) {
  const customer = new Customer(customerArg, movies);
  let result = `Rental Record for ${customer.name}n`;
  for (let r of customer.rentals) {
    result += `t${r.movie.title}t${r.amount}n`;
  }
  result += `Amount owed is ${customer.amount}n`;
  result += `You earned ${customer.frequentRenterPoints} frequent renter pointsn`;
  return result;
}

class Customer...

  get frequentRenterPoints() {
    return this.rentals
      .map((r) => r.frequentRenterPoints)
      .reduce((a, b) => a + b)
      ;
  }
  get amount() {
    return this.rentals
      .reduce((total, r) => total + r.amount, 0);
  }

Когда логика вычислений переместилась в объекты rental customer, написать html-версию statement просто.

statement.es6…

  function htmlStatement(customerArg, movies) {
    const customer = new Customer(customerArg, movies);
    let result = `<h1>Rental Record for <em>${customer.name}</em></h1>n`;
    result += "<table>n";
    for (let r of customer.rentals) {
      result += `  <tr><td>${r.movie.title}</td><td>${r.amount}</td></tr>n`;
    }
    result += "</table>n";
    result += `<p>Amount owed is <em>${customer.amount}</em></p>n`;
    result += `<p>You earned <em>${customer.frequentRenterPoints}</em> frequent renter points</p>n`;
    return result;
  }

Классы без синтаксиса

Синтаксис классов в ES2015 противоречив, а некоторые думают, что он вообще не нужен (косо поглядывая на Java-разработчиков). Вы можете проделать те же этапы рефакторинга и получить результаты вроде таких:

function statement(customerArg, movies) {
  const customer = createCustomer(customerArg, movies);
  let result = `Rental Record for ${customer.name()}n`;
  for (let r of customer.rentals()) {
    result += `t${r.movie().title}t${r.amount()}n`;
  }
  result += `Amount owed is ${customer.amount()}n`;
  result += `You earned ${customer.frequentRenterPoints()} frequent renter pointsn`;
  return result;
}

function createCustomer(data, movies) {
  return {
    name: () => data.name,
    rentals: rentals,
    amount: amount,
    frequentRenterPoints: frequentRenterPoints
  };

  function rentals() {
    return data.rentals.map(r => createRental(r, movies));
  }
  function frequentRenterPoints() {
    return rentals()
      .map((r) => r.frequentRenterPoints())
      .reduce((a, b) => a + b)
      ;
  }
  function amount() {
    return rentals()
      .reduce((total, r) => total + r.amount(), 0);
  }
}

function createRental(data, movies) {
  return {
    days: () => data.days,
    movieID: () => data.movieID,
    movie: movie,
    amount: amount,
    frequentRenterPoints: frequentRenterPoints
  };

  function movie() {
    return movies[data.movieID];
  }

  function amount() {
    let result = 0;
    switch (movie().code) {
      case "regular":
        result = 2;
        if (data.days > 2) {
          result += (data.days - 2) * 1.5;
        }
        return result;
      case "new":
        result = data.days * 3;
        return result;
      case "childrens":
        result = 1.5;
        if (data.days > 3) {
          result += (data.days - 3) * 1.5;
        }
        return result;
    }
    return result;
  }

  function frequentRenterPoints() {
    return (movie().code === "new" && data.days > 2) ? 2 : 1;
  }

В этом подходе используется шаблон Function As Object. Функции-конструкторы (createCustomer и createRental) возвращают объект JavaScript (хэш) вызовов функции. Каждая функция-конструктор содержит замыкание с данными объекта. Поскольку возвращаемые объекты функции находятся в том же контексте функции, они имеют доступ к этим данным. С моей точки зрения это такой же шаблон, что и использование синтаксиса классов, но реализованный иначе. Я предпочитаю использовать явный синтаксис, потому что он более явный — это позволяет мне яснее рассуждать.

Преобразование данных

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

В терминах рефакторинга это пример ещё не написанного рефакторинга Split Phase, который описал мне Кент Бек прошлым летом. С таким рефакторингом я разбиваю вычисления на две фазы, которые сообщаются между собой через промежуточную структуру данных. Начнём этот рефакторинг с введения промежуточной структуры данных.

  function statement(customer, movies) {
    const data = createStatementData(customer, movies);
    let result = `Rental Record for ${data.name}n`;
    for (let r of data.rentals) {
      result += `t${movieFor(r).title}t${amountFor(r)}n`;
    }
    result += `Amount owed is ${totalAmount()}n`;
    result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
    return result;
  
    function createStatementData(customer, movies) {
      let result = Object.assign({}, customer);
      return result;
    }

Для этого случая я улучшу оригинальную структуру данных customer, добавив в неё элементы, начав с вызова к Object.assign. Я мог бы сделать и полностью новую структуру данных. В реальности выбор зависит от того, насколько отличается преобразованная структура данных от оригинальной.

Затем то же самое сделаем с каждой строкой rental.

function statement…

  function createStatementData(customer, movies) {
    let result = Object.assign({}, customer);
    result.rentals = customer.rentals.map(r => createRentalData(r));
    return result;

    function createRentalData(rental) {
      let result = Object.assign({}, rental);
      return result;
    }
  }

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

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

function statement(customer, movies) {
  const data = createStatementData(customer, movies);
  let result = `Rental Record for ${data.name}n`;
  for (let r of data.rentals) {
    result += `t${r.title}t${amountFor(r)}n`;
  }
  result += `Amount owed is ${totalAmount()}n`;
  result += `You earned ${totalFrequentRenterPoints()} frequent renter pointsn`;
  return result;
  //…

  function createStatementData(customer, movies) {
    // …
    function createRentalData(rental) {
      let result = Object.assign({}, rental);
      result.title = movieFor(rental).title;
      return result;
    }
  }

Продолжим с вычислением количества и общей суммы.

function statement(customer, movies) {
  const data = createStatementData(customer, movies);
  let result = `Rental Record for ${data.name}n`;
  for (let r of data.rentals) {
    result += `t${r.title}t${r.amount}n`;
  }
  result += `Amount owed is ${data.totalAmount}n`;
  result += `You earned ${data.totalFrequentRenterPoints} frequent renter pointsn`;
  return result;

  function createStatementData(customer, movies) {
    let result = Object.assign({}, customer);
    result.rentals = customer.rentals.map(r => createRentalData(r));
    result.totalAmount = totalAmount();
    result.totalFrequentRenterPoints = totalFrequentRenterPoints();
    return result;

    function createRentalData(rental) {
      let result = Object.assign({}, rental);
      result.title = movieFor(rental).title;
      result.amount = amountFor(rental);
      return result;
    }
  }

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

function statement (customer, movies) {
  // body …
  function createStatementData (customer, movies) {
    // body …

    function createRentalData(rental) { … }
    function totalFrequentRenterPoints() { … }
    function totalAmount() { … }
    function movieFor(rental) { … }
    function amountFor(rental) { … }
    function frequentRenterPointsFor(rental) { … }
  }
}

Затем перенесу createStatementData за пределы statement.

function statement (customer, movies) { … }

function createStatementData (customer, movies) {
  function createRentalData(rental) { … }
  function totalFrequentRenterPoints() { … }
  function totalAmount() { … }
  function movieFor(rental) { … }
  function amountFor(rental) { … }
  function frequentRenterPointsFor(rental) { … }
}

Когда я разделил функции таким образом, можно написать HTML-версию statement, которая будет использовать ту же структуру данных.

function htmlStatement(customer, movies) {
  const data = createStatementData(customer, movies);
  let result = `<h1>Rental Record for <em>${data.name}</em></h1>n`;
  result += "<table>n";
  for (let r of data.rentals) {
    result += `  <tr><td>${r.title}</td><td>${r.amount}</td></tr>n`;
  }
  result += "</table>n";
  result += `<p>Amount owed is <em>${data.totalAmount}</em></p>n`;
  result += `<p>You earned <em>${data.totalFrequentRenterPoints}</em> frequent renter points</p>n`;
  return result;
}

Можно также перенести createStatementData в отдельный модуль, чтобы ещё чётче обозначить границы между вычислением данных и рендерингом (печатью) отчётов.

Сравнение подходов

Итак, пришло время отступить назад и окинуть взглядом результат. Есть первоначальный код, написанный как единая встроенная функция. Я захотел провести рефакторинг этого кода, чтобы сделать HTML-рендеринг отчёта без повторения кода вычислений. Первым делом я разбил этот код на несколько функций, существующих внутри оригинальной функции. После этого я исследовал четыре отдельных пути.
Рефакторинг салона видеопроката на JavaScript - 3


top-level-functions

Все функции пишем как функции верхнего уровня

    function htmlStatement(customer, movies)
    function textStatement(customer, movies)
    function totalAmount(customer, movies)
    function totalFrequentRenterPoints(customer, movies)
    function amountFor(rental, movies)
    function frequentRenterPointsFor(rental, movies)
    function movieFor(rental, movies)

Показать код


parameter-dispatch

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

    function statement(customer, movies, format)
        function htmlStatement()
        function textStatement()
        function totalAmount()
        function totalFrequentRenterPoints()
        function amountFor(rental)
        function frequentRenterPointsFor(rental)
        function movieFor(rental)

Показать код


classes

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

    function textStatement(customer, movies)
    function htmlStatement(customer, movies)
    class Customer
        get amount()
        get frequentRenterPoints()
        get rentals()
    class Rental
        get amount()
        get frequentRenterPoints()
        get movie()

Показать код


transform

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

    function statement(customer, movies)
    function htmlStatement(customer, movies)
    function createStatementData(customer, movies)
        function createRentalData()
        function totalAmount()
        function totalFrequentRenterPoints()
        function amountFor(rental)
        function frequentRenterPointsFor(rental)
        function movieFor(rental)

показать код


Начну с примера функций верхнего уровня, чтобы выбрать в качестве базиса для сравнения концептуально простейшую альтернативу. [2] Она простая, потому что делит работу на ряд чистых функций, а к ним всем можно обратиться из любой точки кода. Такое просто использовать и просто тестировать — я могу легко протестировать любую отдельную функцию или с помощью наборов тестовых данных, или с помощью REPL.

Отрицательная сторона top-level-functions — в большом количестве повторяющихся передач параметров. Каждой функции нужно дать структуру данных с фильмами, а функциям уровня customer — ещё и структуру данных пользователей. Меня здесь волнует не набор одного и того же текста на клавиатуре, а чтение одного и того же. Каждый раз при чтении параметров я должен понять, что это такое, и проверить их на изменение. Для всех этих функций данные о пользователях и фильмах являются общим контекстом — но с функциями верхнего уровня этот общий контекст не выделен явно. Я делаю такой вывод, когда читаю программу и строю модель её выполнения в своей голове, и я предпочитаю, чтобы вещи были настолько внятными, насколько возможно.

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

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

Но подход parameter-dispatch начинает вихлять, когда от моего контекста требуются другие общие модели поведения, вроде ответа в формате HTML. Мне пришлось написать что-то вроде диспетчера для определения, какую функцию я хочу вызвать. Определение формата для рендерера — это не очень плохо, но такая логика диспетчера явно дурно пахнет. Однако я написал её, она всё ещё существенно копирует базовую возможность языка по вызову именованной функции. И я направляюсь по пути, который быстро приводит меня к такой несуразице:

function executeFunction (name, args) {
  const dispatchTable = {
    //...

Для такого подхода есть контекст, если выбор формата выдачи поступает на вызов как данные. В этом случае должен быть механизм диспетчера для этого элемента данных. Однако если вызов идёт к функции statement вроде такого…

const someValue = statement(customer, movieList, 'text');

…то я вообще не могу написать никакую логику диспетчера в коде.

Ключевое здесь — способ вызова. Использовать литеральные значения для указания выбора функции — дурной подход. Вместо этого API позволим вызывающему оператору сказать, что он хочет получить, в самом названии функции, textStatement или htmlStatement. Тогда я могу использовать механизм диспетчера функций в языке и избежать сколачивания костылей своими силами.

Итак, с двумя вариантами в кармане, где я нахожусь? Я хочу какой-то явный общий контекст для какой-то логики, но с помощью этой логики нужно вызывать разные операции. Когда приходится сталкиваться с подобными требованиями, мне немедленно приходит мысль об объектном подходе — который по сути представляет набор независимо вызываемых операций в общем контексте. [3] Это приводит меня к примеру с классами, в котором можно захватить общий контекст пользователей и фильмов в объектах customer и rental. Я устанавливаю контекст один раз, когда утверждаю объекты, а затем вся дальнейшая логика может использовать общий контекст.

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

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

Я описал объекты как общий набор с частичным применением, но на них можно посмотреть иначе. Объекты создаются с входящей структурой данных, но пополнение этих данных результатами вычислений происходит через их вычислительные функции. Я усилил такой взгляд на вещи, сделав эти геттеры, так что клиент расценивает их в точности так же, как сырые данные — применяя Uniform Access Principle. На это можно посмотреть как на переход от аргумента конструктора к этой виртуальной структуре данных геттеров. В примере преобразования transform та же идея, но реализована созданием новой структуры данных, которая сочетает первоначальные данные и результаты вычислений. Как объекты инкапсулируют вычислительную логику внутрь классов customer и rental, так же и подход transform инкапсулирует эту логику внутрь createStatementData и createRentalData. Этот подход преобразования базовых структур данных List And Hash часто является общим свойством функционального мышления. Он позволяет функциям create…Data разделять нужный им контекст, а логике рендеринга использовать разнообразную выдачу без осложнений.

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

Итак, четыре подхода — какой предпочесть? Мне не хочется писать логику диспетчера, так что я бы не использовал подход parameter-dispatch. Можно было бы выбрать top-level-functions, но они быстро портят впечатление о себе, когда общий контекст увеличивается в размере. Даже всего с двумя аргументами я бы лучше встроил функции для достижения других альтернатив. Выбрать между классами и преобразованием тяжелее, оба подхода дают возможность сделать явным общий контекст и разделение. Мне неинтересны петушиные бои, так что я просто брошу монетку и пусть жребий выберет победиителя.

Дальнейший рефакторинг

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

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

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

Примечания

[1] Этот каталог рефакторинга был написан во времена популярности объектно-ориентированного словаря, так что я использовал термин «метод» для наименования функции/подпрограммы/процедуры и тому подобного. В JavaScript будет более разумно использовать термин «функция», но я использую названия терминов рефакторинга из книги. Вернуться к статье

[2] Вариант parameter-dispatch лучше подходит для первоначального рефакторинга, поскольку его структура ближе к оригинальному набору вложенных функций. Но если сравнивать альтернативы, тогда проще начать с top-level-functions. Вернуться к статье

[3] Мне скорее нравится предложение Уилла Кука для определения «объекта». Вернуться к статье

Автор: m1rko

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js