Рефакторинг

Алексей Золотых, Wrike

Clock


					#!/bin/sh
					npm install -g cloc;
					clock master
					

https://goo.gl/PGu4Tb

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

Зачем рефакторить!?

Когда нельзя рефакторить

Необходимость рефакторинга куска кода как функция от частоты изменения

https://goo.gl/56pRGe
  1. Изменение сигнатуры метода (Change Method Signature)
  2. Инкапсуляция поля (Encapsulate Field)
  3. Выделение класса (Extract Class)
  4. Выделение интерфейса (Extract Interface)
  5. Выделение локальной переменной (Extract Local Variable)
  6. Выделение метода (Extract Method)
  7. Генерализация типа (Generalize Type)
  8. Встраивание (Inline)
  9. Введение фабрики (Introduce Factory)
  10. Введение параметра (Introduce Parameter)
  11. Подъём метода (Pull Up Method)
  12. Спуск метода (Push Down Method)
  13. Переименование метода (Rename Method)
  14. Перемещение метода (Move Method)
  15. Замена условного оператора полиморфизмом (Replace Conditional with Polymorphism)
  16. Замена наследования делегированием (Replace Inheritance with Delegation)
  17. Замена кода типа подклассами (Replace Type Code with Subclasses)

Все это не подходит для фронтенда

Все это не очень подходит для фронтенда

Все это не всегда подходит для фронтенда

В браузере много контекстов

Контексты

  1. HTML
  2. CSS
  3. JS
  4. Сборщик
  5. LESS
  6. TypeScript

Делайте изменения маленькими

К рефакторингу хорошо бы подготовиться

  • Пофиксить все проблемы линтера
  • Автоматически форматировать код
  • Написать тесты

Зачем линтер?!

— код единообразней

Зачем тесты?

Правильные тесты повышают надежность

Код компонента


class FilterForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: '',
      frameworks: ['react', 'angular', 'ember', 'backbone']
    };

    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }


  render() {
    const filteredElements = this.state.frameworks
      .filter(e => e.includes(this.state.value))
      .map(e => 
  • { e }
  • ) return (
      { filteredElements }
    ); } }

    Код компонента

    
    class FilterForm extends React.Component {
      render() {
        const filteredElements = this.state.frameworks
          .filter(e => e.includes(this.state.value))
          .map(e => 
  • { e }
  • ) return (
      { filteredElements }
    ); } }
    
    test('should work', () => {
    	const wrapper = shallow(<FilterForm />);
    	
    	expect(wrapper.find('li').length).to.equal(4);
    	
    	wrapper.find('input').simulate('change', {target: {value: 'react'}});
    	
    	expect(wrapper.find('li').length).to.equal(1);
    });	
    					
    
    const filteredElements = this.state.frameworks
          .filter(e => e.includes(this.state.value))
          .map(e => 
  • { e }
  • )
    
    const filteredElements = this.state.frameworks
          .filter(e => e.includes(this.state.value))
          .map(e => <li>{ e }</li>)
    					
    
    test('should work', () => {
    	const wrapper = shallow(<FilterForm />);
    	
    	expect(wrapper.find('li').length).to.equal(4);
    	
    	wrapper.find('input').simulate('change', {target: {value: 'react'}});
    	
    	expect(wrapper.find('li').length).to.equal(1);
    });	
    					
    
    test('should work', () => {
    	const wrapper = shallow(<FilterForm />);
    	
    	expect(wrapper.find('li').length).to.equal(4);
    	
    	wrapper.find('input').simulate('change', {target: {value: 'react'}});
    	
    	expect(wrapper.find('li').length).to.equal(1);
    });	
    					
    					

    Правило туриста

    — полянку нужно оставить чище, чем она была

    — иногда нужно внедрять принудительно

    Что насчет больших рефакторингов?

    Иногда помогает мыслить нестандартно

    Недостатки

    • Глобальная область видимости
    • Долгая сборка и пересборка
    • Легаси в коде

    webpack

    webpack

    Используем консольку

    
    							google_closure_compiler *.js | sed 's/\(.*\)/require("\1");/g' > index.js
    						

    
    							webpack index.js dist/output.js
    						

    А что же стало с глобальными именами?

    script-loader

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

    eval('Ваш скрипт');

    script-loader 💩

    expose-loader

    Добавляет модуль в глобальный конектст

    require("expose-loader?$!jquery");
    Пользуйтесь статическими анализаторами кода!
    
    ...
    constructor: function(arguments) {
      arguments.store = this._escapeValue(arguments.store);
      $wspace.task.customfields.ComboBoxField.superclass
        .constructor.call(this, arguments);
    },
    ...
    
    
    ...
    constructor: function(arguments) {
      arguments.store = this._escapeValue(arguments.store);
      $wspace.task.customfields.ComboBoxField.superclass
        .constructor.call(this, arguments);
    },
    ...
    

    Рефакторинг на основе AST

    (Абстрактное синтаксическое дерево)

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

    grasp

    npm install -g grasp

    http://www.graspjs.com/

    CSS подобный синтаксис

    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  
    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  
    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  
    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  
    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  
    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  
    
    $ grasp 'if.test[op=&&]' a.js
    
      2:   if (x && f(x)) { return x; }
      4:   if (x != j) { return 'test'; }
      5:   if (xs.length && ys.length) {
      10:  if (x == 3 && list[x]) {
                  

    Поиск по шаблонам

    
    $ grasp -e 'return __ + __' b.js
    
      3:   if (x < 2) { return x + 2; }
      13:  return '>>' + str.slice(2);
      15:  return f(z) + x;
                  
    
    $ grasp -e 'return __ + __' b.js
    
      3:   if (x < 2) { return x + 2; }
      13:  return '>>' + str.slice(2);
      15:  return f(z) + x;
                  
    
    $ grasp -e 'return __ + __' b.js
    
      3:   if (x < 2) { return x + 2; }
      13:  return '>>' + str.slice(2);
      15:  return f(z) + x;
                  

    Рефакторинг

    
    if (y < 2) {
      window.x = y + z;
    }
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    
    if (y < 2) {
      window.x = y + z;
    }
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    
    if (y < 2) {
      window.x = y + z;
    }
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    
    if (y < 2) {
      window.x = y + z;
    }
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    
    if (y < 2) {
      window.x = y + z;
    }
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    
    if (y < 2) {
      window.x = y + z;
    }
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    if (f(y < 2)) {
      window.x = f(y + z);
    }
    
    $ grasp '[left=#y]' --replace 'f({{}})' f.js
    
    if (f(y < 2)) {
      window.x = f(y + z);
    }

    jscodeshift

    jscodeshift is a toolkit for running codemods over multiple JS files.

    https://github.com/facebook/jscodeshift

    Codemods

    
    module.exports = function(fileInfo, api) {
      return api.jscodeshift(fileInfo.source)
        .findVariableDeclarators('foo')
        .renameTo('bar')
        .toSource();
    }
                  

    Codemods

    
    module.exports = function(fileInfo, api) {
      return api.jscodeshift(fileInfo.source)
        .findVariableDeclarators('foo')
        .renameTo('bar')
        .toSource();
    }
                  

    Codemods

    
    module.exports = function(fileInfo, api) {
      return api.jscodeshift(fileInfo.source)
        .findVariableDeclarators('foo')
        .renameTo('bar')
        .toSource();
                  

    Codemods

    
    module.exports = function(fileInfo, api) {
      return api.jscodeshift(fileInfo.source)
        .findVariableDeclarators('foo')
        .renameTo('bar')
        .toSource();
                  

    Готовый сборник рецептов

    https://github.com/cpojer/js-codemod

    • var в const или let.
    • Обратные вызовы в cтрелочные функции
    • Строки в шаблоны
    Стили тоже можно рефакторить в AST режиме
    • Stylus
    • LESS
    • CSS
     LESS

    Stylus → CSS + комменатрии

    
     ...
    color: blue
    }
    /* $$$ file1.stylus */
    .my-awesome-class {
    	color: red;
    ....
                
    
     ...
    color: blue
    }
    /* $$$ file1.stylus */
    .my-awesome-class {
    	color: red;
    ....
                

    Stylus → CSS

    Stylus → CSS → PostCSS

    POSTCSS + plugins

    • Убрать дублирование стилей
    • Уменьшить разброс цветов
    • Убрать префиксы
    • Выделить новые переменные

    POSTCSS в сборку

    Stylus → CSS → PostCSS

    Stylus → CSS → PostCSS → CSS 🔥

    Stylus → CSS → PostCSS → CSS 🔥 → Less

    Stylus → CSS → PostCSS → CSS 🔥 → Less 😇

    Мердж изменений — очень больно

    Gulp

    
    gulp.task('refactor', function () {
      return gulp.src('folder/**/*.js')
        .pipe(RefactoringPlugin())
        .pipe(gulp.dest('./'))
    })
                  

    Если что-то пошло не так, то

    
    $ git reset --hard
    $ git merge origin/master
    $ gulp refactor
                  

    Спасибо!

    @zolotyh

    email: aazolotyh@gmail.com

    https://zolotyh.github.io/refactoring2/