Как сделать рефакторинг? Или как писать совершенный код? Большинство тех, кто задумываются над данным вопросом, знакомятся с книгой (скачать) Мартина Фаулера, «Рефакторинг». Итак, читаем о том, как книга была глубоко переработана и нашла свое применение в жизни.

Зачем? Когда и как?

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

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

  1. Дублирование кода.
  2. Длинный метод.
  3. Большой класс.
  4. Длинный список параметров.
  5. Расходящиеся модификации.
    Если при добавлении нового функционала приходится модифицировать несколько методов и значительную часть кода в классе.
  6. Стрельба дробью.
    Если при добавлении нового функционала приходится вносить одинаковые изменения в большое число классов.
  7. Завистливые функции.
    Метод интересуется больше не тем классом, в котором находится, а другим.
  8. Группы данных.
    Похожие группы данных, в разных частях кода.
  9. Одержимость элементарными типами.
  10. Операторы типа switch.
    Не забываем про ООП.
  11. Параллельные иерархии наследования.
    Дублированный код.
  12. Ленивый класс.
    Неиспользуемый или содержащий мало методов (оставшийся после рефакторинга/проектирования).
  13. Теоретическая общность.
    Переизбыток абстракциями тоже вреден.
  14. Временное поле.
    Если у класса есть переменные, которые используются в одном методе из пяти, то лучше эту переменную передавать в тот самый метод через параметр, а не через конструктор или какой другой способ.
  15. Цепочка сообщений.
    Глубокая последовательность вызовов до необходимой информации, через объекты по иерархии классов.
  16. Посредник.
    Большую часть методов класс делегирует другому классу.
  17. Неуместная близость.
    Классы не должны выставлять наружу закрытые части, т.е. внутреннюю кухню. При наследовании, подклассы должны знать минимум необходимой информации о родителе.
  18. Альтернативные классы с разными интерфейсами.
    Дублирование логики.
  19. Неполнота библиотечного класса.
    Не бойтесь расширять функционал библиотечных классов: extension методы или декорировать объект библиотечного класса.
  20. Классы данных.
    Делите на логические единицы. Доступ на изменение данных должен быть осмысленным.
  21. Отказ от наследства.
    Если наследнику нужна лишь малая часть информации (данных, методов) о родителе.
  22. Комментарии.
    Свидетельство кода с «запахом», нужно рефакторить. Возможно код остаётся на будущий рефакторинг или описывает сложные манипуляции

Я не привожу решения этих проблем, поскольку за базу можно взять, то что описано в книге. А с учётом опыта, опишу что-то своё. Что это даст? Простоту в поддержке и понимании кода, а также в написании тестов. Синтаксис будет на C#, но главное понимать идею, а её можно использовать в любом другом языке программирования.

