Умный и безопасный рефакторинг с Scientist 🔬⚗️

Ученый — это библиотека Ruby, которая устраняет неопределенность рефакторинга. Он не предназначен для написания тестов и не предназначен для их замены. Скорее, это показывает вам как рефакторинговый код будет работать в продакшене фактически без используя его в производстве. Если вы выполняете какой-либо рефакторинг кода, который не можете позволить себе сломать, Scientist для вас.

В этом посте будет рассказано о том, как начать работу с Scientist, а также о некоторых распространенных ловушках, но сначала немного контекста.

Проблемы с рефакторингом

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

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

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

Сэнди Мец описал это явление в прошлогоднем разговоре. Она называет это Антипаттерн Звезды Смертии доходит до того, что говорит, «Каждый есть эта проблема». Ее доклад предлагает принципы ООП в качестве средства правовой защиты (это хорошо, смотрите), но это решает только половину проблемы. Ваш набор тестов предполагаемый чтобы позаботиться о другой половине, а именно о том, что вы не можете позволить себе сломать код, который вы только что отрефакторили, но иногда этого просто недостаточно.

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

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

Тут в дело вступает Ученый.

Основы

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

Применение

мертв просто:

science "My first experiment" do |experiment|
  experiment.use { original_code }
  experiment.try { refactored_code }
end

(Для простоты я опустил немного шаблонного кода, но вы можете найти его в ПРОЧТИ МЕНЯ.)

Конфигурация

просто немного грязнее:

class MyExperiment
  include Scientist::Experiment
  attr_accessor :name

  def initialize(name)
    @name = name
  end

  def enabled?
    ...
  end

  def publish(result)
    ...
  end
end


module Scientist::Experiment
  def self.new(name)
    MyExperiment.new(name)
  end
end

Вам нужно будет определить пользовательские реализации для #enabled? а также #publish. Один называется до каждый эксперимент и определяет, будет ли он вообще проводиться; другой называется после каждого эксперимента и, как вы уже догадались, публикует результаты.

Если вы работаете над проектом Rails, поместите этот материал в инициализатор.

Проверить ПРОЧТИ МЕНЯ для подробного изложения параметров конфигурации и API. А пока давайте углубимся в два метода, которые мы видели выше.

#enabled?

Самый простой подход — просто запускать эксперимент каждый раз:

def enabled?
  true
end

Так почему бы вам сделать это по-другому?

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

    def enabled?
      Rails.env.staging? || Rails.env.production?
    end
    

    Есть другие подходы к этомуесли вы хотите получить умный.

  • Обе ветви вашего эксперимента выполняются на переднем плане одна за другой. Если это ресурсоемкие операции, возможно, не стоит запускать их каждый раз. Бывший главный инженер GitHub Джесси Тот объясняет:

    Мы часто медленно наращиваем процент запросов, которые запускают эксперимент Scientist, если мы считаем, что эксперимент будет очень дорогим. Запуска эксперимента с 1% или 5% трафика может быть достаточно, чтобы собрать много данных о производительности и несоответствиях.

    Официальная документация содержит хороший образец конфигурации для «наращивание экспериментов» этим способом.

#publish

README содержит большое пример реализации #publish который отправляет результаты в Graphite (через StatsD) и Redis. Но если у вас нет такого механизма отчетности, настроенного вокруг вашего приложения, не беспокойтесь — нет никаких причин, по которым мы не можем адаптировать его для использования вместо этого обычных текстовых файлов.

Следующая конфигурация будет публиковать результаты в разделе log/scientist/<experiment-name>/с данными о времени и несоответствии, хранящимися в формате YAML и ограниченными 1000 записями:

class MyExperiment
  include Scientist::Experiment
  attr_accessor :name

  def initialize(name)
    @name = name
    FileUtils.mkdir_p(log_dir)
  end

  ...

  def publish(result)
    store_timing_data

    if result.matched?
      increment('matched')
    elsif result.ignored?
      increment('ignored')
    else
      increment('mismatched')
      store_mismatch_data(result)
    end
  end

  private

  
  def log_dir
    @log_dir ||= Rails.root.join('log', 'scientist', name.parameterize)
  end

  def store_timing_data
    fname = log_dir.join('timing.yml')

    log = begin
            YAML.safe_load(File.read(fname), [Symbol])
          rescue Errno::ENOENT
            { control: [], candidate: [] }
          end

    log[:control].push(result.control.duration)
    log[:candidate].push(result.candidates.first.duration)
    log.values.each { |timings| timings.shift if timings.length > 1000 }

    File.write(fname, log.to_yaml)
  end

  def increment(type)
    fname = log_dir.join(type)
    File.write(fname, File.read(fname).next)
  rescue Errno::ENOENT
    File.write(fname, "0\n")
  end

  def store_mismatch_data(result)
    payload = {
      name:            name,
      context:         context,
      control:         observation_payload(result.control),
      candidate:       observation_payload(result.candidates.first),
      execution_order: result.observations.map(&:name)
    }

    fname = log_dir.join('mismatch_data.yml')

    log = begin
            YAML.safe_load(File.read(fname), [Symbol])
          rescue Errno::ENOENT
            []
          end

    log.push(payload)
    log.shift if log.length > 1000
    File.write(fname, log.to_yaml)
  end

  def observation_payload(observation)
    if observation.raised?
      {
        exception: observation.exception.class,
        message:   observation.exception.message,
        backtrace: observation.exception.backtrace
      }
    else
      {
        value: observation.cleaned_value
      }
    end
  end
end

Ловушка: побочные эффекты

Одна из самых больших проблем с Scientist заключается в том, что он в основном сосредоточен на возвращаемые значения скорее, чем побочные эффекты (функциональные программисты объединяйтесь!). То есть успех или неудача ваших экспериментов зависит исключительно от того, насколько вы #use а также #try блоки (или «управление» и «кандидат») возвращают одно и то же значение (хотя есть место для конфигурации).

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

  1. Либо код является идемпотентным, что означает, что у ученого нет возможности проверить, действительно ли вторая ветвь эксперимента что-то сделала, либо
  2. код нет idempotent, и в этом случае электронные письма отправляются (или платежи обрабатываются!) дважды.

Это существенное ограничение библиотеки, хотя существует одно потенциальное обходное решение, когда рассматриваемым побочным эффектом является запись в базу данных: #try ветвь в транзакции и выполнить откат в конце.

Ловушка: нечистые функции

Один рефакторинг, над которым я недавно работал, касался метода «счетчика»: каждый раз, когда вы его вызываете, он возвращает уникальную числовую строку, начинающуюся с текущей даты. Так, например, если вы вызвали его три раза сегодня и один раз завтра, вы получите четыре возвращаемых значения:

  • "201906290001"
  • "201906290002"
  • "201906290003"
  • "201906300001"

Технически это был частный случай побочных эффектов: номера присваивались «билетам» в системе, поэтому каждый раз #try блокировать пробеги, счетчик будет продвигаться вперед, и номер билета будет «потерян».

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

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

Подведение итогов

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

Если ваша кодовая база остро нуждается в редизайне, попробуйте. Кто знает, возможно, вам даже удастся убедить своего клиента/менеджера/и т.д. что небольшой рефакторинг стоит потраченного времени.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *