Статьи

Портирование на Гриффон

В « Полете с Гриффоном» я создал простой сценарий в Гриффоне, чтобы показать, как функционирует эта новая среда Swing MVC и каковы некоторые из ее преимуществ. Другой подход, потенциально потенциально более наглядный, заключается в том, чтобы взять существующее настольное Java-приложение и перенести его на Griffon. Это то, что я предлагаю сделать в этой статье.

Сначала загадка. В чем разница между этими двумя скриншотами?

Ответ. Первый — один из стандартных образцов рабочего стола Java, поставляемых с каждым дистрибутивом IDE NetBeans. Это небольшое графическое приложение с JFrame, разработанным в Matisse GUI Builder, с интерфейсом и реализацией для оценки попытки пользователя выяснить анаграммы. А что за второй скриншот? Это то же самое приложение, на этот раз написанное на фреймворке Griffon! Ясно, что результат идентичен, по крайней мере, по номиналу. Вам придется поверить мне, что все остальное работает так же, или просто следуйте инструкциям ниже и убедитесь сами.

Прежде чем рассматривать преимущества и недостатки переноса приложения на Griffon, давайте посмотрим, что означает «перенос приложения на Griffon». Ниже вы видите скриншот обоих приложений. Первое — это оригинальное Java-приложение, то есть то же самое, что вы найдете в IDE NetBeans, а второе — эквивалент Griffon:

Посмотрите на первое приложение, а затем посмотрите на второе. Некоторые из преимуществ второго должны быть уже очевидны. Но прежде чем исследовать их, давайте рассмотрим подход MVC для изучения каждой из частей, которые вы видите выше, и того, что нужно для их портирования:

Кстати, большое спасибо Andres Almiray, который посмотрел на код в этом примере и внес немало изменений. Однако он не несет ответственности за оставшиеся неточности!

Вид

Как видно на предыдущем скриншоте, исходный вид состоял из двух JFrames. Первый предоставляет пользовательский интерфейс для игры Anagram; вторая содержит поле «О программе». Оба были созданы в Matisse GUI Builder, в результате чего приложение было заблокировано в IDE NetBeans.

Исходное представление было очень тесно связано с кодом, который оценивает вводимые пользователем данные, и с кодом, используемым для обработки действий пользователя. Портировать на Griffon означает тратить много времени на разводку кода и тщательно обдумывать, какой бит что делает. Цель представления — сделать его максимально «видимым», без какого-либо кода обработки, загромождающего его.

Само представление Гриффона можно разделить на три части. Во-первых, есть основной вид, который видит пользователь. Дополнительные представления, такие как, в данном случае, поле «О программе», определены в отдельном файле, оканчивающемся на «Диалоги» (хотя в настоящее время это не правило, а просто соглашение, которое имеет смысл в контексте всего остального). Точно так же все действия определяются в отдельном файле с именем, оканчивающимся на «Действия». Зачем все это нужно? Таким образом, вы (и, особенно, все сопровождающие вашего кода) можете сразу же узнать, где искать вещи, что является главной задачей «соглашения о конфигурации», но на этот раз привело к миру рабочего стола Java.

Итак, вот мой файл представления, который называется «AnagramGameGriffonView.groovy», который автоматически создается (и регистрируется в соответствующих местах) с помощью команды griffon create-app: 

Я переписал пользовательский интерфейс игры Anagram следующим образом, используя тот же LayoutManager, что и оригинал, то есть GridBagLayout:

build(AnagramGameGriffonActions)