Составление методов

  1. Выделение метода (преобразуйте фрагмент кода в метод, название которого объясняет его значение).
    Длинный метод разбиваем на логические под-методы.
    From
    void PrintUserInfo(decimal amount)
    {
        PrintParentInfo();
    
        Console.WriteLine(string.Format("имя: {0}", name);
        Console.WriteLine(string.Format("возраст: {0}", age);
        Console.WriteLine(string.Format("кол-во: {0}", amount);
    }
    

    To
    void PrintUserInfo(decimal amount)
    {
        PrintParentInfo();
        PrintUserDetails(decimal amount);
    }
    void PrintUserDetails(decimal amount)
    {
        Console.WriteLine(string.Format("имя: {0}", name);
        Console.WriteLine(string.Format("возраст: {0}", age);
        Console.WriteLine(string.Format("кол-во: {0}", amount);
    }
    
  2. Встраивание метода (поместите тело метода в код, который его вызывает и удалите метод).
    Излишняя косвенность (разбивка на методы) может усложнить класс.
    From
    int GetPoints()
    {
        return HasMaxSum() ? decimal.One : decimal.Zero;
    }
    bool HasMaxSum()
    {
        return summ >= maxSumm;
    }
    

    To
    int GetPoints()
    {
        return summ >= maxSumm ? decimal.One : decimal.Zero;
    }
    
  3. Встраивание временной переменной (замените этим выражением, все ссылки на данную переменную).
    Лишний промежуточный код.
    From
    decimal cost = order.GetCost();
    return cost > 1000;
    

    To
    return order.GetCost() > 1000;
    
  4. Замена временной переменной вызовом метода (преобразуйте выражение в метод).
    Метод может вызываться в любом месте класса, если таких мест много.
    From
    decimal MethodA()
    {
        decimal summ = amount * cost;
        if(summ > 1000)
        {
            //do something
            return summ * 10;
        }
        return 0;
    }
    decimal MethodB()
    {
        //do something
    
        decimal summ = amount * cost;
    
        return summ != 0 ? summ * 100  : 1;
    }
    

    To
    decimal MethodA()
    {
        decimal summ = GetSumm();
        if(summ  > 1000)
        {
            //do something
            return summ * 10;
        }
        return 0;
    }
    decimal MethodB()
    {
        //do something
    
        return GetSumm() != 0 ? GetSumm() * 100  : 1;
    }
    
    decimal GetSumm()
    {
        return amount * cost;
    }
    
  5. Введение поясняющей переменной (поместите результат выражения или его части во временную переменную).
    Упрощается чтение и понимание кода.
    From
    if(VisualItems.ColumnCount == FocusedCell.X && (key == Keys.Alt | Keys.Shift) && WasInitialized() && (resize > 0))
    {
        // do something
    }
    

    To
    bool isLastFocusedColumn = VisualItems.ColumnCount == FocusedCell.X;
    bool altShiftPressed = (key == Keys.Alt | Keys.Shift);
    bool wasResized = resize > 0;
    
    if(isLastFocusedColumn && altShiftPressed && WasInitialized() && wasResized)
    {
        // do something
    }
    
  6. Расщепление поясняющей переменной (создайте для каждого присвоения отдельную временную переменную).
    Упрощается чтение и понимание кода.
    From
    decimal temp = 2 * (height + width);
    Console.WriteLine(temp);
    temp = height * width;
    Console.WriteLine(temp);
    

    To
    decimal perimetr = 2 * (height + width);
    Console.WriteLine(perimetr);
    decimal area = height * width;
    Console.WriteLine(area);
    
  7. Удаление присвоений параметрам (лучше воспользоваться временной переменной).
    Методы не должны менять значения входящих параметров, если это явно не указано (например out, ref в C#).
    From
    int Discount(int amount, bool useDefaultDiscount, DateTime date)
    {
        if(amount == 0 && useDefaultDiscount)
        {
            amount = 10;
        }
        return amount;
    }
    

    To
    int Discount(int amount, bool useDefaultDiscount, DateTime date)
    {
        int result = amount;
        if(amount == 0 && useDefaultDiscount)
        {
            result = 10;
        }
        return result;
    }
    
  8. Замена метода объектом методов (преобразуйте метод в отдельный объект).
    Декомпозиция класса для гибкости кода.
    From
    class MessageSender
    {
        public Send(string title, string body)
        {
            // do something
            // long calculation of some condition
    
            if(condition)
            {
                // sending message            
            }
        }
    }
    

    To
    class MessageSender
    {
        public Send(string title, string body, decimal percent,  int quantity)
        {
            // do something
            Condition condition = new Condition (percent, quantity, dateTime);
    
            if(condition.Calculate())
            {
                // sending message            
            }
        }
    }
    
    class Condition
    {
        public Condition(decimal percent, int quantity, DateTime dt)
        {
        }
    
        public bool Calculate()
        {
            // long calculation of some condition
        }
    }
    
  9. Замещение алгоритма (замените алгоритм на более понятный).
    Оптимизация кода для лучшей читаемости и/или производительности.
    From
    string FoundPerson(List<string> people)
    {
        foreach(var person in people)
        {
            if(person == "Max")
            {
                 return "Max";
            } 
            else if(person == "Bill")
            {
                 return "Bill";
            }
            else if(person == "John")
            {
                 return "John";
            }
            return "";
        }
    }
    

    To
    string FoundPerson(List<string> people)
    {
        var candidates = new List<string>{ "Max",  "Bill", "John"};
        foreach(var person in people)
        {
            if(candidates.Contains(person))
            {
                return person;
            }
        }
        return "";
    }
    

 

Немного реальности и процедуре code review.


Перед рефакторингом или для проверки внесенных изменения применяется процедура code review.
В данном случае, речь пойдёт о «замечаниях» (критериях) review-ера.

  • Необходимо обязательно поправить.
    Как правило, это очевидные вещи, которые могут приводить: к исключениям (например отсутствие проверок на null), к просадке производительности, к изменению логики работы программы.
    Сюда же можно отнести и архитектурные штрихи. Например если у вас весь проект написан на MVC/MVP, а новый коллега добавил форму, заколбасив всё в один файл/класс. Чтобы не создавать прецедент и следовать правилам, конечно лучше этот спагетти-код переделать под общую архитектуру.
  • Замечания-вопросы.
    Если вам, что-то не понятно, не стесняйтесь задавать вопросы: почему именно такое решение, почему выбран такой алгоритм, зачем такие навороты. Вопросы полезны как для общего развития, можно освоить полезную фичу или лучше узнать текущий код. Так и для понимания того, что коллега выбрал решение осознанно.
  • Замечания-предложения.
    Тут высказываются предложения по улучшению кода, но если code commiter проигнорирует их, то я не буду настаивать.
    Если же мне как code commiter-у что-то предлагают, я чаще стараюсь сделать это. Тут я исхожу из того соображения, что review-ер это преподаватель, который проверяет код и он прав, конечно если предложения не доходит до абсурда.
    Что-то предлагая, можно это аргументировать кодом для наглядности.


Реальность такова, что грань между этими критериями может быть разная для каждого программиста, но это терпимо — нормально. Хуже когда у review-ера отсутствует 3-й критерий, т.е. он всегда прав, а если не прав, то докажите ему это (но не всё доказательно, всё-таки не математика). Становясь уже code commiter-ом он наоборот, только и применяет 3-ий критерий.
А когда сталкиваются два таких человека, это флейм, крики (видел и с матерком :)).
Чтобы разрешить конфликт, нужна 3 сила, как правило другой программист (желательно с большим опытом) должен высказать своё мнение.

http://habrahabr.ru/post/173903/

Вверх