понедельник, 22 мая 2017 г.

Constructors Must Be Code-Free

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

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


interface Name {
  String first();
}

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

public final class EnglishName implements Name {
  private final String name;
  public EnglishName(final CharSequence text) {
    this.name = text.toString().split(" ", 2)[0];
  }
  @Override
  public String first() {
    return this.name;
  }
}

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

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];
  }
}

Это правильный дизайн. Я вижу, что вы улыбаетесь, поэтому позвольте мне доказать свою точку зрения.Прежде чем я начну доказывать, позвольте мне попросить вас прочитать эту статью: Composable Decorators vs. Imperative Utility Methods. Он объясняет разницу между статическим методом и композиционными декораторами. Первый фрагмент, приведенный выше, очень близок к императивному методу служебной программы, хотя он и выглядит как объект. Второй пример - истинный объект.В первом примере мы злоупотребляем новым оператором и превращаем его в статический метод, который делает все вычисления для нас прямо здесь и сейчас. Вот что такое императивное программирование. В императивном программировании мы делаем все вычисления прямо сейчас и возвращаем полностью готовые результаты. В декларативном программировании мы вместо этого пытаемся как можно дольше откладывать вычисления.Давайте попробуем использовать наш класс EnglishName:

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.first (), если что-то пойдет не так, и нам нужно создать объект исключения.Я хочу сказать, что любые вычисления внутри конструктора - плохая практика, и их следует избегать, потому что они являются побочными эффектами и не запрашиваются владельцем объекта.Что касается производительности во время повторного использования имени, вы можете спросить. Если мы создадим экземпляр EnglishName и затем вызываем name.first () пять раз, мы получим пять вызовов метода String.split ().Чтобы решить эту проблему, мы создадим другой класс, компонуемый декоратор, который поможет нам решить эту проблему «повторного использования»:

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-аспектов, но вы можете использовать любые другие инструменты кэширования, доступные на Java (или других языках), например Guava Cache:

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 изменчивым и лениво загруженным - это анти-паттерн, о котором я говорил ранее в «Объекты должны быть неизменными».Вот как будет выглядеть наш код:

final Name name = new CachedName(
  new EnglishName(
    new NameInPostgreSQL(/*...*/)
  )
);

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

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

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