Статьи

Чистый подход к моделям Wicket

Одной из основных концепций веб-фреймворка Apache Wicket является модель и IModel как ее интерфейс программирования. Компоненты калитки в значительной степени зависят от моделей, что делает их важными элементами архитектуры. Apache Wicket — это структура с сохранением состояния, которая хранит страницы и их компоненты в хранилище страниц, которое обычно находится в сеансе HTTP. Компоненты создают публичный вид из содержимого моделей. Неправильное использование моделей может привести к неловким побочным эффектам, таким как страницы, не обновляющие свое содержимое, или огромному потреблению памяти в вашем приложении. И большинство проблем с новыми пользователями Wicket, из того, что я видел, связано с правильным использованием моделей. В этом посте я попытаюсь объяснить, как и какие модели следует использовать в различных случаях использования.

Наиболее простой из реализаций Wicket IModel является класс Model. Это в основном простой заполнитель для содержимого модели. Это подходит для ситуаций, когда содержимое модели не занимает много памяти и больше похоже на константу. Простая строка может быть хорошим кандидатом на содержание модели.

1
IModel<String> name = new Model<String>("John");

Вы должны понимать, что при создании объекта Model его содержимое остается неизменным, даже если страница, содержащая модель, обновляется. Такое поведение вызвано использованием статической модели. Динамические модели могут изменять фактическое содержимое каждый раз, когда значение запрашивается из модели. Использование статической модели вместо динамической является довольно распространенной ошибкой для новых пользователей Wicket. Если концепция статических и динамических моделей вам не ясна, вам следует проверить главу 4 «Понимание моделей» из книги « Калитка в действии» .

1
2
3
4
5
6
IModel<Date> date = new Model<Date>() {
    @Override
    public Date getObject() {
        return new Date();
    }
};

Приведенная выше модель динамической даты создается как анонимный класс из Model путем переопределения метода getObject () . Модель всегда возвращает свежую копию даты, которая содержит текущее время.

Wicket — это веб-инфраструктура с отслеживанием состояния, и модели обычно хранятся в сеансе HTTP пользователя. Данные в модели, скорее всего, выбираются из базы данных или через какой-либо веб-сервис. Если мы используем подход статической модели, мы скоро израсходуем много памяти и не получим новый вид из источника данных, если страница будет перезагружена. Если мы решим использовать динамическую модель, как мы это делали с объектом Date, мы могли бы в конечном итоге сделать много запросов к базе данных или вызвать веб-сервисы в пределах одной загрузки страницы. Анонимный внутренний класс тоже не очень красивый код. У Wicket есть встроенное решение для этой проблемы, LoadableDetachableModel, которое кэширует содержимое модели до тех пор, пока его безопасно удалить, предоставляя эффективную, но все еще динамичную модель.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PersonModel extends LoadableDetachableModel<Person> {
  
    private Long id;
    private PersonService personService;
  
    public PersonModel(final Long id, final PersonService personService) {
        super();
        this.id = id;
        this.personService = personService;
    }
  
    public PersonModel(final Person person, final PersonService personService) {
        super(person);
        this.id = person.getId();
        this.personService = personService;
    }
  
    @Override
    protected Person load() {
        return personService.findById(id);
    }
}

Приведенный выше пример создает загружаемую и съемную модель для загрузки одного человека по его уникальному идентификатору. Модель имеет два конструктора по причине. Один для создания отложенной загруженной модели в отдельном состоянии, которая будет загружать содержимое при первом вызове метода getObject () , а второй — для создания модели в подключенном состоянии, не требующей вызова PersonService, когда метод getObject () называется. Хорошей практикой является предоставление этих двух конструкторов для всех моделей LoadableDetachableModel. Вы можете создать модель в подключенном состоянии, когда у вас уже есть содержимое, или создать ее в отключенном состоянии, когда доступен только идентификатор.

