рефакторинг. Зачем нужен рефакторинг. Зачем нужен рефакторинг
Скачать 26.38 Kb.
|
Зачем нужен рефакторинг Стройный, хорошо структурированный код легко читается и быстро дорабатывается. Но редко удаётся сразу сделать его таким. Разработчики спешат, в процессе могут меняться требования к задаче, тестировщики находят баги, которые нужно быстро исправить, или возникают срочные доработки, и их приходится делать второпях. В результате даже изначально хорошо структурированный исходник становится беспорядочным и непонятным. Программисты знают, как легко завязнуть в этом хаосе. Причём неважно, чужой это код или собственный. Чтобы решить все эти проблемы, делается рефакторинг программы. В новом проекте он нужен, чтобы: сохранить архитектуру проекта, не допустить потери структурированности; упростить будущую жизнь разработчиков, сделать код понятным и прозрачным для всех членов команды; ускорить разработку и поиск ошибок. Но любое приложение со временем устаревает: язык программирования совершенствуется, появляются новые функции, библиотеки, операторы, делающие код проще и понятнее. То, что год назад требовало пятидесяти строк, сегодня может решаться всего одной. Поэтому даже идеальная когда-то программа со временем требует нового рефакторинга, обновляющего устаревшие участки кода. Программный код предназначен не только для компьютера, но и для человека, который будет его дорабатывать. Плохо, если ему придётся неделю разбираться в исходниках, чтобы изменить в программе несколько строк. И не исключено, что этим человеком окажетесь вы сами. Чем рефакторинг отличается от оптимизации Рефакторинг — не оптимизация, хотя и может быть с нею связан. Часто его проводят одновременно с оптимизацией, поэтому понятия кажутся синонимами. Но у этих процессов разные цели. Цель оптимизации — улучшение производительности программы, а рефакторинга — улучшение понятности кода. После оптимизации исходный код может стать сложнее для понимания. После рефакторинга программа может начать работать быстрее, но главное — её код становится проще и понятнее. Когда нужно срочно улучшать код Признаки, показывающие, что назрела необходимость в рефакторинге: Программа работает, но даже небольшие доработки сильно затягиваются из-за того, что каждый раз приходится долго разбираться в коде. Разработчик постоянно не может точно сказать, сколько времени ему нужно на выполнение задачи, потому что “там надо вначале разбираться”. Одинаковые изменения приходится вносить в разные места текста программы. Такой код нужно срочно рефакторить, иначе он будет тормозить реализацию проекта и затруднять внесение правок. Вообще рефакторинг нужно проводить постоянно. Делайте его каждый раз, после того как поменяли программу и убедились, что всё работает. Например, если добавили или изменили какую-то функцию, метод, класс или объявили новую переменную. Как делают рефакторинг Рефакторинг — это маленькие последовательные улучшения кода. Чистить можно всё, но в первую очередь найдите эти проблемы: Мёртвый код. Переменная, параметр, метод или класс больше не используются: требования к программе изменились, но код не почистили. Мёртвый код может встретиться и в сложной условной конструкции, где какая-то ветка никогда не исполняется из-за ошибки или изменения требований. Такие элементы или участки текста нужно удалить. Дублирование. Один и тот же код выполняет одно и то же действие в нескольких местах программы. Вынесите эту часть в отдельную функцию. Имена переменных, функций или классов не передают их назначение. Имена должны сообщать, почему элемент кода существует, что он делает и как используется. Если видите, что намерения программиста непонятны без комментария, — рефакторьте. Примеры корректных имен: totalScore — переменная, означающая итоговый счёт в игре, maxWeight — максимальный вес. Для функций и методов лучше использовать глаголы, например: saveScore () — сохранить счет, setSize () — задать размер, getSpeed () — получить скорость. Слишком длинные функции и методы. Оптимальный размер этих элементов — 2-3 десятка строк. Если получается больше, разделите функцию на несколько маленьких и добавьте одну общую. Пусть маленькие выполняют по одной операции, а общая функция их вызывает. Слишком длинные классы. То же самое. Оптимальная длина класса — 20–30 строк. Разбейте длинный класс на несколько маленьких и включите их объекты в один общий класс. Слишком длинный список параметров функции или метода. Они только запутывают, а не помогают. Если все эти параметры действительно нужны, вынесите их в отдельную структуру или класс с понятным именем, а в функцию передайте ссылку на него. Много комментариев. Плохой код часто прикрывается обильными комментариями. Если почувствовали желание пояснить какой-то участок кода, попробуйте сначала его переписать, чтобы и так стал понятным. Бесполезные комментарии загромождают программу, а устаревшие и неактуальные вводят в заблуждение. После каждой правки посмотрите на соседние участки кода: возможно, их тоже стоит поправить и сделать понятнее. И на те участки кода, которые давно не редактировались, — они уже могли стать некорректными. После каждого изменения программу надо тестировать, поэтому перед началом рефакторинга подготовьте комплект тестов: модульных, функциональных или интеграционных. Изменения при рефакторинге вносятся небольшие, так что ошибки обычно легко найти и исправить. Код чистят и на этапе тестирования, когда всё уже готово и проверяется работоспособность программы. Тут разработчик выполняет требования тестировщиков и одновременно проводит рефакторинг. Как правило, руководители проектов понимают важность рефакторинга и делают его элементом разработки. Особое место он занимает в экстремальном программировании, когда программисты попеременно то пишут код и разрабатывают тесты, то проводят рефакторинг написанного. Не страдайте перфекционизмом! Если вы поправили какой-то кусочек кода, не надо перетряхивать всю программу, разыскивая, что ещё можно улучшить. Стремление к совершенству вечно, но лучше обойтись без фанатизма. В чём опасности рефакторинга Мы всё-таки меняем рабочий код. Тут можно не только всё упростить, но и сильно напортачить. Небрежный рефакторинг может отбросить выполнение проекта на дни и недели. Опасно делать рефакторинг не постоянно, а от случая к случаю. Соблазн сильно улучшить код становится невыносимым. Вы всё глубже закапываетесь в программу и копаете себе яму, в которой легко увязнуть. Рефакторьте постоянно и по чуть-чуть. Иногда бывают злоупотребления: рефакторинг может стать способом саботажа, отговоркой, с помощью которой откладываются важные релизы и внедрение новых фич. Пример рефакторинга кода на 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"); } } |