Статьи

Конструкторы должны быть без кода

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

Убить Билла: Том 2 (2004) Квентина Тарантино

Убить Билла: Том 2 (2004) Квентина Тарантино

Допустим, мы создаем интерфейс, который будет представлять имя человека:

1
2
3
interface Name {
  String first();
}

Довольно легко, правда? Теперь давайте попробуем реализовать это:

01
02
03
04
05
06
07
08
09
10
public final class EnglishName implements Name {
  private final String name;
  public EnglishName(final CharSequence text) {
    this.parts = text.toString().split(" ", 2)[0];
  }
  @Override
  public String first() {
    return this.name;
  }
}

Что с этим не так? Это быстрее, верно? Он разбивает имя на части только один раз и инкапсулирует их. Затем, независимо от того, сколько раз мы вызываем метод first() , он вернет одно и то же значение и не будет нуждаться в повторном разбиении. Однако это ошибочное мышление! Позвольте мне показать вам правильный путь и объяснить:

01
02
03
04
05
06
07
08
09
10
public final class EnglishName implements Name {
  private final CharSequence text;
  public EnglishName(final CharSequence txt) {
    this.text = txt;
  }
  @Override
  public String first() {
    return this.text.toString().split("", 2)[0];
  }
}

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

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

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

Давайте попробуем использовать наш класс EnglishName :

01
02
03
04
05
06
07
08
09
10
11
final Name name = new EnglishName(
  new NameInPostgreSQL(/*...*/)
);
if (/* something goes wrong */) {
  throw new IllegalStateException(
    String.format(
      "Hi, %s, we can't proceed with your application",
      name.first()
    )
  );
}

В первой строке этого фрагмента мы просто создаем экземпляр объекта и помечаем его name . Мы пока не хотим идти в базу данных и извлекать полное имя оттуда, разбивать его на части и инкапсулировать их в name . Мы просто хотим создать экземпляр объекта. Такое поведение анализа будет побочным эффектом для нас и в этом случае замедлит работу приложения. Как видите, нам может понадобиться только name.first() если что-то пойдет не так, и нам нужно создать объект исключения.

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

Что касается производительности во время повторного использования name , вы можете спросить. Если мы name.first() экземпляр EnglishName и затем вызовем name.first() пять раз, мы получим пять вызовов метода String.split() .

Чтобы решить эту проблему, мы создаем другой класс, составной декоратор , который поможет нам решить эту проблему «повторного использования»:

01
02
03
04
05
06
07
08
09
10
11
public final class CachedName implements Name {
  private final Name origin;
  public CachedName(final Name name) {
    this.origin = name;
  }
  @Override
  @Cacheable(forever = true)
  public String first() {
    return this.origin.first();
  }
}

Я использую аннотацию Cacheable из jcabi-aspect , но вы можете использовать любые другие инструменты кэширования, доступные в Java (или других языках), такие как Guava Cache :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public final class CachedName implements Name {
  private final Cache<Long, String> cache =
    CacheBuilder.newBuilder().build();
  private final Name origin;
  public CachedName(final Name name) {
    this.origin = name;
  }
  @Override
  public String first() {
    return this.cache.get(
      1L,
      new Callable<String>() {
        @Override
        public String call() {
          return CachedName.this.origin.first();
        }
      }
    );
  }
}

Но, пожалуйста, не делайте CachedName изменчивым и лениво загружаемым — это анти-паттерн, который я обсуждал ранее в « Объектах должны быть неизменяемыми» .

Вот как теперь будет выглядеть наш код:

1
2
3
4
5
final Name name = new CachedName(
  new EnglishName(
    new NameInPostgreSQL(/*...*/)
  )
);

Это очень примитивный пример, но я надеюсь, что вы поняли идею.

В этом проекте мы в основном разделяем объект на две части. Первый знает, как получить имя из английского имени. Второй знает, как кэшировать результаты этого расчета в памяти. И теперь я, как пользователь этих классов, решил, как именно их использовать. Я решу, нужно ли мне кэшировать или нет. Вот что такое объектная композиция.

Позвольте мне повторить, что единственным допустимым оператором внутри конструктора является присваивание. Если вам нужно добавить что-то еще, подумайте о рефакторинге — ваш класс определенно нуждается в редизайне.

Ссылка: Конструкторы должны быть свободны от кода от нашего партнера JCG Егора Бугаенко в блоге About Programming .