28 июля 2012 г.

Синтаксический сахарочек

Вообще, java нас не балует синтаксическим сахаром. Лично мне это даже очень нравится — терпеть не могу выкапывать суть под горой сладкого. Из-за этого отказался от мысли перейти на Scala — побоялся диабет схватить. Но иногда хочется все-таки сделать своему компоненту такой сладенький интерфейс, чтобы пальчики оближешь. И когда я нахожу в аскетической джаве какую-нибудь карамельку — я очень радуюсь :)

Например, есть у нас такой класс-мэппер
public class Mapping{
  public String map(final String arg1, final String arg2);
}
И есть какая-то реализация, скажем SimpleImmutableMapping, и хочется сделать для его конфигурирования фабрику-билдер, типа вот такой:
public class MappingBuilder{
    public MappingBuilder map( final String arg1 ){...}
    public MappingBuilder with( final String arg2 ){...}
    public MappingBuilder to( final String mapping ){...}

    public Mapping build(){...}
}
...
final Mapping mapping = new MappingBuilder()
                           .map( "A" ).with( "B" ).to("ab")
                           .map( "C" ).with( "D" ).to("cd")
                           .map( "E" ).with( "F" ).to("ef")
                           .build();
Что мне здесь не нравится? — мне не нравится дуракоустойчивость. Особенно мне не нравится то, что ее здесь нет, и это хорошо видно на скриншоте справа (Да, это у меня такая цветовая схема, нет, глаза не устают, да, мне нравится, идите в жопу).
Что будет, если я вызову в этой точке .build()? Очевидно, ничего хорошего. В лучшем случае уже во время выполнения вылетит IllegalStateException("Can't build mapping with supplied data: first argument is not enough"). Это если я позаботился о том, как мой объект себя ведет, если его используют "неправильно". В более реальном случае будет что-нибудь вроде NullPointerException из недр метода .build(). В самом паршивом случае здесь все отработает без ошибок, а веселье начнется при использовании созданного Mapping-а где-нибудь совсем в другом месте.

Так вот, я раньше думал, что устроить в подобном Builder-е проверку времени выполнения — на яве не получится. А оказывается можно — правда так гемморойно как и все у нас, джавистов:
public class SimpleMapping {

  //код самого мэппера -- скучный и банальный
  .....

  //а вот дальше начинается треш и содомия
  //уберите от монитора женщин и детей, и глубоко вдохните 

  public interface FirstArgumentGivenState {
    public SecondArgumentGivenState with( final String arg2 );
  }

  public interface SecondArgumentGivenState {
    public FullMappingGivenState to( final String mapping );
  }

  public interface FullMappingGivenState {
    public Mapping build();
    public FirstArgumentGivenState map( final String arg1 );
  }

  private static class Builder implements
      FirstArgumentGivenState,
      SecondArgumentGivenState,
      FullMappingGivenState {

    public FirstArgumentGivenState map( final String arg1 ) {
      ...
      return this;
    }

    public SecondArgumentGivenState with( final String arg2 ) {
      ...
      return this;
    }

    public FullMappingGivenState to( final String mapping ) {
      ...
      return this;
    }

    public Mapping build() {
      ...
    }
  }

  public static FullMappingGivenState builder() {
    return new Builder();
  }
}
....
final Mapping mapping = SimpleMapping.builder()
    .map( "A" ).with( "B" ).to( "ab" )
    .map( "C" ).with( "D" ).to( "cd" )
    .map( "E" ).with( "F" ).to( "ef" )
    .build();
Попробуйте теперь вызвать что-то неправильно :)

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


По-правде, я не уверен, что придумать такое было хорошей идеей — встреть я такой адъ в чужом коде, матерился бы в голос. А ведь самому-то теперь будет сложно удержаться :)

Если серьезно, то мне кажется, делать такое в каждом Builder-е это over-engeneering. Сложность понимания "как, и зачем, черт подери, такое здесь наворочено" не окупает небольшого увеличения дуракоустойчивости. Но вот если какая-нибудь изолированная, часто используемая библиотека... Тогда к странным типам возвращаемых значений у методов Builder-а можно и привыкнуть, а вот compile time safety при широком использовании может вполне себя окупить.

Если что — похороны за ваш счет. Я предупреждал :)

UPD:Как мне подсказывает Александр в комментариях — это уже описано как паттерн Wizard. Было бы слишком оптимистично ожидать, что такую штуку еще никто не придумал. Статью рекомендую прочитать, примеры там забавные.

11 комментариев:

  1. >>А ведь самому-то теперь будет сложно удержаться :)
    Очень точно сказано

    ОтветитьУдалить
  2. Натыкался на статью, где похожее делалось без создания лишних интерфейсов на базе generics и без ограничения на порядок определения аргументов. Автор предлагал определить два фантомных типа (true/false), а у билдера сделать параметров-типов по количеству шагов создания. Изначально создавался Builder, потом при установке соответствующего аргумента возвращался
    новый билдер с типом True в соответствующей позиции.

    ОтветитьУдалить
    Ответы
    1. Тэги съелись, Builder<False, False, ... False>

      Удалить
  3. А ссылочку на статью?.. Потому что пока не очень понятно, но если этот метод позволяет обрабатывать случай, когда определенного порядка вызовов нет, просто надо все аргументы задать, то это как раз будет хороший дополняющий метод. В моем методе этот кейс очень громоздко получится

    ОтветитьУдалить
  4. Да, штука любопытная. Но она, пожалуй, не менее монстроподобная чем мой вариант, а гибкости у нее меньше.

    ОтветитьУдалить
  5. А что, прикольно. Мне нравится.
    Не понимаю правда почему в метод map не передавать просто три параметра сразу (-: , но и так недурно.

    ОтветитьУдалить
  6. Ну это ж не будет DSL тогда :) Вообще, пример-то учебный, тут можно и попроще сделать. Сложный пример еще придумать надо -- да и код к нему будет километровый.

    ОтветитьУдалить
  7. Похоже на
    паттерн Wizard. Серьёзный пример использования такого подхода я видел в библиотеке Curator - API для работы с ZooKeeper-ом

    ОтветитьУдалить
  8. @Александр
    Да, это Wizzard и есть. Спасибо за ссылку, приятно знать, что про это уже кто-то догадался

    ОтветитьУдалить
  9. @rappo
    Там, к сожалению, эта сложность видна снаружи -- в подсказках IDE будут видны эти странные типы данных. Это, наверное, главное, что меня смущает

    ОтветитьУдалить