application(title:'Anagrams', minimumSize:[297, 200], location:[50,50],
    pack:true, locationByPlatform:true) {
    menuBar( id: 'menuBar') {
        menu(text: 'File', mnemonic: 'F') {
            menuItem(aboutAction)
            menuItem(exitAction)
        }
    }
    panel(border:emptyBorder(12)) {
        gridBagLayout()
        label(text: 'Scrambled Word:',
            constraints: gridBagConstraints(
                gridwidth: 1, gridheight: 1,
                fill: HORIZONTAL, anchor: WEST,
                weightx: 0.0, weighty: 0.0,
                insets: [0,0,12,6]))
        textField(id: 'scrambledWord', text: bind {model.scrambledWord},
            columns: 20, editable: false,
            constraints: gridBagConstraints(
                gridwidth: REMAINDER, gridheight: 1,
                fill: HORIZONTAL, anchor: CENTER,
                weightx: 1.0, weighty: 0.0,
                insets: [0,0,12,0]))
        label(text: 'Your Guess:',
            constraints: gridBagConstraints(
                gridwidth: 1, gridheight: 1,
                fill: HORIZONTAL, anchor: WEST,
                weightx: 0.0, weighty: 0.0,
                insets: [0,0,20,6]))
        textField(id: 'guessedWord',
            columns: 20,
            constraints: gridBagConstraints(
                gridwidth: REMAINDER, gridheight: 1,
                fill: HORIZONTAL, anchor: CENTER,
                weightx: 1.0, weighty: 0.0,
                insets: [0,0,20,0]))
        label(id:'feedbackLabel', text: bind {model.feedback},
            constraints: gridBagConstraints(
                gridx: 1, gridy: RELATIVE,
                gridwidth: REMAINDER, gridheight: 1,
                fill: HORIZONTAL, anchor: CENTER,
                weightx: 1.0, weighty: 0.0,
                insets: [0,0,20,0]))
        button(action: guessWordAction, constraints: gridBagConstraints(
                gridx: 1, gridy: RELATIVE,
                gridwidth: 1, gridheight: REMAINDER,
                fill: NONE, anchor: SOUTHEAST,
                weightx: 1.0, weighty: 1.0,
                insets: [0,0,0,6]))
        button(action: nextWordAction, constraints: gridBagConstraints(
                gridwidth: REMAINDER, gridheight: REMAINDER,
                fill: NONE, anchor: SOUTHEAST,
                weightx: 0.0, weighty: 1.0,
                insets: [0,0,0,0]))
        bean( model, guessedWord: bind {guessedWord.text} )
    }
}

Все эти вещи GridBagLayout довольно громоздки, и я был бы счастлив с другим LayoutManager, но, поскольку я пытаюсь воспроизвести оригинал максимально близко, я просто согласился с этой сложностью. В какой-то момент я хотел бы посмотреть, как GroupLayout будет работать в этом контексте, а также MiGLayout.

Далее, посмотрите на строки 19, 39 и 57. О чем все это «связывание»? Свойства «text: bind {model.scrambledWord}» и «text: bind {model.feedback}» связывают текст текстового поля (в первом случае) и текст метки обратной связи (во втором случае) с свойство, определенное в модели. Когда модель изменяется, через что-то, что происходит в контроллере, тексты этих двух компонентов будут автоматически обновляться.

Теперь снова посмотрите на определения двух кнопок выше, а также на линию, с которой начинаются вышеуказанные строки. Как объяснено в «Полете с Гриффоном», комбинация этого начального оператора с двумя объявлениями «action:» в кнопках позволяет заполнить кнопки описаниями, указанными в отдельном файле, который, как указано выше, называется «AnagramGameGriffonActions». Groovy «:

actions {

action( id: 'guessWordAction',
name: "Guess",
closure: controller.guessWord,
accelerator: shortcut('G'),
mnemonic: 'G',
shortDescription: "Guess the given scrambled word"
)

action( id: 'nextWordAction',
name: "New Word",
closure: controller.nextWord,
accelerator: shortcut('N'),
mnemonic: 'N',
shortDescription: "Move to the next word"
)

action( id: 'exitAction',
name: "Exit",
closure: controller.exit,
mnemonic: 'E',
)

action( id: 'aboutAction',
name: "About",
closure: controller.about,
mnemonic: 'A',
)

}

