Теория и практика Java: Антишаблон pseudo-typedef

Расширение не является определением имени типа

Добавление обобщений к языку ™ Java усложнило систему типов и повысило словесное наполнение многих объявлений переменных и методов. Поскольку средство "typedef" не было предусмотрено для определения коротких имен типов, некоторые разработчики обратились к расширению как к "typedef для бедных" с результатами ниже среднего. В Теории и практике Java за этот месяц Java-эксперт Брайан Гетц объясняет ограничения этого "антишаблона."

Брайан Гетц, главный консультант, Quiotix

Брайан Гетц (Brian Goetz) - консультант по ПО и последние 15 лет работал профессиональными разработчиком ПО. Сейчас он является главным консультантом в фирме Quiotix, занимающейся разработкой ПО и консалтингом и находящейся в Лос-Альтос, Калифорния. Следите за публикациями Брайана в популярных промышленных изданиях. Вы можете связаться с Брайаном по адресу brian@quiotix.com



17.01.2007

Обычная жалоба по поводу нового средства обобщения в Java 5.0 состоит в том, что оно визуализирует код слишком многословно. Описания переменных, которые обычно полностью умещались в одной строке, больше в ней не умещаются, и повторение, связанное с объявлением переменных параметризованного типа, может быть утомительным, особенно без хорошей поддержки IDE для автоматического завершения. Например, если вам нужно декларировать Map, ключи которого - Socket, а значения Future<String>, то старый способ:

Map socketOwner = new HashMap();

более компактен, чем новый:

Map<Socket, Future<String>> socketOwner 
  = new HashMap<Socket, Future<String>>();

Конечно, новый способ включает больше информации о типе, сокращая ошибки программирования и улучшая читаемость программы, но он связан с большим объемом предварительной работы по объявлению переменных и сигнатур методов. Повторение параметров типа при объявлении и инициализации кажется особенно ненужным; Socket и Future<String> нужно печатать два раза, что вынуждает нас нарушать "СУХОЙ" принцип (не повторяйся).

Синтезирование typedef... вроде бы

Добавление обобщения повышает сложность системы типов. Там, где "type" и "class" были почти синонимичными до Java 5.0, параметризованные типы, особенно типы со связанными символами, делают концепты подтипов и подклассов совершенно разными. Типы ArrayList<?>, ArrayList<? extends Number> и ArrayList<Integer> - разные типы, несмотря на то, что они все применяются одним классом, ArrayList. Эти типы образуют иерархию; ArrayList<?> - подтип ArrayList<? extends Number> и ArrayList<? extends Number> - супертип ArrayList<Integer>.

В первоначальной простой системе типов такое свойство, как typedef языка C не имело смысла. Но в более усложненной системе типов средство typedef могло бы дать некоторые преимущества. Хорошо это или плохо, typedef не был добавлен к языку вместе с обобщениями.

Одно (неправильное) решение, которое используется некоторыми людьми как "typedef для бедных" - тривиальное расширение: создание класса, который расширяет параметризованный тип, но не добавляет ему функциональности, как, например, тип SocketUserMap, как показано в листинге 1:

Листинг 1. Антишаблон pseudo-typedef - не делайте этого
public class SocketUserMap extends HashMap<Socket<Future<String>> { }
SocketUserMap socketOwner = new SocketUserMap();

Этот прием, который я буду называть антишаблоном pseudo-typedef, осуществляет (сомнительную) цель возвращения определения socketOwner на одну строчку, но оно получается несколько больше и, в конечном счете, становится препятствием для повторного использования и сопровождения. (Для классов, имеющих конструкторы, кроме конструкторов no-arg, производный класс также должен описать каждый конструктор, так как конструкторы не наследуются)


Проблемы с псевдотипами

В C определение нового типа при помощи typedef более похоже на макрос, чем на объявление типа. Typedef, которые определяют эквивалентные типы, могут быть свободно взаимозаменяемы друг другом, так же как и "сырым" типом. Листинг 2 показывает пример определения функции внешнего вызова, где typedef используется в сигнатуре, но вызывающая программа обеспечивает внешний вызов эквивалентного типа, что устраивает и компилятор, и среду выполнения:

Листинг 2. Примеры typedef в C
// define a type called "callback" that is a function pointer
typedef void (*Callback)(int);

void dosomething(Callback callback) { }

// This function conforms to the type defined by Callback
void callbackFunction(int arg) { }

// So a caller can pass the address of callbackFunction to dosomething
void useCallback() {
  dosomething(&callbackFunction); 
}

Расширение - не определение имени типа

Аналогичная программа в языке Java, которая попыталась бы использовать антишаблон pseudo-typedef, столкнулась бы с проблемой. Типы StringList и UserList в листинге 3 расширяют обычный суперкласс, но они - не эквивалентные типы. Это значит, что любой код, который собирается вызвать lookupAll, должен передать StringList, а не List<String> или UserList.

