Рефакторинг Определение • Рефакторинг или рефакторирование (refactoring) — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы. • В основе рефакторинга лежит последовательность небольших эквивалентных преобразований. Цели • Цель рефакторинга — сделать код программы легче для понимания; без этого рефакторинг нельзя считать успешным. • Рефакторинг следует отличать от оптимизации производительности. Как и рефакторинг, оптимизация обычно не изменяет поведение программы, а только ускоряет её работу. Но оптимизация часто затрудняет понимание кода, что противоположно рефакторингу. • С другой стороны, нужно отличать рефакторинг от реинжиниринга, который осуществляется для расширения функциональности программного обеспечения. Как правило, крупные рефакторинги предваряют реинжиниринг. Причины применения • Необходимо добавить новую функцию, которая недостаточно укладывается в принятое архитектурное решение; • Необходимо исправить ошибку, причины возникновения которой сразу не ясны; • Преодоление трудностей в командной разработке, которые обусловлены сложной логикой программы. Рефакторинги Введение объекта Null Формирование шаблона метода Замена делегирования наследованием Замена подкласса полями Разделение запроса и модификатора Удаление посредника Сохранение всего объекта Замена локального расширения методами расширения (Replacing Extension Wrapper with Extension Method) Введение объекта Null Применимость Есть многократные проверки совпадения значения одного типа на null. Краткое описание Создать нулевую реализацию объекта и заменить null-ссылки экземпляром этого объекта. Введение объекта Null Техника Создайте подкласс исходного класса, который будет выступать как нулевая версия исходного класса. Создайте метод isNull, который возвращает true или false в нулевом и исходном классах соответственно. Найдите все места, где при запросе исходного объекта возвращается null и отредактируйте их так, чтобы вместо этого возвращался нулевой объект. Найдите и замените все сравнения на null вызовом метода isNull. Используйте утверждения для проверки валидности объектов. Замените для каждого их этих случаев операции в нулевом классе альтернативным поведением. Удалите проверку условия в тех местах, где используется перегруженное поведение. Введение объекта Null Пусть есть сущность участка с пользователем: public class Site { Customer customer; public Customer Customer { get { return customer; } } } public class Customer { public string Name { get; } public BillingPlan GetBillingPlan(); public PaymentHistory GetHistory(); } Введение объекта Null Варианты использования: Customer customer = new Customer(); BillingPlan plan; if (customer == null) plan = BillingPlan.GetBasicPlan(); else plan = customer.GetBillingPlan(); String customerName; if (customer == null) customerName = "occupant"; else customerName = customer.Name; int weeksDelinquent; // Количество неоплаченных недель if (customer == null) weeksDelinquent = 0; else weeksDelinquent = customer.GetHistory().GetWeeksDelinquentLastYear(); Введение объекта Null Создадим нулевой объект и метод IsNull: public class Customer { public virtual bool IsNull() { return false; } } public class NullCustomer : Customer { public override bool IsNull() { return true; } } Введение объекта Null Создадим фабричный метод, возвращающий экземпляр нулевого объекта, и заменим места, где может быть возвращен нулевой указатель: public class Customer { public static Customer CreateNullCustomer() { return new NullCustomer(); } } public class Site { public Customer Customer { get { if (customer == null) return Customer.CreateNullCustomer(); else return customer; } } } Введение объекта Null Заменим проверку на нулевой указатель вызовом метода IsNull: BillingPlan plan; if (customer.IsNull()) plan = BillingPlan.GetBasicPlan(); else plan = customer.GetBillingPlan(); String customerName; if (customer.IsNull()) customerName = "occupant"; else customerName = customer.Name; int weeksDelinquent; if (customer.IsNull()) weeksDelinquent = 0; else weeksDelinquent = customer.GetHistory().GetWeeksDelinquentLastYear(); Введение объекта Null Теперь можно заменить реализации методов в нулевом объекте: public class NullCustomer : Customer { public override string Name { get { return "occupant"; } } public override BillingPlan GetBillingPlan() { return BillingPlan.GetBasicPlan(); } } Введение объекта Null В результате избавляемся от проверок на клиенте: Customer customer = new Customer(); BillingPlan plan; plan = customer.GetBillingPlan(); String customerName; customerName = customer.Name; Введение объекта Null Очень часто оказывается, что одни нулевые объекты возвращают другие. Например, в данном случае нам необходимо создать нулевую реализацию сущности PaymentHistory: public class PaymentHistory { public static PaymentHistory CreateNullPaymentHistory() { return new NullPaymentHistory(); } } public class NullPaymentHistory : PaymentHistory { public override int GetWeeksDelinquentLastYear() { return 0; } } Введение объекта Null public class NullCustomer : Customer { public override PaymentHistory GetHistory() { return PaymentHistory.CreateNullPaymentHistory(); } } В результате удаляем последнюю проверку в клиентском коде: int weeksDelinquent; weeksDelinquent = customer.GetHistory().GetWeeksDelinquentLastYear(); Формирование шаблона метода Применимость Есть два метода в подклассах, выполняющие аналогичные шаги в одинаковом порядке, однако эти шаги различны. Краткое описание Образовать из аналогичных шагов методы с одинаковой сигнатурой, чтобы исходные методы стали одинаковыми. После этого поместить их в родительский класс. Формирование шаблона метода Техника Выполните декомпозицию методов, чтобы выделенные методы полностью совпадали или полностью отличались. С помощью «Подъѐма метода» переместите идентичные методы в родительский класс. Переименуйте различающиеся методы так, чтобы сигнатуры всех методов на каждом шаге были одинаковы. Примените «Подъѐм метода» к одному из исходных методов. Определите их сигнатуры как абстрактные методы родительского класса. Формирование шаблона метода Пусть есть класс Customer, в котором есть методы вывода информации о счѐте ― в текстовом виде и в виде HTML: public class Customer { public string GetStatement() { string result = "Учёт аренды для " + Name + "\n"; foreach (Rental rental in Rentals) result += "\t" + rental.Movie.Title + "\t" + rental.Charge.ToString() + "\n"; result += "Сумма задолженности составляет: " + GetTotalCharge().ToString() + "\n"; result += "Вы заработали " + GetTotalFrequentRenterPoints().ToString() + " очков за активность"; return result; } } Формирование шаблона метода public class Customer { public string GetHTMLStatement() { string result = "<H1>Учёт аренды для <EM>" + Name + "</EM></H1><P>"; foreach (Rental rental in Rentals) result += rental.Movie.Title + " " + rental.Charge.ToString() + "<BR>"; result += "<P>Сумма задолженности составляет <EM>" + GetTotalCharge().ToString() + "</EM><BR><P>"; result += "Вы заработали <EM>" + GetTotalFrequentRenterPoints().ToString() + "</EM> очков за активность"; return result; } } Формирование шаблона метода Эти методы должны появиться в подклассах некоторого общего родительского класса. Для этого создадим классы TextStatement и HTMLStatement и делегируем им работу: public class Customer { public string GetStatement() { return (new TextStatement()).GetValue(this); } public string GetHTMLStatement(Customer customer) { return (new HTMLStatement()).GetValue(this); } } public class TextStatement : Statement { public string GetValue(Customer customer); } public class HTMLStatement : Statement { public string GetValue(Customer customer); } Формирование шаблона метода Реализация метода GetValue для класса TextStatement: public class TextStatement : Statement { public string GetValue(Customer customer) { string result = "Учёт аренды для " + customer.Name + "\n"; foreach (Rental rental in customer.Rentals) result += "\t" + rental.Movie.Title + "\t" + rental.Charge.ToString() + "\n"; result += "Сумма задолженности составляет: " + customer.GetTotalCharge().ToString() + "\n"; result += "Вы заработали " + customer.GetTotalFrequentRenterPoints().ToString() + " очков за активность"; return result; } } Формирование шаблона метода Теперь выделим вывод заголовка в методы с одинаковой сигнатурой. В классе TextStatement: public class TextStatement : Statement { private string GetHeaderString(Customer customer) { return "Учёт аренды для " + customer.Name + "\n"; } public string GetValue(Customer customer) { string result = GetHeaderString(customer); ... } } Формирование шаблона метода И в классе HTMLStatement: public class HTMLStatement : Statement { private string GetHeaderString(Customer customer) { return "<H1>Учёт аренды для <EM>" + customer.Name + "</EM></H1><P>"; } public string GetValue(Customer customer) { string result = GetHeaderString(customer); ... } } Формирование шаблона метода Аналогично поступим с остальными элементами счѐта: public class TextStatement : Statement { private string GetRentalString(Rental rental) { return "\t" + rental.Movie.Title + "\t" + rental.Charge.ToString() + "\n"; } private string GetFooterString(Customer customer) { return "<P>Сумма задолженности составляет <EM>" + customer.GetTotalCharge().ToString() + "</EM><BR><P>" + "Вы заработали <EM>" + customer.GetTotalFrequentRenterPoints().ToString() + "</EM> очков за активность"; } } Формирование шаблона метода public class HTMLStatement : Statement { private string GetRentalString(Rental rental) { return rental.Movie.Title + " " + rental.Charge.ToString() + "<BR>"; } private string GetFooterString(Customer customer) { return "<P>Сумма задолженности составляет <EM>" + customer.GetTotalCharge().ToString() + "</EM><BR><P>" + "Вы заработали <EM>" + customer.GetTotalFrequentRenterPoints().ToString() + "</EM> очков за активность"; } } Формирование шаблона метода Теперь методы GetValue в обоих классах идентичны: public class HTMLStatement : Statement { public string GetValue(Customer customer) { string result = GetHeaderString(customer); foreach (Rental rental in customer.Rentals) result += GetRentalString(rental); result += GetFooterString(customer); return result; } } Формирование шаблона метода Необходимо поднять метод GetValue в родительский класс. При подъѐме остальные методы надо объявить абстрактными: public class Statement { protected abstract string GetHeaderString(Customer customer); protected abstract string GetRentalString(Rental rental); protected abstract string GetFooterString(Customer customer); public string GetValue(Customer customer) { string result = GetHeaderString(customer); foreach (Rental rental in customer.Rentals) result += GetRentalString(rental); result += GetFooterString(customer); return result; } } Замена делегирования наследованием Применимость Вы используете слишком много простых делегирований в методах некоторого класса. Краткое описание Сделать делегирующий класс подклассом делегата. Замена делегирования наследованием Техника Сделайте делегирующий объект подклассом делегата. Заставьте поле делегирования ссылаться на сам объект. Удалите простые методы делегирования. Замените все другие делегирования обращениями к самому объекту. Удалите поле делегирования. Замена делегирования наследованием Пусть есть простой класс Employee, который делегирует всю работу классу Person: public class Employee { Person person = new Person(); public string Name { get { return person.Name; } set { person.Name = value; } } public override string ToString() { return "Emp: " + person.GetLastName(); } } Замена делегирования наследованием Класс Person: public class Person { private string name; public string Name { get { return name; } set { name = value; } } public string GetLastName() { return name.Substring(name.LastIndexOf(' ') + 1); } } Замена делегирования наследованием Объявляем Employee подклассом Person, а также заставляем поле делегирования ссылаться на сам объект: public class Employee : Person { Person person; public Employee() { person = this; } } Замена делегирования наследованием Затем удаляем простые методы делегирования. Вызовы методов делегирования заменяем на обычные вызовы. После этого можно удалить поле делегирования: public class Employee : Person { Person person; public string Name { get { return person.Name; } set { person.Name = value; } } public override string ToString() { return "Emp: " + GetLastName(); } } Замена делегирования наследованием Код после рефакторинга: public class Person { private string name; public string Name { get { return name; } set { name = value; } } public string GetLastName() { return name.Substring(name.LastIndexOf(' ') + 1); } } public class Employee : Person { public override string ToString() { return "Emp: " + GetLastName(); } } Замена подкласса полями Применимость Есть подклассы, которые различаются только методами, возвращающими данныеконстанты. Краткое описание Заменить методы полями в родительском классе и удалить подклассы. Замена подкласса полями Техника Примените к подклассам «Замену конструктора фабричным методом». Удалите ссылки на подклассы. Для каждого константного метода объявите соответствующие поля. Объявите защищенный конструктор для инициализации полей. Измените имеющиеся конструкторы так, чтобы они использовали новый конструктор. Реализуйте каждый константный метод так, чтобы он возвращал поле, и удалите метод из подкласса. Замена подкласса полями Пусть есть класс Person и подклассы, выделенные по полу: public abstract class Person { public abstract bool IsMale(); public abstract char GetCode(); } public class Male : Person { public override bool IsMale() { return true; } public override char GetCode() { return 'M'; } } Замена подкласса полями public class Female : Person { public override bool IsMale() { return false; } public override char GetCode() { return 'F'; } } Замена подкласса полями Первым шагом создаѐм фабричные методы: public abstract class Person { public static Person CreateMale() { return new Male(); } public static Person CreateFemale() { return new Female(); } } Заменяем вызовы конструкторов на вызовы фабричных методов, тем самым избавляясь от ссылок на подклассы в клиентском коде: Person person = new Male(); Person person = Person.CreateMale(); Замена подкласса полями Объявляем поля для каждого константного метода и создаѐм закрытый конструктор для инициализации: public abstract class Person { private bool isMale; private char code; protected Person(bool isMale, char code) { this.isMale = isMale; this.code = code; } } Замена подкласса полями Добавим в подклассы конструкторы, вызывающие новый конструктор базового класса: public class Male : Person { public Male() : base(true, 'M') { } } public class Female : Person { public Female() : base(false, 'F') { } } Замена подкласса полями Теперь поместим методы, возвращающие поля, в базовый класс и удалим соответствующие методы из подклассов: public abstract class Person { public bool IsMale() { return isMale; } } public class Male : Person { public override bool IsMale() { return true; } } Замена подкласса полями Теперь с класса Person можно снять пометку абстрактности, удалить подклассы и встроить вызов конструкторов в фабричные методы: public class Person { public static Person CreateMale() { return new Person(true, 'M'); } public static Person CreateFemale() { return new Person(false, 'F'); } } Разделение запроса и модификатора Применимость Есть метод, возвращающий значение, но, кроме того, изменяющий состояние объекта. Краткое описание Необходимо создать два метода — один для запроса и один для модификации. Разделение запроса и модификатора Техника Создайте запрос, возвращающий то же значение, что и исходный метод. Модифицируйте исходный метод так, чтобы он возвращал результат обращения к запросу. Для каждого вызова замените одно обращение к исходному методу вызовом запроса. Вызов запроса поместите после вызова исходного метода. Объявите тип возвращаемого значения void и удалите выражения return. Разделение запроса и модификатора Пусть есть функция, которая сообщает для системы безопасности имя злодея и посылает предупреждение: public string FoundMiscreant(List<string> people) { foreach (string man in people) { if (man == "Don") { SendAlert(); return "Don"; } if (man == "John") { SendAlert(); return "John"; } } return ""; } Функция используется следующим образом: private void CheckSecurity(List<string> people) { string found = FoundMiscreant(people); SomeLaterCode(found); } Разделение запроса и модификатора Первым шагом создадим функцию-запрос, которая возвращает то же значение, что и исходная функция, но не создаѐт побочных эффектов: public string FoundPeople(List<string> people) { foreach (string man in people) { if (man == "Don") return "Don"; if (man == "John") return "John"; } return ""; } Разделение запроса и модификатора Поочерѐдно заменим все операторы return в исходной функции на вызовы нового запроса: public string FoundMiscreant(List<string> people) { foreach (string man in people) { if (man == "Don") { SendAlert(); return FoundPeople(people); } if (man == "John") { SendAlert(); return FoundPeople(people); } } return FoundPeople(people); } Разделение запроса и модификатора Теперь изменим клиентский код так, чтобы происходило два вызова ― сначала модификатора, потом запроса: private void CheckSecurity(List<string> people) { FoundMiscreant(people); string found = FoundPeople(people); SomeLaterCode(found); } Разделение запроса и модификатора Установим тип возвращаемого значения исходной функции в void: public void FoundMiscreant(List<string> people) { foreach (string man in people) { if (man == "Don") { SendAlert(); return; } if (man == "John") { SendAlert(); return; } } return; } Разделение запроса и модификатора Теперь можно переименовать функцию-модификатор и устранить излишнее дублирование: public void SendAlert(List<string> people) { if (FoundPeople(people) != "") SendAlert(); } Клиентский код после рефакторинга: private void CheckSecurity(List<string> people) { SendAlert(people); string found = FoundPeople(people); SomeLaterCode(found); } Удаление посредника Применимость Класс занят слишком простым делегированием. Краткое описание Необходимо заставить клиента обращаться к делегату непосредственно. Удаление посредника Техника Создайте метод доступа к делегату. Для каждого случая использования клиентом метода-посредника замените его вызов на обращение к делегату. Удалите метод посредник. Удаление посредника Пусть есть класс Person, который скрывает делегирование к Department: public class Person { private Department department; public Person Manager { get { return department.Manager; } } } public class Department { private Person manager; public Department(Person manager) { this.manager = manager; } public Person Manager { get { return manager; } } } Удаление посредника Чтобы узнать, кто является менеджером некоторого лица, клиент делает запрос: Person john; manager = john.Manager; В первую очередь создаѐм метод доступа к делегату: public class Person { private Department department; public Department Department { get { return department; } } } Удаление посредника После этого необходимо рассмотреть каждый метод, использующий этот метод Person, и изменить его так, чтобы он обращался к классу-делегату: manager = john.Department.Manager; Возможно, что одним клиентам нужно оставить методы-делегаты, а от других скрыть. В этом случае необходимо сохранить некоторые простые делегирования. Сохранение всего объекта Применимость Вы получаете от объекта несколько значений, которые затем передаѐте как параметры при вызове метода. Краткое описание Необходимо вместо значений передать объект полностью. Сохранение всего объекта Техника Создайте новый параметр для передачи всего объекта. Определите, какие параметры нужны от объекта в целом, и замените ссылки на него в теле метода вызовами метода объекта параметра. Удалите из вызывающего метода код, который получает удалѐнные параметры. Сохранение всего объекта Рассмотрим объект, представляющий помещение и регистрирующий самую высокую и самую низку температуру. Он сравнивает этот диапазон с заранее установленным планом обогрева: public class HeatingPlan { TempRange range; public bool WithinRange(int low, int high) { return (low >= range.Low && high <= range.High); } } public class Room { public bool WithinPlan(HeatingPlan plan) { int low = DaysTempRange.Low; int high = DaysTempRange.High; return plan.WithinRange(low, high); } } Сохранение всего объекта Вместо распаковки значений можно передать объект диапазона полностью. Сначала добавим к списку параметров объект, от которого будем получать значения: public class HeatingPlan { public bool WithinRange(TempRange roomRange, int low, int high) { return (low >= range.Low && high <= range.High); } } public class Room { public bool WithinPlan(HeatingPlan plan) { int low = DaysTempRange.Low; int high = DaysTempRange.High; return plan.WithinRange(DaysTempRange, low, high); } } Сохранение всего объекта Затем заменяем обращение к параметрам вызовом методов объекта-параметра и удаляем ненужные параметры: public class HeatingPlan { TempRange range; public bool WithinRange(TempRange roomRange, int low, int high) { return (roomRange.Low >= range.Low && roomRange.High <= range.High); } } Сохранение всего объекта Теперь временные переменные нам не нужны: public class Room { public bool WithinPlan(HeatingPlan plan) { int low = DaysTempRange.Low; int high = DaysTempRange.High; return plan.WithinRange(DaysTempRange); } } Сохранение всего объекта Такое применение объектов позволяет нам понять, что поведение можно поместить в сам объект: public class HeatingPlan { public bool WithinRange(TempRange roomRange) { return range.Includes(roomRange); } } Replacing Extension Wrapper with Extension Method Применимость Есть обѐрточный класс, который расширяет функциональность существующего посредством делегирования. Краткое описание Удалить класс обѐртку и реализовать дополнительную функциональсть в виде методов расширения. Методы расширения Методы расширения позволяют «добавлять» методы в существующие типы без создания нового производного типа, перекомпиляции или иного изменения исходного типа. Для клиентского кода, написанного на языке C#, нет видимого различия между вызовом метода расширения и вызовом методов, фактически определенных в типе. Методы расширения Методы расширения определяются как статические методы, но вызываются с помощью синтаксиса обращения к методу экземпляра. Их первый параметр определяет, с каким типом оперирует метод (такому параметру обязательно должен предшествовать модификатор this). В данном примере к классу String добавляется метод подсчѐта количества слов: public static class StringExtensions { public static int WordCount(this String str) { return str.Split( new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length; } } Пример использования: string s = "Hello Extension Methods"; int i = s.WordCount(); Replacing Extension Wrapper with Extension Method Техника Создать статический класс расширения и скопировать в него реализацию методов, расширяющих функциональность. В классе-обѐртке заменить реализацию расширенной функциональности делегированием методу расширения. Удалить класс-обѐртку, когда он будет содержать только делегирующие методы. Replacing Extension Wrapper with Extension Method Пусть есть класс, который расширяет функциональность класса SqlConnection следующим образом: public class EnhancedSqlConnection : IDbConnection { private SqlConnection wrapped; public EnhancedSqlConnection() { this.wrapped = new SqlConnection(); } public void RestoreDatabase(string backupFileName) { // ... implementation } public void Open() { wrapped.Open(); } public IDbTransaction BeginTransaction() { wrapped.BeginTransaction(); } } Replacing Extension Wrapper with Extension Method Создадим класс расширений и перенесѐм туда реализацию метода RestoreDatabase: public static class EnhancedSqlConnectionExtension { public static void RestoreDatabase( this SqlConnection connection, string backupFileName) { // ... implementation } } Replacing Extension Wrapper with Extension Method В исходном классе реализацию метода заменим на вызов метода расширения: public class EnhancedSqlConnection : IDbConnection { private SqlConnection wrapped; public EnhancedSqlConnection() { this.wrapped = new SqlConnection(); } public void RestoreDatabase(string backupFileName) { wrapped.RestoreDatabase(backupFileName); } } Повторив операцию для каждого метода расширения, получим класс, который содержит только делегирующие методы. После этого можно удалить исходный класс и создавать вместо него класс SqlConnection.