Выше вы видите описания действий для 4-х действий, два («guessWordAction» и «nextWordAction») используются в главном виде игры Anagram, а два других используются для пунктов меню в меню «Файл» (строки С 5 до 10). Поскольку мы все еще в поле зрения, мы не имеем никакого отношения к событиям действия . Как вы можете видеть выше, свойство «closure» указывает на контроллер (который автоматически определяется в файлах конфигурации как «AnagramGameGriffonController»), где мы позже увидим, что на самом деле делают рассматриваемые действия .

Наконец, точно так же, как действия определены в списке замыканий, то же самое относится и к диалогам. Вот файл «AnagramGameGriffonDialogs», который в этом случае определяет только один диалог:

dialog(
title: "About Anagram Game", minimumSize: [123,141],
preferredSize: [298,216], id: "aboutDialog",
modal: true, pack:true, locationByPlatform:true) {

panel(){
gridBagLayout()
textArea(
columns: 25,
editable: false,
lineWrap: true,
rows: 8,
text:"Anagrams\n\nCopyright (c) 2003 Irritable Enterprises, Inc.",
wrapStyleWord: true,
border: null,
focusable: false)
button(text: "Close", actionPerformed: {event ->
aboutDialog.dispose()
},
constraints: gridBagConstraints(
gridx: 0, gridy: 1,
gridwidth: 1, gridheight: 1,
fill: NONE, anchor: SOUTHEAST,
weightx: 0.0, weighty: 0.0,
insets: [0,0,0,0]))
}

}

Обратите внимание, что кнопка «actionPerformed» здесь определена в файле Dialogs, а не в контроллере. Это своего рода обман, но поскольку то, что должно произойти, настолько тривиально, его можно найти здесь, а не в контроллере. (По крайней мере, это моя интерпретация, основанная на одном из примеров, поставляемых с дистрибутивом Griffon.) Здесь я снова использовал GridBagLayout, потому что это то же самое, что и оригинал.

Контроллер

Контроллер — это место, где ВСЕ происходит обработка. Все это происходит в одном файле под названием «AnagramGrameGriffonController.groovy». Прежде всего, нам нужно поместить туда оригинальный код обработки. Это оригинальный код, который определяет, правильно ли введенный пользователем код расшифровывает данное слово:

private static final String[] WORD_LIST = {
"abstraction",
"arithmetic",
"traditional"};

private static final String[] SCRAMBLED_WORD_LIST = {
"batsartcoin",
"ratimhteci",
"rtdatioialn"
};

public String getWord(int idx) {
return WORD_LIST[idx];
}

public String getScrambledWord(int idx) {
return SCRAMBLED_WORD_LIST[idx];
}

public int getSize() {
return WORD_LIST.length;
}

public boolean isCorrect(int idx, String userGuess) {
return userGuess.equals(getWord(idx));
}

Примечание. В реальном приложении гораздо больше слов (т. Е. Больше, чем 3, которые вы видите выше), но я удалил большинство из них, чтобы сэкономить место.

И вот Groovified версия выше:

def WORD_LIST = [
"abstraction",
"arithmetic",
"traditional"]

def SCRAMBLED_WORD_LIST = [
"batsartcoin",
"ratimhteci",
"rtdatioialn"
]

def getWord(int idx) {
WORD_LIST.get(idx);
}

def getScrambledWord(int idx) {
SCRAMBLED_WORD_LIST.get(idx);
}

def getSize() {
WORD_LIST.size;
}

def isCorrect(int idx, String userGuess) {
userGuess.equals(getWord(idx));
}

Вторая область обработки, которой занимается контроллер, — это события действия . Помните, как кнопки и пункты меню в файле View ссылались на описания в файле Actions, который в свою очередь ссылался на контроллер? Ну, вот события действия, все в контроллере. Обратите особое внимание на ссылки на модель (например, «model.feedback»), потому что здесь происходит изменение модели, которое из-за атрибутов «bind», указанных в предыдущем разделе, автоматически приводит к представлению обновляется, создавая таким образом слабосвязанное отношение view / controller:

