Интерфейсы классов и коллекции

в 6:55, , рубрики: java, интерфейсы, проектирование, метки: , ,

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

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

List<String> getElements(String key);

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

public interface MyCollection {
    List<String> getElements(String key);
    String getElement(String key, int index);
    List<String> getElementsRange(String key, int fromIndex, int toIndex);
    int getElementsCount(String key);
}

Подобные интерфейсы я время от времени встречаю в коммерческом или свободном коде. К примеру, интерфейс UserProvider в Jabber-сервере OpenFire (там есть getUsers, getUsers с параметрами и getUserCount; аналогичный пример и с двумя методами findUsers). Если задуматься, ни один из дополнительных методов не нужен. Их можно тривиально выразить через getElements:

String getElement(String key, int index) {
    return getElements(key).get(index);
}
   
List<String> getElementsRange(String key, int fromIndex, int toIndex) {
    return getElements(key).subList(fromIndex, toIndex);
}
   
int getElementsCount(String key) {
    return getElements(key).size();
}

Причём реализации настолько простые, что даже ради синтаксического сахара вводить новые методы сомнительно. Малоопытный разработчик тут говорит: «Как же так, первый метод же возвращает весь список, а если он большой и не влезет в память? И вообще если нам нужен всего один элемент?» Всё потому, что интерфейс List прочно ассоциируется у некоторых с конкретной реализацией (обычно ArrayList) и люди забывают, что можно сделать настолько же ленивые методы List'а, насколько они ленивы у вашего собственного интерфейса.

Можно возразить, что вспомогательные методы хоть и не несут явной пользы, но и не мешают. Однако опыт показывает, что мешают. Имея такой интерфейс с дополнительными ненужными методами разработчики будут считать себя вправе действительно возвращать ArrayList с полным списком элементов в getElements, не утруждаясь созданием ленивого списка. В результате, например, getElements(key).size() уже не работает, так как огромное количество элементов будет загружено в память, и она кончится. Поэтому пользователь будет обязан использовать вашу реализацию getElementsCount(key).

Пусть, к примеру, у вас есть утилитный метод, который выводит строку, содержащую количество элементов, несколько первых и многоточие (если элементов больше). Например, так:

[100500] First, second, third...

Реализация могла бы быть такой:

public static String getSummary(List<String> list) {
    StringBuilder sb = new StringBuilder();
    int size = list.size();
    sb.append('[').append(size).append("] ");
    int maxElements = Math.min(3, size);
    for(int i=0; i<maxElements; i++) sb.append(i==0?"":", ").append(list.get(i));
    if(maxElements < size) sb.append("...");
    return sb.toString();
}

И вы б такой метод спокойно вызывали getSummary(myCollection.getElements(key)). Но тут мы вспоминаем, что имеется много неленивых реализаций, и жизнь усложняется. В результате возникают методы с уродливыми наборами параметров. Первый подход — это создать метод getSummary(MyCollection collection, String key). Из прекрасного независимого метода, который можно было бы переиспользовать, получается метод, зависящий от вашего интерфейса коллекции, который уже не применить к чему-то другому. Второй подход — это создать метод getSummary(int count, List firstThreeElements) и вызывать его через getSummary(myCollection.getElementsCount(key), myCollection.getElementsRange(key, 0, 3)). Хотя теперь метод не завязан на ваш интерфейс, такое решение ещё хуже, потому что интерфейс метода завязан на его реализацию. Представьте, пользователь захотел, чтобы последний элемент тоже выводился:

[100500] First, second, third... last

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

Хорошим решением могло бы быть создание адаптера, реализующего интерфейс List. Тогда вызов выглядел бы как getSummary(new MyCollectionListAdapter(myCollection, key)). Но это всё равно ненужное усложнение, ведь можно было с самого начала сделать хорошо. Кроме того, такой адаптер изначально ограничен. К примеру, вы не можете реализовать оптимально List.contains или List.indexOf, так как интерфейс MyCollection не обладает нужным методом. Скажем, если в конкретной реализации MyCollection список по факту загружается из SQL-базы, то contains легко оптимизируется: вам не нужно перебирать все элементы.

Если же в интерфейсе MyCollection есть только getElements, то при его реализации у программиста не останется выхода кроме как сделать всё хорошо и реализовать ленивый список (конечно, для заведомо небольших коллекций можно этим и пренебречь).

В общем, хочу отметить, что не стоит дублировать те хорошие интерфейсы, которые уже созданы за вас. Вы таким образом засоряете свой интерфейс и в итоге можете принести больше вреда, чем пользы. Не бойтесь также реализовывать интерфейсы стандартных коллекций. Даже для Map это совсем не сложно, а уж для List и подавно. В java.util есть вспомогательные классы вроде AbstractList, которые помогут вам.

Автор: lany


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js