Wicket+лямбды: типобезопасная и лаконичная реализация IModel

в 8:37, , рубрики: java, java 8, lambda, method reference, Веб-разработка

Стандартная задача при разработке веб-приложения: есть объект данных, требуется эти данные отобразить (вывести в HTML). В Apache Wicket данные для этого привязываются к компонентам (которые и будут заниматься отображением) с помощью моделей (реализующих интерфейс IModel).

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

    T getObject();

Абстракция простая и лаконичная, но не всё так просто на практике. Под катом — сказ о том, как Java 8 помогла победить многословность и небезопасность стандартных подходов.

Данные

Подопытный класс данных:

public class User implements Serializable {
    private final String name;
    private final int age;

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

Попытка №1: мы писали, мы писали...

Вот как можно подойти к решению поставленной задачи:

public class AbstractReadOnlyModelPanel extends Panel {
    public AbstractReadOnlyModelPanel(String id, IModel<User> model) {
        super(id, model);

        add(new Label("name", new AbstractReadOnlyModel<String>() {
            @Override
            public String getObject() {
                return model.getObject().getName();
            }
        }));
        add(new Label("age", new AbstractReadOnlyModel<Integer>() {
            @Override
            public Integer getObject() {
                return model.getObject().getAge();
            }
        }));
    }
}

Всё дёшево, надёжно и практично: создаём анонимный субкласс AbstractReadOnlyModel. Работает быстро, выглядит понятно. Одна проблема — из-за использования того самого анонимного класса код получается громоздкий: 6 строк на компонент — не шутка.

Попытка №2: стреляем в направлении ноги

Теперь попробуем PropertyModel:

public class PropertyModelPanel extends Panel {
    public PropertyModelPanel(String id, IModel<User> model) {
        super(id, model);

        add(new Label("name", PropertyModel.of(model, "name")));
        add(new Label("age", PropertyModel.of(model, "age")));
    }
}

Ух ты, гораздо компактнее. Но в бочке мёда есть немало дёгтя:

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

На сцену выходят лямбды

К счастью, Java 8 уже давно выпущена, и лямбды вместе с Method References уже спешат на помощь:

public class GetterModel<E, P> extends AbstractReadOnlyModel<P> {
    private final E entity;
    private final IModel<E> entityModel;
    private final IPropertyGetter<E, P> getter;

    private GetterModel(E entity, IModel<E> entityModel, IPropertyGetter<E, P> getter) {
        this.entity = entity;
        this.entityModel = entityModel;
        this.getter = getter;
    }

    public static <E, P> GetterModel<E, P> ofObject(E entity, IPropertyGetter<E, P> getter) {
        Objects.requireNonNull(entity, "Entity cannot be null");
        Objects.requireNonNull(getter, "Getter cannot be null");

        return new GetterModel<>(entity, null, getter);
    }

    public static <E, P> GetterModel<E, P> ofModel(IModel<E> entityModel, IPropertyGetter<E, P> getter) {
        Objects.requireNonNull(entityModel, "Entity model cannot be null");
        Objects.requireNonNull(getter, "Getter cannot be null");

        return new GetterModel<>(null, entityModel, getter);
    }

    @Override
    public P getObject() {
        return getter.getPropertyValue(getEntity());
    }

    private E getEntity() {
        return entityModel != null ? entityModel.getObject() : entity;
    }
}

public interface IPropertyGetter<E, P> {
    P getPropertyValue(E entity);
}

Ну и сразу же пример, переписанный с этой реализацией модели:

public class GetterModelPanel extends Panel {
    public GetterModelPanel(String id, IModel<User> model) {
        super(id, model);

        add(new Label("name", GetterModel.ofModel(model, User::getName)));
        add(new Label("age", GetterModel.ofModel(model, User::getAge)));
    }
}

Почти так же кратко, как и в примере с PropertyModel, к тому же:

  • типобезопасно: компилятор проверит, что тип совпадает (если, конечно, задействовать более разборчивый, чем Label, компонент);
  • гораздо более безопасно с точки зрения возможности опечаток: если вы опечатаетесь, компилятор это скорее всего отловит;
  • не используется рефлексия.

По сравнению с PropertyModel, правда, есть и некоторые недостатки:

  • GetterModel — только для чтения, тогда как PropertyModel позволяет и писать. Добавление ещё и setter-а лишает задумку элегантности, к тому же добавляет ещё один источник ошибок (появляется возможность указать setter от одного свойства, а getter от другого).
  • PropertyModel позволяет обращаться к вложенным свойствам с помощью выражений вида «outerObject.itsProperty.propertyOfProperty».

Зато есть приятный бонус: аналог магической возможности PropertyModel использовать в качестве источника данных и модели, и POJO реализуется без всякой магии: мы просто добавили два фабричных метода (ofModel() и ofObject()).

Ссылки

  1. Apache Wicket Framework
  2. Java: Lambda Expressions
  3. Java: Method References

Автор: rpuch

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js