def guessWord = { evt ->
if (isCorrect(wordIdx, model.guessedWord)){
model.feedback = AnagramGameGriffonModel.FEEDBACK_CORRECT
} else {
model.feedback = AnagramGameGriffonModel.FEEDBACK_INCORRECT
}
view.guessedWord.text = ""
}

def nextWord = { evt ->
wordIdx = (wordIdx + 1) % getSize()
model.scrambledWord = getScrambledWord(wordIdx)
model.feedback = AnagramGameGriffonModel.FEEDBACK_NONE
}

def exit = { evt ->
app.shutdown()
}

def about = { evt ->
showDialog("aboutDialog")
}

private void showDialog( dialogName ) {
def dialog = view."$dialogName"
if( dialog.visible ) return
dialog.pack()
int x = app.appFrames[0].x + (app.appFrames[0].width - dialog.width) / 2
int y = app.appFrames[0].y + (app.appFrames[0].height - dialog.height) / 2
dialog.setLocation(x, y)
dialog.show()
}

Разве это не круто, что события действия находятся в том же файле, что и соответствующий код обработки? В исходном Java-приложении это выглядело как «guessedWordActionPerformed»:

private void guessedWordActionPerformed(java.awt.event.ActionEvent evt)
{
if (wordLibrary.isCorrect(wordIdx, guessedWord.getText())){
feedbackLabel.setText("Correct! Try a new word!");
getRootPane().setDefaultButton(nextTrial);
} else {
feedbackLabel.setText("Incorrect! Try again!");
guessedWord.setText("");
}
guessedWord.requestFocusInWindow();
}

Обратите внимание, как isCorrect вызывается для wordLibrary выше. Больше это не нужно, и вид и контроллер не так переплетены, как это было в ретроспективе.

Еще одна вещь, на которую стоит обратить внимание, это то, как работает метод showDialog выше (строки с 24 по 32, два фрагмента назад). Посмотри на это. Он получает имя диалога и затем открывает связанный диалог в представлении. Связанное имя диалога — это идентификатор диалога, определенный в данном случае как «aboutDialog». Таким образом, один и тот же метод «showDialog» можно использовать для каждого диалога в приложении, т. Е. Вы будете использовать его везде, где это применимо, просто передавая идентификатор диалога, который вы хотите, чтобы метод «showDialog» показывал для вас. Но как метод showDialog находит поле About? Или любой другой диалог? На самом деле, как инициализируется все приложение? Это последний элемент контроллера, который еще не был затронут. Подобно подходу, принятому в «Полете с Гриффоном»,контроллер предоставляет точку входа (и проводку к другим частям триады MVC) следующим образом:

// these will be injected by Griffon
def model
def view

def wordIdx = 0

def loadPages() {
// called inside EDT
model.scrambledWord = getScrambledWord(wordIdx)
}

«LoadPages ()» может быть вызван как угодно. Однако файл «Startup.groovy», который является одним из файлов жизненного цикла приложения, выглядит следующим образом, т. Е. Используется для инициализации всего приложения, а также для добавления файла «AnagramGameGriffonDialogs» в смесь:

def rootBuilder = app.builders.root
def rootController = app.controllers.root
def rootModel = app.models.root

rootBuilder.build(AnagramGameGriffonDialogs)
rootController.loadPages()

И это все, что происходит в контроллере. На самом деле все происходит в контроллере, что именно так и должно быть.

Модель

Наконец, красиво и чисто в отдельном файле, который называется «AnagramGameGriffonModel.groovy», все наши статические переменные найдены, поэтому теперь мы точно знаем, где их искать:

import groovy.beans.Bindable

class AnagramGameGriffonModel {
static final String FEEDBACK_NONE = ""
static final String FEEDBACK_CORRECT = "Correct! Try a new word!"
static final String FEEDBACK_INCORRECT = "Incorrect! Try again!"

@Bindable String scrambledWord = ""
@Bindable String feedback = FEEDBACK_NONE
@Bindable String guessedWord = ""
}

