SwingでUIアーキテクチャパターン

@ITさんのこちらの記事「開発者が知っておくべき、6つのUIアーキテクチャ・パターン」に触発されて、あとは自分の考えを纏めるために、以上の記事で紹介されているアーキテクチャパターンをSwingとJavaで実装して見ようかと思います。
サンプルは同じく「BMIによる肥満度判断」を使用させて頂きます。


まずは「フォームとコントロール」から、全体のコードは以下の通り。

import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import java.awt.Color;

public class Form extends javax.swing.JFrame implements DocumentListener{

    public static void main( String args[] ){

        SwingUtilities.invokeLater( new Runnable(){
            public void run(){
                new Form().setVisible( true );
            }
        } );
    }

    /**
     * Creates new form Form
     */
    public Form(){
        initComponents();
        initListeners();
    }

    private void initListeners(){
        heightField.getDocument().addDocumentListener( this );
        weightField.getDocument().addDocumentListener( this );
    }

    public void insertUpdate( DocumentEvent e ){
        textChanged();
    }

    public void removeUpdate( DocumentEvent e ){
        textChanged();
    }

    public void changedUpdate( DocumentEvent e ){
    }

    public void textChanged(){
        float height;
        float weight;

        try{
            height =Float.valueOf( heightField.getText().trim() );
            weight = Float.valueOf( weightField.getText().trim() );

        } catch ( NumberFormatException ignored ){
            bmiField.setText( "" );
            bmiField.setBackground( Color.WHITE );
            return;
        }

        if( height == 0f ){
            return;
        }

        float bmi = weight / ( height * height );
        bmiField.setText( Float.toString( bmi ) );

        Color bg;
        if(bmi < 18.5f){
            bg = Color.WHITE;
        } else if( bmi < 20.0f){
            bg = Color.YELLOW;
        }else if( bmi < 30.0f ){
            bg = Color.ORANGE;
        }else{
            bg = Color.RED;
        }

        bmiField.setBackground( bg );
    }

    /**
     * This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings( "unchecked" )
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents(){
        java.awt.GridBagConstraints gridBagConstraints;

        bmiPanel = new javax.swing.JPanel();
        heightLabel = new javax.swing.JLabel();
        heightField = new javax.swing.JTextField();
        mLabel = new javax.swing.JLabel();
        weightLabel = new javax.swing.JLabel();
        weightField = new javax.swing.JTextField();
        kgLabel = new javax.swing.JLabel();
        bmiLabel = new javax.swing.JLabel();
        bmiField = new javax.swing.JTextField();

        setDefaultCloseOperation( javax.swing.WindowConstants.EXIT_ON_CLOSE );
        setTitle( "BMI checker" );
        setLocationByPlatform( true );
        setResizable( false );

        bmiPanel.setBorder( javax.swing.BorderFactory.createEmptyBorder( 5, 5, 5, 5 ) );
        bmiPanel.setLayout( new java.awt.GridBagLayout() );

        heightLabel.setText( "Height:" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 6 );
        bmiPanel.add( heightLabel, gridBagConstraints );

        heightField.setName( "heightLabel" ); // NOI18N
        heightField.setPreferredSize( new java.awt.Dimension( 100, 21 ) );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets( 4, 4, 4, 4 );
        bmiPanel.add( heightField, gridBagConstraints );

        mLabel.setText( "m" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 2 );
        bmiPanel.add( mLabel, gridBagConstraints );

        weightLabel.setText( "Weight:" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 6 );
        bmiPanel.add( weightLabel, gridBagConstraints );

        weightField.setName( "weightLabel" ); // NOI18N
        weightField.setPreferredSize( new java.awt.Dimension( 100, 21 ) );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.insets = new java.awt.Insets( 4, 4, 4, 4 );
        bmiPanel.add( weightField, gridBagConstraints );

        kgLabel.setText( "kg" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 2;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 2 );
        bmiPanel.add( kgLabel, gridBagConstraints );

        bmiLabel.setText( "BMI:" );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START;
        gridBagConstraints.insets = new java.awt.Insets( 4, 2, 4, 6 );
        bmiPanel.add( bmiLabel, gridBagConstraints );

        bmiField.setBackground( new java.awt.Color( 255, 255, 255 ) );
        bmiField.setEditable( false );
        bmiField.setDisabledTextColor( new java.awt.Color( 0, 0, 0 ) );
        bmiField.setName( "bmiLabel" ); // NOI18N
        bmiField.setPreferredSize( new java.awt.Dimension( 100, 21 ) );
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 2;
        gridBagConstraints.insets = new java.awt.Insets( 4, 4, 4, 4 );
        bmiPanel.add( bmiField, gridBagConstraints );

        getContentPane().add( bmiPanel, java.awt.BorderLayout.CENTER );

        pack();
    }// </editor-fold>//GEN-END:initComponents

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JTextField bmiField;
    private javax.swing.JLabel bmiLabel;
    private javax.swing.JPanel bmiPanel;
    private javax.swing.JTextField heightField;
    private javax.swing.JLabel heightLabel;
    private javax.swing.JLabel kgLabel;
    private javax.swing.JLabel mLabel;
    private javax.swing.JTextField weightField;
    private javax.swing.JLabel weightLabel;
    // End of variables declaration//GEN-END:variables
}

今回、SwingのGUI部分はNetBeansGUIビルダー Matisseを使用して作成してます。
Matisseは非常に使いやすいので、Swingをやる方にはNetBeansがお勧めです。
閑話休題、以下解説。


処理はすべてイベントハンドラへ、ということなので、BMIの計算・画面を更新する処理はtextChanged() メソッドに実装しています。
このメソッドはinsertUpdateメソッド及びremoveUpdateメソッドから呼び出しています。
insertUpdateメソッド及びremoveUpdateメソッドは、インターフェイス DocumentListener が宣言するメソッドであり、これはJTextFieldのモデルとなるDocumentに登録するリスナーです。

   public void textChanged(){
        float height;
        float weight;

        try{
            height =Float.valueOf( heightField.getText().trim() );
            weight = Float.valueOf( weightField.getText().trim() );

        } catch ( NumberFormatException ignored ){
            bmiField.setText( "" );
            bmiField.setBackground( Color.WHITE );
            return;
        }

        if( height == 0f ){
            return;
        }

        float bmi = weight / ( height * height );
        bmiField.setText( Float.toString( bmi ) );

        Color bg;
        if(bmi < 18.5f){
            bg = Color.WHITE;
        } else if( bmi < 20.0f){
            bg = Color.YELLOW;
        }else if( bmi < 30 ){
            bg = Color.ORANGE;
        }else{
            bg = Color.RED;
        }

        bmiField.setBackground( bg );
    }


そしてこのリスナーを身長の入力欄と、体重の入力欄のJTextFieldに登録しているので、どちらかの入力値が変更されるごとにinsertUpdateメソッド及びremoveUpdateメソッド経由で、textChanged()メソッドが呼び出され、画面が更新されることになります。

    private void initListeners(){
        heightField.getDocument().addDocumentListener( this );
        weightField.getDocument().addDocumentListener( this );
    }


このコードのメリットとしては、クラスが一つで済むのと、すべての処理がtextChanged()にあるので(この程度の規模の処理であれば)読みやすいということです。
反面、画面を構築するコードとBMIを計算するコード、画面を更新するコードが混ざり合っているためコードの共有やユニットテストがやりにくいことです。また、リソースを使う画面を表示する処理が実際に動いてしまうため、テストコードの実行が非常に遅くなってしまいます。


次回、MVCに続きます。