Предостережение о загружаемых съемных моделях — это частные поля модели. Wicket хранит модель вместе с компонентами в хранилище страниц, что может потребовать сериализации модели. Когда модель сериализуется, также закрываются частные поля (фактическое содержимое отсоединяется). Наше поле id не является проблемой, потому что оно сериализуемо, но может быть проблема PersonService. Обычно интерфейсы сервисного уровня по умолчанию не сериализуемы. Существует как минимум два достойных решения этой проблемы: либо сделать ваши сервисы сериализуемыми, либо лучше, обернуть сервис в сериализуемый прокси. Поведение прокси может быть достигнуто, например, с помощью интеграции в различные структуры внедрения зависимостей, например. Весна (калитка-весна) или Guice (калитка-guice). Модули интеграции гарантируют, что служебные прокси-серверы будут сериализуемыми при их внедрении.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public class ProfilePage extends WebPage {
  
    @SpringBean
    private PersonService personService;
  
    public ProfilePage(final PageParameters parameters) {
        super(parameters);
  
        Long id = parameters.get("id").toLongObject();
        IModel<Person> personModel =
                new PersonModel(id, personService);
        add(new ProfilePanel("profilePanel", personModel));
    }
}

В приведенном выше примере используется интеграция wicket-spring для добавления сервиса person на нужную страницу. Аннотация @SpringBean предоставляет сериализуемый прокси, поэтому при создании модели person нам не нужно беспокоиться о сериализации службы. Wicket не допускает инъекцию конструктора, потому что вся магия внедрения фактически происходит, когда мы вызываем конструктор super () . Это означает, что у нас есть введенные значения, инициализированные после завершения базового конструктора Component. Просто не забудьте использовать сериализуемые идентификаторы или локаторы для данных и использовать сериализуемые прокси для сервисов.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class PersonFriendsModel extends AbstractReadOnlyModel<String> {
  
    private IModel<Person> personModel;
  
    public PersonFriendsModel(final IModel<Person> personModel) {
        super();
        this.personModel = personModel;
    }
  
    @Override
    public String getObject() {
        Person person = personModel.getObject();
        Iterable<String> friendNames =
                Iterables.transform(person.getFriends(),
                        new PersonNameFunction());
        return person.getName()
                + "'s friends are "
                + Joiner.on(", ").join(friendNames);
    }
  
    @Override
    public void detach() {
        personModel.detach();
    }
  
    private class PersonNameFunction implements Function<Person, String> {
        public String apply(final Person input) {
            return input.getName();
        }
    }
}

Здесь мы строим модель, которая могла бы составить PersonModel, который мы создали ранее. Мы используем его как источник, чтобы создать хороший список друзей этого человека. Модель, которую мы расширяем — это AbstractReadOnlyModel, которую предлагает нам Wicket. Это в основном нормальная модель, но она не позволяет устанавливать содержимое модели. Это имеет смысл, потому что мы создаем объединенную строку, и было бы неудобно анализировать подобный список и устанавливать друзей этого человека из этого списка. Мы используем эту модель только для чтения, чтобы показать список друзей в представлении. Вы можете увидеть аналогию представления здесь, мы все еще используем ту же модель домена Person, но выставляем из нее пользовательское представление с помощью модели. Обратите внимание, что мы делегируем метод detach составной модели. Это гарантирует, что если у нас нет прямой ссылки на составную модель в каком-либо компоненте, мы все равно сможем отсоединить ее от модели друзей. Многократный вызов метода detach () не причиняет вреда, поэтому его можно использовать даже из нескольких компонентов.

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

Последнее, о чем я буду говорить, это модели свойств. Они широко используются во многих приложениях Wicket, и даже книга Wicket in Action рекомендует их использовать, но у них есть некоторые нежелательные функции. Модели свойств быстро кодируются и просты в использовании, но они делают ваш код «строго типизированным». И это то, что мы не хотим в типобезопасном языке, таком как Java.

1
2
PropertyModel<String> nameModel =
        new PropertyModel<String>(personModel, "name");

