Я уже писал в блоге о двух вариантах рефакторинга , которые NetBeans 7.2 предоставляет в моих удачно названных публикациях NetBeans 7.2: Рефакторинг параметризованного конструктора как построителя и NetBeans 7.2: Рефакторинг конструктора как статической фабрики . В этой статье я расскажу о другом варианте рефакторинга, представленном в NetBeans 7.2, который может наиболее экономить время из всех: ввести локальное расширение .
Martin Fowler «s Рефакторинг Главная страница включает в себя„ каталог общих рефакторинга“, в том числе ввести локальные Extension рефакторинга . Этот конкретный рефакторинг описан на этой странице следующим образом (представление проблемы и решения): «[Проблема] Для используемого класса сервера требуется несколько дополнительных методов, но вы не можете изменить класс. [Решение] Создайте новый класс, который содержит эти дополнительные методы. Сделайте этот класс расширения подклассом или оберткой оригинала. «
Одним из новых рефакторингов, доступных в NetBeans 7.2, является «Введение локального расширения», к которому можно легко получить доступ, щелкнув правой кнопкой мыши класс, для которого требуется предоставить локальное расширение, выбрав опцию «Refactor», чтобы развернуть раскрывающийся список. меню и выбрав «Ввести локальное расширение». В качестве альтернативы, можно просто выделить класс интереса и использовать клавиатуру: Alt + Shift + X . Первый (с использованием раскрывающихся меню в сочетании с правым и левым щелчками мыши) показан на следующем снимке экрана после наведения курсора на использование класса Java Date в исходном коде (который указан после снимка экрана).
На приведенном выше снимке экрана показан результат щелчка правой кнопкой мыши по типу Date, используемому, например, для переменной date в следующем листинге исходного кода.
package dustin.examples; import java.util.Date; /** * * @author Dustin */ public class Main { private Date date; /** * @param arguments the command line arguments */ public static void main(final String[] arguments) { } }
Когда рефакторинг «Ввести локальное расширение» выбран либо щелчком мыши, как показано выше, либо с помощью Alt + Shift + X, отображается экран мастера, подобный следующему.
В этом случае, поскольку я использовал Alt + Shift + X при наведении указателя мыши на тип данных Date, мастер готов помочь мне сгенерировать локальное расширение даты Java. Обратите внимание, что если бы я наводил курсор на большинство других частей этого класса, мастер вместо этого начал бы помогать мне создавать локальное расширение моего основного класса (класса, загруженного в окне редактора NetBeans).
В соответствии с приведенным ранее определением реализаций рефакторинга «Внедрение локального расширения», существует два способа, которые мастер NetBeans 7.2 допускает для этого: оболочка (композиция) и подтип (наследование реализации). Если выбран параметр «Обертка» (как в данном случае), то есть еще три варианта «Равенство» («Делегировать», «Создать» или «Отдельно»). Я вернусь к этим позже. А пока давайте предположим, что выбран «Подтип». На следующем снимке экрана показано, как остальные три параметра больше не применяются и отображаются серым цветом.
Если в этот момент я нажму кнопку «Refactor», мой новый класс также будет называться Date, но будет dustin.examples.Date, а не java.util.Date. В этом случае, чтобы избежать путаницы, я собираюсь изменить имя сгенерированного класса на DustinDate.
Также стоит отметить, что есть флажок, который позволяет мне указать, хочу ли я заменить исходный класс (в данном случае, Дата) с измененным локальным расширением. Другими словами, я могу заставить рефакторинг не только создать локальное расширение подтипа, но и заставить NetBeans заменить использование Date в исходном коде Main на новый класс. Это показано на следующем снимке экрана.
На следующем снимке экрана показаны результаты нажатия кнопки «Refactor» и создания DustinDate в качестве подтипа java.util.Date. Обратите внимание, что использование Date in Main было автоматически обновлено, чтобы использовать только что созданный DustinDate, оставляя неиспользованный импорт java.util.Date позади.
Следующий листинг кода — это исходный код DustinDate, который был сгенерирован исключительно NetBeans как рефакторинг java.util.Date для подтипа «Введение в локальное расширение».
package dustin.examples; import java.util.Date; /** * * @author Dustin */ public class DustinDate extends Date { /** * Allocates a <code>Date</code> object and initializes it so that * it represents the time at which it was allocated, measured to the * nearest millisecond. * * @see java.lang.System#currentTimeMillis() */ public DustinDate() { super(); } /** * Allocates a <code>Date</code> object and initializes it to * represent the specified number of milliseconds since the * standard base time known as "the epoch", namely January 1, * 1970, 00:00:00 GMT. * * @param date the milliseconds since January 1, 1970, 00:00:00 GMT. * @see java.lang.System#currentTimeMillis() */ public DustinDate(long date) { super(date); } /** * Allocates a <code>Date</code> object and initializes it so that * it represents midnight, local time, at the beginning of the day * specified by the <code>year</code>, <code>month</code>, and * <code>date</code> arguments. * * @param year the year minus 1900. * @param month the month between 0-11. * @param date the day of the month between 1-31. * @see java.util.Calendar * @deprecated As of JDK version 1.1, * replaced by <code>Calendar.set(year + 1900, month, date)</code> * or <code>GregorianCalendar(year + 1900, month, date)</code>. */ public DustinDate(int year, int month, int date) { super(year, month, date); } /** * Allocates a <code>Date</code> object and initializes it so that * it represents the instant at the start of the minute specified by * the <code>year</code>, <code>month</code>, <code>date</code>, * <code>hrs</code>, and <code>min</code> arguments, in the local * time zone. * * @param year the year minus 1900. * @param month the month between 0-11. * @param date the day of the month between 1-31. * @param hrs the hours between 0-23. * @param min the minutes between 0-59. * @see java.util.Calendar * @deprecated As of JDK version 1.1, * replaced by <code>Calendar.set(year + 1900, month, date, * hrs, min)</code> or <code>GregorianCalendar(year + 1900, * month, date, hrs, min)</code>. */ public DustinDate(int year, int month, int date, int hrs, int min) { super(year, month, date, hrs, min); } /** * Allocates a <code>Date</code> object and initializes it so that * it represents the instant at the start of the second specified * by the <code>year</code>, <code>month</code>, <code>date</code>, * <code>hrs</code>, <code>min</code>, and <code>sec</code> arguments, * in the local time zone. * * @param year the year minus 1900. * @param month the month between 0-11. * @param date the day of the month between 1-31. * @param hrs the hours between 0-23. * @param min the minutes between 0-59. * @param sec the seconds between 0-59. * @see java.util.Calendar * @deprecated As of JDK version 1.1, * replaced by <code>Calendar.set(year + 1900, month, date, * hrs, min, sec)</code> or <code>GregorianCalendar(year + 1900, * month, date, hrs, min, sec)</code>. */ public DustinDate(int year, int month, int date, int hrs, int min, int sec) { super(year, month, date, hrs, min, sec); } /** * Allocates a <code>Date</code> object and initializes it so that * it represents the date and time indicated by the string * <code>s</code>, which is interpreted as if by the * {@link Date#parse} method. * * @param s a string representation of the date. * @see java.text.DateFormat * @see java.util.Date#parse(java.lang.String) * @deprecated As of JDK version 1.1, * replaced by <code>DateFormat.parse(String s)</code>. */ public DustinDate(String s) { super(s); } }
Поскольку использовался подход «подтипа», NetBeans должен был генерировать только конструкторы, чтобы использовать расширяемый класс. Общедоступные или защищенные методы «get», «set» и любые другие методы Date автоматически доступны расширяющему классу DustinDate. Иногда существуют ограничения и недостатки использования наследования реализации (его даже называют злым ). Одним из таких недостатков является невозможность продления выпускных классов. Чтобы проиллюстрировать это, я перехожу к введению локального расширения java.lang.String вместо Date.
package dustin.examples; /** * * @author Dustin */ public class Main { private DustinDate date; private String string; /** * @param arguments the command line arguments */ public static void main(final String[] arguments) { } }
Выделение типа данных String и выбор рефакторинга «Ввести локальное расширение» приводит к появлению экрана мастера, подобного следующему.
Я был приятно удивлен, увидев, что NetBeans 7.2 не допускает опцию «Подтип» в этом случае (строка является окончательной и не может быть расширена). Я должен использовать «Обертку», но могу выбрать один из трех вариантов «Равенство» для «Обертки». В этом примере я не буду вдаваться в подробности со String, но мастер сгенерирует локальные расширения как оболочки, используя любую из трех опций «Равенство». Во всех случаях определенные методы в сгенерированном классе должны быть удалены или изменены для компиляции нового кода. Снимок экрана использования этого с String с использованием «Wrapper» и «Delegate» показан далее.
Чтобы упростить демонстрацию локального расширения типа оболочки с рефакторингом NetBeans 7.2 с различными наборами параметров, я заставил NetBeans сгенерировать большую часть исходного кода для простого, совершенно нового класса с именем Person, который будет показан в следующем листинге кода.
package dustin.examples; import java.util.Objects; /** * * @author Dustin */ public final class Person { private String lastName; private String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } public String getLastName() { return this.lastName; } public void setLastName(String newLastName) { this.lastName = newLastName; } public String getFirstName() { return this.firstName; } public void setFirstName(String newFirstName) { this.firstName = newFirstName; } @Override public int hashCode() { int hash = 3; hash = 83 * hash + Objects.hashCode(this.lastName); hash = 83 * hash + Objects.hashCode(this.firstName); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (!Objects.equals(this.lastName, other.lastName)) { return false; } if (!Objects.equals(this.firstName, other.firstName)) { return false; } return true; } @Override public String toString() { return "Person{" + "lastName=" + lastName + ", firstName=" + firstName + '}'; } }
При использовании локального расширения типа «Обертка» возможны три варианта «Равенство». Следующие три снимка экрана показывают использование каждого с классом, соответствующим образом названным для каждого. Обратите внимание, что я снял флажок, чтобы поменять свое использование Person на этот новый класс, потому что я хотел использовать этот исходный класс Person для демонстрации рефакторинга для параметров равенства «Создать» и «Отдельно» в дополнение к «Делегировать».
Три сгенерированных «Wrapper» класса (PersonDelegate, PersonGenerate и PersonSeparate идентичны, за исключением того, как они переопределяют Object.equals (Object) и Object.hashCode () . Учитывая это, я сначала показываю листинг кода для PersonDelegate, а затем показываю только разные равно реализации для трех подходов.
package dustin.examples; /** * * @author Dustin */ public final class PersonDelegate { private Person delegate; public PersonDelegate(Person delegate) { this.delegate = delegate; } public PersonDelegate(final String newLastName, final String newFirstName) { this.delegate = new Person(newLastName, newFirstName); } public String getLastName() { return delegate.getLastName(); } public void setLastName(String newLastName) { delegate.setLastName(newLastName); } public String getFirstName() { return delegate.getFirstName(); } public void setFirstName(String newFirstName) { delegate.setFirstName(newFirstName); } public String toString() { return delegate.toString(); } public boolean equals(Object o) { Object target = o; if (o instanceof PersonDelegate) { target = ((PersonDelegate) o).delegate; } return this.delegate.equals(target); } public int hashCode() { return this.delegate.hashCode(); } }
Как указывалось перед последним листингом кода, только реализация «equals» и «hashCode» изменяется в зависимости от выбранной настройки равенства. Чтобы сделать различия более очевидными, далее показаны только реализации equals и hashCode трех сгенерированных классов.
public boolean equals(Object o) { Object target = o; if (o instanceof PersonDelegate) { target = ((PersonDelegate) o).delegate; } return this.delegate.equals(target); } public int hashCode() { return this.delegate.hashCode(); }
@Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final PersonGenerate other = (PersonGenerate) obj; if (!Objects.equals(this.delegate, other.delegate)) { return false; } return true; } @Override public int hashCode() { int hash = 5; hash = 43 * hash + Objects.hashCode(this.delegate); return hash; }
public boolean equalsPersonSeparate(PersonSeparate o) { return this.delegate.equals(o.delegate); } public boolean equals(Object o) { Object target = o; if (o instanceof PersonSeparate) { target = ((PersonSeparate) o).delegate; } return this.delegate.equals(target); } public int hashCode() { return this.delegate.hashCode(); }
Looking at the different implementations of equals andhashCode helps us to see how they are different. The «Generate» equality approach generates the equals method for this new class using NetBeans’s standard generation mechanism. Because I have the source set to JDK 1.7 in my NetBeans project, it takes advantage of the new (to Java 7) Objects class. The «Generate» approach also generates its hash code as if done for an all-new class rather than relying explicitly on the delegate’s hash code. The other two «Equality» approaches («Delegate» and «Separate») for generating a «Wrapper» both simply return the delegate’s hash code.
The «Equality» setting for «wrapper» local extensions of Equality «Delegate» or «Separate» lead to the same implementations of equals and hashCode. The difference between the two is that «Separate» adds a new method (not part of the Objects contract) called equalsXXXXXXXX where the XXXXXXXX represents the generated class’s name [so it is called equalsPersonSeparate(PersonSeparate) in this case]. Note that this new method does not accept an Object like equals, but expects its own type.
NetBeans 7.2’s online help covers all of this in more detail under «Introduce Local Extension Dialog Box.» The following is an excerpt from that section.
- Equality. Select one of the following options to set how the equals and hashCode methods should be handled:
- Delegate. Select to delegate to the equals and hashCode methods of the original class.
- Generate. Select to generate new equals and hashCode methods using the IDE code generator.
- Separate. Select to separate the equals method into two. A new method is added to check if the original class equals the extension class.
Before ending this post, I want to point out how easy it is to use NetBeans to identify differences between the generated files. The next screen snapshot shows what it looks like when one right-clicks on the tab in the source code editor for the PersonDelegate.java class/file. This brings up the drop-down menu that includes «Diff To …» as an option.
I’m then presented with the option of the file to diff PersonDelegate to in either the same package (or browser allows arbitrary location to be specified) or opened in the editor window. In this case, I have selected PersonGenerate in the right side choices of files already open in the editor.
NetBeans displays the differences between PersonDelegate and PersonGenerate as shown in the next screen snapshot.
Most of the changes indicated by the colored bars on the far right are changes in class names. However, as the image above shows, the substantial changes are in the equality methods equals and hashCode.
The NetBeans diff shows that the primary difference between «Delegate» and «Separate» Equality in the corresponding generated Wrapper classes is the new method in the «Separate» class.
Finally, NetBeans shows that the differences between the «Generate» and «Separate» wrapper implementations.
Using NetBeans 7.2’s «Wrapper» approach to the «Introduce Local Extension» refactoring provides an easy mechanism for employing the delegation pattern (as commonly understood these days rather than the more historically based use of it described in The Gang of Four is Wrong and You Don’t Understand Delegation).
Conclusion
This post has focused on use of NetBeans 7.2’s ability to automatically generate code based on the «Introduce Local Extension» refactoring using implementation inheritance («Subtype») and composition («Wrapper»). Along the way, the post also examined NetBeans’s handy file differencing («diff») capability.