четверг, 25 мая 2017 г.

Композиционные декораторы против императивных вспомогательных методов

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


Во-первых, практический пример. Вот интерфейс для объекта, который должен где-то читать текст и возвращать его:

interface Text {
  String read();
}
Вот реализация, которая читает текст из файла:
final class TextInFile implements Text {
  private final File file;
  public TextInFile(final File src) {
    this.file = src;
  }
  @Override
  public String read() {
    return new String(
      Files.readAllBytes(), "UTF-8"
    );
  }
}
И теперь декоратор, который является еще одной реализацией Text, которая удаляет все непечатаемые символы из текста:
final class PrintableText implements Text {
  private final Text origin;
  public PrintableText(final Text text) {
    this.origin = text;
  }
  @Override
  public String read() {
    return this.origin.read()
      .replaceAll("[^\p{Print}]", "");
  }
}
Вот как я его использую:
final Text text = new PrintableText(
  new TextInFile(new File("/tmp/a.txt"))
);
String content = text.read();
Как вы можете видеть, PrintableText не читает текст из файла. На самом деле неважно, откуда берется текст. Он делегирует чтение текста инкапсулированному экземпляру Text. Как этот инкапсулированный объект будет иметь дело с текстом и где он его получит, не относится к PrintableText.

Давайте продолжим и попробуем создать реализацию Text, которая загладит все буквы в тексте:
final class AllCapsText implements Text {
  private final Text origin;
  public AllCapsText(final Text text) {
    this.origin = text;
  }
  @Override
  public String read() {
    return this.origin.read().toUpperCase(Locale.ENGLISH);
  }
}
Как насчет текста, который обрезает ввод:
final class TrimmedText implements Text {
  private final Text origin;
  public TrimmedText(final Text text) {
    this.origin = text;
  }
  @Override
  public String read() {
    return this.origin.read().trim();
  }
}
Я могу продолжать и продолжать работу с этими декораторами. Я могу создать многие из них, подходящие для их индивидуальных случаев использования. Но давайте посмотрим, как они все могут играть вместе. Предположим, я хочу прочитать текст из файла, заглавными буквами, обрезать его и удалить все непечатаемые символы. И я хочу быть декларативным. Вот что я делаю:
final Text text = new AllCapsText(
  new TrimmedText(
    new PrintableText(
      new TextInFile(new File("/tmp/a.txt"))
    )
  )
);
String content = text.read();
Во-первых, я создаю экземпляр Text, составляя несколько декораторов в один объект. Я декларативно определяю поведение текста без фактического выполнения чего-либо. Пока не вызывается метод read (), файл не трогается и обработка текста не запускается. Текст объекта представляет собой просто композицию декораторов, а не исполняемую процедуру. Ознакомьтесь с этой статьей о декларативных и императивных стилях программирования: классы Utility не имеют ничего общего с функциональным программированием.

Эта конструкция намного более гибкая и многоразовая, чем более традиционная, где объект Text достаточно умен, чтобы выполнять все указанные операции. Например, класс String из Java - хороший пример плохого дизайна. Он имеет более 20 вспомогательных методов, которые должны были быть представлены в качестве декораторов вместо: trim (), toUpperCase (), substring (), split () и многие другие, например. Когда я хочу подрезать строку, заглавными буквами, а затем разбить ее на части, вот как будет выглядеть мой код:
final String txt = "hello, world!";
final String[] parts = txt.trim().toUpperCase().split(" ");
Это необходимо и процедурное программирование. С другой стороны, композитные декораторы сделали бы этот код объектно-ориентированным и декларативным. Что-то вроде этого было бы здорово иметь на Java вместо (псевдокод):
final String[] parts = new String.Split(
  new String.UpperCased(
    new String.Trimmed("hello, world!")
  )
);
В заключение я рекомендую вам дважды подумать каждый раз, когда вы добавляете новый метод утилит в интерфейс / класс. Старайтесь как можно меньше избегать служебных методов и вместо этого использовать декораторы. Идеальный интерфейс должен содержать только методы, которые вы абсолютно не можете удалить. Все остальное должно быть сделано с помощью декораторов.

Комментариев нет:

Отправить комментарий