Статьи

Преобразование коллекций с декораторами

Образец Декоратора

С тех пор, как я впервые изучил шаблоны проектирования программ, шаблон декоратор был моим любимым. Это показалось мне новой идеей и гораздо более интересным, чем другие. Не поймите меня неправильно, большинство других тоже взорвало мне голову, но не так сильно, как образец декоратора. По сей день это все еще один из моих любимых.

(Если вы не знакомы с шаблонами проектирования, я настоятельно рекомендую Head First Design Patterns . Если вы просто хотите узнать о шаблоне декоратора, вот выдержка из главы декоратора из этой книги.)

Лично я считаю, что шаблон декоратора обычно используется недостаточно. Есть пара причин для этого. Во-первых, я не думаю, что это применимо ко всем таким множеству ситуаций. Другое дело, что проблемы, которые можно решить с помощью шаблона декоратора, обычно довольно трудно обнаружить. То, что делает этот образец таким потрясающим для меня, является той же самой причиной, по которой может быть трудно понять, где это необходимо, причина в том, что это такая необычная идея. То есть, кажется, пока вы не будете сильно знакомы с принципом «композиция над наследованием».

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

Во всяком случае, шаблон декоратора — это не только мой любимый шаблон, он активно используется в одной из моих любимых новых функций Java 8: Stream API. На самом деле, многое из того, что я собираюсь показать вам, во многом имитирует поведение Stream API.

Проблема

Допустим, у вас есть список строк, но они могут иметь или не иметь начальные или конечные пробелы, которые вам не нужны. Вы, вероятно, сделали бы что-то подобное, чтобы избавиться от ненужных мест.

1
2
3
4
5
6
7
8
9
List untrimmedStrings = aListOfStrings();
List trimmedStrings = new ArrayList();
 
for(String untrimmedString : untrimmedStrings)
{
    trimmedStrings.add(untrimmedString.trim());
}
 
//use trimmed strings...

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

Давайте посмотрим на более декларативную версию кода, а затем посмотрим, как использовать его для решения других проблем.

1
2
3
4
List untrimmedStrings = aListOfStrings();
List trimmedStrings = trimmed(untrimmedStrings);
 
//use trimmed strings...

Черт, в этой функции trimmed () может происходить все что угодно! И посмотри на это; он возвращает список строк, как и в предыдущем случае. Жирный груз добра, который сделал, верно?

Неправильно. Да, технически эта функция могла бы просто делать то же самое, что мы делали ранее, что означает, что все, что мы делали, это делали этот внешний код декларативным. Но в этом примере он предназначен для использования в качестве статического фабричного метода (со статическим импортом), который создает новый обрезанный объект, который оборачивает список untrimmedStrings. Trimmed реализует интерфейс List, но он делегирует почти все в свернутый список, но часто с декорированной функциональностью. Когда новая строка добавляется или удаляется, это делается для «обоих» списков, выполняя это в свернутом списке. И когда он добавляет новую строку, он может добавить ее как есть, но тогда ему просто нужно убедиться, что она обрезается при выходе.

Кроме того, поскольку обрезка выполняется только при извлечении данных из списка, нам не пришлось выполнять всю работу по обрезке каждой строки сразу. Существует вероятность того, что с некоторыми Струнами никогда не будет иметь дело, поэтому эти Струны никогда не будут излишне обрезаны.

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

Я не хочу тратить слишком много времени и места в этой статье, чтобы показать вам полностью созданную реализацию List Trimmed (существует более 30 методов для определения List), поэтому я собираюсь настроить ее так, чтобы это было просто Итерируемые методы, которые определены. Поскольку в большинстве случаев все, что вы действительно делаете, это перебираете коллекции, это должно быть относительно приемлемым.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Trimmed implements Iterable
{
   public static List trimmed(List base) {
      return base;
   }
 
   public Trimmed(Iterable base)
   {
      this.base = base;
   }
 
   public Iterator iterator()
   {
      return new TrimmedIterator(base.iterator());
   }
 
   private Iterable base;
}
 
class TrimmedIterator implements Iterator
{
   public TrimmedIterator(Iterator base)
   {
      this.base = base;
   }
 
   public boolean hasNext()
   {
      return base.hasNext();
   }
 
   public String next()
   {
      return base.next().trim();
   }
 
   public void remove()
   {
      throw new UnsupportedOperationException();
   }
 
   private Iterator base;
}

Как украсить предметы

Я не помню, чтобы кто-нибудь когда-либо упоминал об этом, но это довольно важно, поэтому я хочу рассказать вам об этом.

Есть две основные мысли о том, как украсить предмет. Первый — когда вы просто создаете новый экземпляр декоратора с переданным декорированным / обернутым объектом. Второй вариант — вызвать метод для декорируемого объекта.

Оба варианта показаны здесь

1
2
3
4
5
6
7
8
9
MyCollection untrimmedStrings = aCollectionOfStrings();
 
//new Decorator Instance
MyCollection trimmedStrings = new TrimmingDecorator(untrimmedStrings);
 
//OR
 
//method call on the to-be-decorated object
MyCollection trimmedStrings2 = untrimmedStrings.trimmed();

И код trimmed () выглядит так:

1
2
3
public MyCollection trimmed() {
   return new TrimmingDecorator(this);
}

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

Новые экземпляры Pros:

  • Более расширяемый, чем опция вызова метода, поскольку вызовы метода должны пытаться охватить все возможности декоратора
  • Пользователи могут видеть, что это шаблон декоратора легче
  • В интерфейсе Decoratable требуется меньше методов

Вызов метода Плюсы:

  • Скрывает реализацию декоратора, если пользователю не нужно знать
  • Нет явных «новых» ключевых слов на стороне пользователя (что обычно считается плохим)
  • Пользователям легче найти все декораторы, так как все они перечислены в интерфейсе декорируемого объекта.

Исходная библиотека ввода-вывода Java — хороший пример декорирования нового экземпляра, а Stream API в Java 8 — хороший пример декорирования вызова метода. Лично я предпочитаю использовать опцию вызова метода, так как она делает все возможности очевидными для пользователя, но если цель состоит в том, чтобы сделать так, чтобы пользователь мог расширять ваши объекты также с помощью своих собственных декораторов, то вам определенно следует пойти с новый экземпляр маршрута.