Кроме того, обратите внимание, что у нас есть три привязки здесь. Что такое @Bindable? Как обсуждено здесь , «Когда свойство имеет эту аннотацию, Преобразование AST сгенерирует (если оно не существует) PropertyChangeSupport
объект, соответствующие методы добавления слушателя, а затем установщик, который использует поддержку изменения свойства. Конечным результатом является связанный JavaBeans собственность, с гораздо меньшим количеством стандартного кода. «

Вывод

Итак, наконец, какие основные истины можно извлечь из всего вышеперечисленного? Ну, кроме того факта, что у меня теперь также есть апплет и приложение JNLP (хотя пока что я работаю не совсем корректно, по причинам, которые я не понимаю), я смог использовать Groovy вместо Java для написания кода, создавая точно такой же результат, как и раньше. Преимущества Groovy перед Java заключаются в том, что можно кодировать гораздо быстрее, поскольку многие сложные требования Java (запятые, операторы возврата и т. Д.) Просто не требуются. (Хотя, если они вам все равно нравятся, вы можете продолжать использовать их в Groovy.) Еще одна приятная вещь в Groovy через Java состоит в том, что большая часть обработки файлов выполняется в гораздо меньшем количестве строк кода (посмотрите «Полет с Гриффоном», чтобы увидеть насколько легко можно проанализировать HTML или XML).

Затем, несмотря на тот факт, что вы используете гораздо менее структурированные правила кодирования, которые предоставляет Groovy, Griffon позволяет вам размещать все ваши файлы в очень структурированных местах в вашем приложении. Это структура, которая помогает, а не мешает. В некотором смысле, разница между использованием Griffon и не использованием Griffon такая же, как наличие шкафа для одежды и отсутствие такового. Ваша жизнь все еще будет в порядке без шкафа, за исключением того, что вся ваша одежда будет на полу. 

В процессе портирования, описанном выше, я быстро обнаружил, что Matisse GUI Builder действительно сделал меня очень ленивым программистом. Я даже не мог вспомнить, как установить размер текстового поля. Сначала я попробовал «размер», затем «ширина», затем «длина», и в конце концов понял, что я должен был использовать «столбцы». Ручное кодирование пользовательских интерфейсов всегда было сложным, и GUI Builders, такие как Matisse, пришли на помощь и сделали это очень успешно. Однако, в процессе, разве мы не производим поколение программистов, которые не знают в первую очередь о менеджерах по расположению? «Ах, — вы могли бы сказать, — но вам не нужнобольше знать о менеджерах по компоновке. «Это может быть так. Мне не нужно знать, как починить мойку, потому что это то, для чего нужны сантехники. Но в чрезвычайной ситуации у меня нет первого понятия, с чего начать Не знать о «столбцах» равносильно тому, что не знать, как отключить сеть при разрыве канала. Однако в этом контексте некоторые менеджеры по расположению лучше, чем другие, когда кодируется вручную, и я думаю, что GridBagLayout не лучший ручные кодеры, но, как я уже сказал, я пытался соответствовать исходному приложению.Исследование MiGLayout и GroupLayout в этом контексте является следующим в моей повестке дня.

Наконец, я не обязательно рекомендую переносить все приложения Swing повсюду на Griffon. Я бы сказал, что имеет больше смысла портировать на платформу, которая предлагает, по крайней мере, систему стыковки, потому что это даст возможность для роста приложения. Если бы Griffon предлагал такие виды услуг (т. Е. Сервисы для приложений Swing, например, предоставляемые Spring RCP и платформой NetBeans), он внезапно оказался бы в совершенно другой сфере, которой он, вероятно, меньше интересовался. с другой стороны, для небольших / маленьких приложений (то есть, которые не нуждаются в оконной системе), которые используют Swing, то есть, приложения, которые являются маленькими и останутся маленькими,даже чистое интеллектуальное упражнение (помимо замечательной возможности использования Groovy в структурированном контексте) перемещения его в Griffon — это то, что стоит рассмотреть. Также стоит подумать о том, чтобы подобрать одежду и убрать ее. Говоря об этом, я, вероятно, должен пойти и сделать именно это …