Листинг 3. Как псевдотипы заставляют клиентов использовать псевдотипы
class StringList extends ArrayList<String> { }
class UserList extends ArrayList<String> { }
...
class someClass {
    public void validateUsers(UserList users) {... }
    public UserList lookupAll(StringList names) {... }
}

Это ограничение более сурово, чем может показаться. В маленькой программе, оно, возможно, не имеет значения, но по мере того, как программа становится больше, требование последовательно использовать псевдотип могло бы привести к проблемам. Если переменная принадлежит к типу StringList, вы не можете присвоить ей обычное List<String>, потому что List<String> - супертип StringList, и поэтому не является StringList. Так же, как вы не можете присвоить Object переменной типа String, вы не можете присвоить List<String> переменной типа StringList. (Однако вы можете пойти другим путем; например, можно присвоить StringList переменной типа List<String> так как List<String> - это супертип StringList.)

То же самое верно относительно параметров методов; если параметр метода принадлежит к типу StringList, нельзя передавать ему обычный List<String>. Это значит, что вы совсем не можете использовать псевдотипы как аргументы метода, не потребовав, чтобы при каждом использовании этого метода использовался бы псевдотип, что на практике означает, что совсем нельзя использовать псевдотипы в библиотеках API. И поскольку большинство API-библиотек выросли из кода, который никогда и не предназначался для этой цели, оправдание "этот код только для меня, никто другой его использовать не будет" - не слишком приемлемо (если допустить, что ваш код хоть отчасти хорош; если он ни на что не годен, то вы, вероятно, правы).

Псевдотипы заразны

Эта "вирусная" природа - один из факторов, который сделал повторное использование кода C проблематичным. Почти каждый пакет C имеет файлы заголовков, которые определяют служебные макросы и такие типы, как int32, boolean, true, false и так далее. Если вы попытаетесь использовать в приложении несколько пакетов, которые пользуются не идентичными определениями для общих элементов, вы можете провести много времени в "аду файлов заголовков" прежде, чем сможете скомпилировать хотя бы пустую программу, которая включает все файлы заголовков. Написание C-приложения, которое использует дюжину пакетов от различных авторов, почти наверняка предполагает некоторые проблемы такого рода. С другой стороны, для Java-приложения вполне обычно использовать дюжину, если не больше различных пакетов без каких-либо проблем. Если бы пакеты должны были использовать псевдотипы в своих API, мы бы пересмотрели эту проблему, от которой осталось бы лишь болезненное воспоминание.

К примеру, каждый из двух различных пакетов определяет StringList, используя антишаблон pseudo-typedef, как показано в листинге 4, и каждый определяет служебные методы для работы с StringList. Тот факт, что оба пакета определили один и тот же идентификатор, уже может вызвать небольшие неудобства; клиентские программы должны выбрать одно определение для импорта и использовать полное имя для другого. Но более серьезная проблема состоит в том, что теперь клиенты этих пакетов не смогут создать объект, который можно передать как sortList, так и reverseList, поскольку эти два типа StringList различны и несовместимы друг с другом. Теперь клиентам придется решить, какой из двух пакетов использовать, или им придется выполнить много работы, переключаясь между различными видами StringList. То, что замышлялось как облегчение работы создателя пакетов, стало существенным препятствием к использованию пакета во всех, кроме самых ограниченных, контекстах.

Листинг 4. Как использование псевдотипов запрещает повторное использование
package a;

class StringList extends ArrayList<String> { }
class ListUtilities {
    public static void sortList(StringList list) { }
}

package b;

class StringList extends ArrayList<String> { }
class someOtherUtilityClass {
    public static void reverseList(StringList list) { }
}
 
...

class Client {
    public void someMethod() {
        StringList list =...;
        // Can't do this
        ListUtilities.sortList(list);
        someOtherUtilityClass.reverseList(list);
    }
}

Псевдотипы обычно слишком конкретны

Еще одна проблема с антишаблоном pseudo-typedef состоит в том, что он обычно игнорирует возможность использования интерфейсов для определения типов переменных и аргументов методов. Поскольку можно определить StringList как интерфейс, который расширяет List<String> и конкретный тип StringArrayList, который расширяет ArrayList<String> и применяет StringList, большинство пользователей антишаблонов pseudo-typedef обычно не используют такие длинные имена, поскольку цель этого приема - главным образом упростить и сократить имена типов. В результате API будут менее полезными и более уязвимыми, так как они скорее используют конкретные типы, такие как ArrayList чем абстрактные, как List.

Более безопасный прием

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

public static <K,V> Map<K,V> newHashMap() {
    return new HashMap<K,V>(); 
}

Вы можете его использовать с полной уверенностью в том, что избежите повторного ввода параметров типа:

Map<Socket, Future<String>> socketOwner = Util.newHashMap();

Этот подход работает, поскольку компилятор может логически выводить значения K и V из контекста, в котором вызывается обобщенный метод newHashMap().


Заключение

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

Ресурсы

Научиться

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=188736
ArticleTitle=Теория и практика Java: Антишаблон pseudo-typedef
publish-date=01172007