Мы создаем модель из имени человека, используя PropertyModel. Модель имени может использовать либо прямой объект Person, либо IModel, который предоставляет нам этого человека. Эта функция может быть достигнута путем реализации интерфейса IChainingModel Wicket. У нас здесь есть две проблемы. Первый тип названия модели. Мы определяем, что имя должно быть String, но на самом деле компилятор не может убедиться, что имя связано со свойством String getName () JavaBean. Вторая проблема связана с рефакторингом, мы используем значение String для определения имени свойства. Если вы выполните рефакторинг объекта Person с помощью вашей IDE и передадите метод getName () в getLastName () , ваше приложение будет разорвано , и компилятор снова не сможет это заметить.

Остановимся здесь на некоторое время, чтобы взглянуть на интерфейс IChainingModel. Его основное назначение — разрешить использование простого объекта или цепной модели в качестве содержимого модели. PropertyModel может использоваться с моделью, которая предоставляет человека или с простым человеком объекта. Если вы посмотрите на исходный код PropertyModel, вы заметите, что реализация IChainingModel требует приведения и, на мой взгляд, кода котельной пластины. Модель PersonFriendsModel, которую мы создали ранее, может быть реорганизована для реализации IChainingModel, чтобы вместо использования только модели в качестве контента она также поддерживала бы непосредственное участие человека. Это действительно необходимо? На самом деле, нет. Если мы хотим использовать обычного человека, мы могли бы создать новую статическую модель из человека, предоставляющего нам ту же функциональность, что и IChainingModel.

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

1
2
3
4
5
6
7
8
9
public class PersonForm extends Form<Person> {
  
    public PersonForm(final String id, final IModel<Person> personModel) {
        super(id, new CompoundPropertyModel<Person>(personModel));
  
        add(new TextField<String>("name"));
        add(new TextField<Integer>("age"));
    }
}

Здесь мы создаем форму для отображения полей ввода имени и возраста человека. Ни к одному из текстовых полей не прикреплена модель, но мы все еще можем связать компонент «имя» с именем человека, а компонент «возраст» — с возрастом человека. Модель составного свойства, которую мы создали в конструкторе, является корневой моделью формы и автоматически связывает компоненты формы со свойствами объекта по идентификаторам компонентов. Те же проблемы применимы и к составной модели свойств, которые применяются к модели свойств, но мы даже добавим еще одну. Теперь мы привязываем идентификатор компонента прямо к имени свойства. Это означает, что у нас есть зависимость от шаблона HTML прямо до имени свойства объектов домена. Это звучит не очень разумно.

Вам решать, хотите ли вы использовать модели свойств, но, по моему мнению, их следует избегать из-за проблем, описанных ранее. Есть такие проекты, как bindgen-wicket, которые пытаются реализовать поведение моделей свойств без потери безопасности типов. Но даже bindgen не очень хорошо работает с рефакторингом. Как мы реализуем модель имени в типе и рефакторинг безопасным способом тогда?

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class NameModel implements IModel<String> {
  
    private IModel<Person> personModel;
  
    public NameModel(final IModel<Person> personModel) {
        this.personModel = personModel;
    }
  
    @Override
    public String getObject() {
        return personModel.getObject().getName();
    }
  
    @Override
    public void setObject(String object) {
        personModel.getObject().setName(object);
    }
  
    @Override
    public void detach() {
        personModel.detach();
    }
}

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

Wicket также предоставляет нам ResourceModel и StringResourceModel, которые предоставляют мощный инструмент для создания локализованного контента для ваших компонентов. Я не буду обсуждать их в этом посте, потому что в книге Wicket in Action есть хороший справочник для них. Я попытался привести некоторые примеры из жизни, как использовать разные модели и какова их цель. Я надеюсь, что это дало вам больше знаний о моделях Wicket и о том, как их правильно использовать.

Ссылка: Чистый подход к моделям Wicket от нашего партнера JCG Тапио Раутонена в блоге RAINBOW WORLDS .