Главная страница
Навигация по странице:

  • Чем рефакторинг отличается от оптимизации

  • Когда нужно срочно улучшать код

  • Имена переменных, функций или классов не передают их назначение.

  • Слишком длинные функции и методы.

  • Слишком длинные классы.

  • Слишком длинный список параметров функции или метода.

  • Много комментариев.

  • В чём опасности рефакторинга

  • рефакторинг. Зачем нужен рефакторинг. Зачем нужен рефакторинг


    Скачать 26.38 Kb.
    НазваниеЗачем нужен рефакторинг
    Анкоррефакторинг
    Дата04.04.2023
    Размер26.38 Kb.
    Формат файлаdocx
    Имя файлаЗачем нужен рефакторинг.docx
    ТипДокументы
    #1036500

    Зачем нужен рефакторинг

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

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

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

    • сохранить архитектуру проекта, не допустить потери структурированности;

    • упростить будущую жизнь разработчиков, сделать код понятным и прозрачным для всех членов команды;

    • ускорить разработку и поиск ошибок.

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

    Поэтому даже идеальная когда-то программа со временем требует нового рефакторинга, обновляющего устаревшие участки кода.

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

    Чем рефакторинг отличается от оптимизации

    Рефакторинг — не оптимизация, хотя и может быть с нею связан. Часто его проводят одновременно с оптимизацией, поэтому понятия кажутся синонимами. Но у этих процессов разные цели.

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

    После рефакторинга программа может начать работать быстрее, но главное — её код становится проще и понятнее.

    Когда нужно срочно улучшать код

    Признаки, показывающие, что назрела необходимость в рефакторинге:

    • Программа работает, но даже небольшие доработки сильно затягиваются из-за того, что каждый раз приходится долго разбираться в коде.

    • Разработчик постоянно не может точно сказать, сколько времени ему нужно на выполнение задачи, потому что “там надо вначале разбираться”.

    • Одинаковые изменения приходится вносить в разные места текста программы.

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

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

    Как делают рефакторинг

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

    1. Мёртвый код. Переменная, параметр, метод или класс больше не используются: требования к программе изменились, но код не почистили. Мёртвый код может встретиться и в сложной условной конструкции, где какая-то ветка никогда не исполняется из-за ошибки или изменения требований. Такие элементы или участки текста нужно удалить.

    2. Дублирование. Один и тот же код выполняет одно и то же действие в нескольких местах программы. Вынесите эту часть в отдельную функцию.

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

      Примеры корректных имен: totalScore — переменная, означающая итоговый счёт в игре, maxWeight — максимальный вес. Для функций и методов лучше использовать глаголы, например: saveScore () — сохранить счет, setSize () — задать размер, getSpeed () — получить скорость.

    4. Слишком длинные функции и методы. Оптимальный размер этих элементов — 2-3 десятка строк. Если получается больше, разделите функцию на несколько маленьких и добавьте одну общую. Пусть маленькие выполняют по одной операции, а общая функция их вызывает.

    5. Слишком длинные классы. То же самое. Оптимальная длина класса — 20–30 строк. Разбейте длинный класс на несколько маленьких и включите их объекты в один общий класс.

    6. Слишком длинный список параметров функции или метода. Они только запутывают, а не помогают. Если все эти параметры действительно нужны, вынесите их в отдельную структуру или класс с понятным именем, а в функцию передайте ссылку на него.

    7. Много комментариев. Плохой код часто прикрывается обильными комментариями. Если почувствовали желание пояснить какой-то участок кода, попробуйте сначала его переписать, чтобы и так стал понятным. Бесполезные комментарии загромождают программу, а устаревшие и неактуальные вводят в заблуждение.

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

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

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

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

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

    В чём опасности рефакторинга

    Мы всё-таки меняем рабочий код. Тут можно не только всё упростить, но и сильно напортачить. Небрежный рефакторинг может отбросить выполнение проекта на дни и недели.

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

    Рефакторьте постоянно и по чуть-чуть.

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

    Пример рефакторинга кода на C#


    Давайте рассмотрим на примере.

    Задача:
    В файл ODBC.INI добавить настройки принтера для устанавливаемой программы. Если настройки для данной программы уже имеются, то заменить их.
    Имеется решение:

    void InstallDriver(string drive)
    {
        string PathToODBCINI = Environment.GetEnvironmentVariable("windir",  EnvironmentVariableTarget.Machine) + @"\ODBC.INI";
        if (!File.Exists(PathToODBCINI))
        {
           try
           {
              File.Create(PathToODBCINI).Close();
           }
           catch (Exception e)
           {
              MessageBox.Show(e.Message);
           }
        }
        StreamReader sr = new StreamReader(PathToODBCINI,    System.Text.ASCIIEncoding.Default);
        string content = sr.ReadToEnd();
        sr.Close();
        int index = content.IndexOf("[ODBC Data Sources]");
        if (index >= 0)
        {
           int lastIndex = content.IndexOf("QEWSD=34751", index) + 11;
           try
           {
               content = content.Remove(index, lastIndex - index);
           }
           catch
           {
               MessageBox.Show(ERROR_WRONG_PREVIOUS_INSTALLATION);
           }
        };
        string path_to_driver = Helpers.AppExecFolder + "files\\driver.txt";
        if (File.Exists(path_to_driver))
        {
            sr = new StreamReader(path_to_driver);
            string driver_text = sr.ReadToEnd();
            driver_text = driver_text.Replace("{1}", drive);
            sr.Close();
            try
            {
                StreamWriter sw = new StreamWriter(File.OpenWrite(PathToODBCINI), System.Text.ASCIIEncoding.Default);
                string config = String.Format("{0}{1}", driver_text, content);
                sw.Write(config);
                sw.Close();
                MessageBox.Show("Installation completed");
            }
            catch
            {
                MessageBox.Show(e1.Message);
            }
         }
         else
         {
            MessageBox.Show(String.Format("Cannot find file {0}", path_to_driver));
         }
    }

    Один метод делает всё... Хороший пример того, как не надо писать код. Сколько разных запахов тут смешалось воедино...
    Давайте рассмотрим, что делает этот код, в виде последовательности действий:
    0. Находит (или создает, если файл не существует) ODBC.INI
    1. Получает содержимое ODBC.INI
    2. Проверяет наличие настроек для программы и удаляет их
    3. Формирует новые настройки
    4. Записывает новые настройки в ODBC.INI
    Давайте для начала разобъём этот метод на 5 отдельных методов в соответствии с выделенными действиями. Что получается:

    string GetODBCINIPath()
    {
        string _PathToODBCINI = Environment.GetEnvironmentVariable("windir", EnvironmentVariableTarget.Machine) + @"\ODBC.INI";
        if (!File.Exists(_PathToODBCINI))
        {
            File.Create(_PathToODBCINI).Close();
        }
        return _PathToODBCINI;
    }
    string GetODBCINIContent(string PathToODBCINI)
    {
        using (StreamReader sr = new StreamReader(PathToODBCINI, System.Text.ASCIIEncoding.Default))
        {
            string content = sr.ReadToEnd();
            return content;
        }
    }
    string GetClearDriverSettings(string commonSettings)
    {
        string clearSettings = commonSettings;
        int index = commonSettings.IndexOf("[ODBC Data Sources]");
        if (index >= 0)
        {
            int lastIndex = commonSettings.IndexOf("QEWSD=34751", index) + 11;
            clearSettings = commonSettings.Remove(index, lastIndex - index);
        };
        return clearSettings;
    }
    string MakeNewSettings(string drive, string oldSettings)
    {
        string driver_text = String.Empty;
        string path_to_driver = Path.Combine(Helpers.AppExecFolder, "files\\driver.txt");
        if (File.Exists(path_to_driver))
        {
           using(StreamReader sr = new StreamReader(path_to_driver))

           {
              string driver_text = sr.ReadToEnd();
              driver_text = driver_text.Replace("{1}", drive);
           }
        }
        string newSettings = String.Concat(driver_text, oldSettings);
        return newSettings;
    }
    bool SetNewDriverSettings(string new_settings, string PathToODBCINI)
    {
        bool result = true;

        using (StreamWriter sw = new StreamWriter(File.OpenWrite(PathToODBCINI), System.Text.ASCIIEncoding.Default))
        {
           try
           {
               sw.Write(new_settings);
           }
           catch
           {
               result = false;
           }
        }
        return result;
    }
    void InstallDriver(string drive)
    {
        string PathToODBCINI = GetODBCINIPath();
        string commonSettings = GetODBCINIContent(PathToODBCINI);
        string clearSettings = GetClearDriverSettings(commonSettings);
        string newSettings = MakeNewSettings(drive, clearSettings);
        if (SetNewDriverSettings(newSettings, PathToODBCINI))
        {
            MessageBox.Show("Installation completed");
        }
        else
        {
            MessageBox.Show("Error");
        }
    }

    Теперь это выглядит немного лучше, но всё еще плохо. В глаза бросается PathToODBCINI, который используется в нескольких методах. Если присмотреться к этим методам, то мы увидим, что всех их объединяет работа с ODBC.INI, то есть логичнее будет выделить эти методы в отдельный класс. Заодно перенесем туда и назойливый PathToODBCINI. Получим:


    class OdbcIniProvider
    {
        private string PathToODBCINI;

        public OdbcIniProvider()
        {
            PathToODBCINI = GetODBCINIPath();
        }

        private string GetODBCINIPath()
        {
            string _PathToODBCINI = Environment.GetEnvironmentVariable("windir", EnvironmentVariableTarget.Machine) + @"\ODBC.INI";
            if (!File.Exists(_PathToODBCINI))
            {
                File.Create(_PathToODBCINI).Close();
            }
            return _PathToODBCINI;
        }

        public string GetODBCINIContent()
        {
           using (StreamReader sr = new StreamReader(PathToODBCINI, System.Text.ASCIIEncoding.Default))
           {
               return sr.ReadToEnd();
           }
        }

        public bool SetNewDriverSettings(string new_settings)
        {
           bool result = true;

           using (StreamWriter sw = new StreamWriter(File.OpenWrite(PathToODBCINI), System.Text.ASCIIEncoding.Default))
           {
               try
               {
                   sw.Write(new_settings);
               }
               catch
               {
                   result = false;
               }
           }
           return result;
        }
    }
    ...
    void InstallDriver(string drive)
    {
       OdbcIniProvider odbcIniProvider = new OdbcIniProvider();
       string commonSettings = odbcIniProvider.GetODBCINIContent();
       string clearSettings = GetClearDriverSettings(commonSettings);
       string newSettings = MakeNewSettings(drive, clearSettings);
       if (odbcIniProvider.SetNewDriverSettings(newSettings))
       {
          MessageBox.Show("Installation completed");
       }
       else
       {
          MessageBox.Show("Error");
       }
    }

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

    class SettingsManager
    {
        public SettingsManager(){}

        public string GetClearDriverSettings(string commonSettings)
        {
            string clearSettings = commonSettings;
            int index = commonSettings.IndexOf("[ODBC Data Sources]");
            if (index >= 0)
            {
               int lastIndex = commonSettings.IndexOf("QEWSD=34751", index) + 11;
               clearSettings = commonSettings.Remove(index, lastIndex - index);
            };
            return clearSettings;
        }
        public string MakeNewSettings(string drive, string oldSettings)
        {
            string driver_text = String.Empty;
            string path_to_driver = Path.Combine(Helpers.AppExecFolder, "files\\driver.txt");
            if (File.Exists(path_to_driver))
            {
               using(StreamReader sr = new StreamReader(path_to_driver))

               {
                  string driver_text = sr.ReadToEnd();
                  driver_text = driver_text.Replace("{1}", drive);
               }
            }
            string newSettings = String.Concat(driver_text, oldSettings);
            return newSettings;
        }
    }

    Коренным образом ситуация не улучшилась. Мне совершенно не нравится, как взаимодействуют OdbcIniProvider и SettingsManager. Кроме того, SettingsManager имеет слишком много открытых методов, о которых вызывающему коду совершенно не обязательно знать. По сути, вызывающему коду достаточно одного метода SetNewSettings(string drive), т.е. :

    class SettingsManager
    {
       ...
       public bool SetNewSettings(string drive)
       {
          OdbcIniProvider odbcIniProvider = new OdbcIniProvider();
          string commonSettings = odbcIniProvider.GetODBCINIContent();
          string clearSettings = GetClearDriverSettings(commonSettings);
          string newSettings = MakeNewSettings(drive, clearSettings);
          return odbcIniProvider.SetNewDriverSettings(newSettings);
       }
    }
    ...
    void InstallDriver(string drive)
    {
        SettingsManager settingsManager = new SettingsManager();

        if (settingsManager.SetNewSettings(drive))
        {
           MessageBox.Show("Installation completed");
        }
        else
        {
           MessageBox.Show("Error");
        }
    }

    Уже лучше, но такой подход тоже имеет свои недостатки. Например, что, если придется менять настройки не в файле, а в реестре? Придется переписывать SettingsManager, добавляя в него новый метод либо еще что-то в таком же духе... Давайте избавимся от такого рода зависимости сразу же. Для этого выделим интерфейс ISettingsProvider для классов, которые будут отвечать за конкретные способы получения и установки настроек:

    interface ISettingsProvider
    {
        string GetSettings();
        bool SetSettings(string new_settings);
    }

    И сразу же перепишем наш OdbcIniProvider под этот интерфейс:

    class OdbcIniProvider : ISettingsProvider
    {
        public string GetSettings() {...}
        public bool SetSettings(string new_settings) {...}
    }

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

    class SettingsManager
    {
        private ISettingsProvider settingsProvider;
        public void SetSettingsProvider(ISettingsProvider _settingsProvider)
        {
           settingsProvider = _settingsProvider;
        }
    ...
    }

    В итоге, получилось следующее:

    class SettingsManager
    {...
        public bool SetNewSettings(string drive)
        {
           string commonSettings = settingsProvider.GetSettings();
           string clearSettings = GetClearDriverSettings(commonSettings);
           string newSettings = MakeNewSettings(drive, clearSettings);
           return settingsProvider.SetSettings(newSettings);
        }
    }
    ...
    void InstallDriver(string drive)
    {
        SettingsManager settingsManager = new SettingsManager();
        settingsManager.SetSettingsProvider(new OdbcIniProvider());
        if (settingsManager.SetNewSettings(drive))
        {
            MessageBox.Show("Installation completed");
        }
        else
        {
           MessageBox.Show("Error");
        }
    }


    написать администратору сайта