3
\$\begingroup\$

I am creating a Java Swing Application which deals with Geographic and Stellar data and I need a solid framework on which I can build (now) and extend (in future) any functionality I wish to. I am aware of the MVC (traditional), MVP or MVVM design patterns typically used by people. However, I ended up creating something that I think is more simpler and cleaner (my requirements below). Notice the word "I think" and hence this question on SO for the fellow developers/software modelers.

Please point out any design flaws or violation of best practices here in this code. This is not a "can't compile or can't run my code" type of question - it's about design. I'm open to feedback, suggestion or criticism.

Before I post the code, here are the basic requirements I had in mind:

  • The UI/View and the Controller/Listener should not have cyclic dependency on each other. Java Swing itself uses this pattern a lot where the Listeners are anonymous inner classes scattered all over the UI (Frames/Panels) code. In my past Swing based projects, I found that kind of code to be difficult/tedious to maintain in the long run.

  • Cleaner separation between the entities of each logical layer. For example, the client code (my application) deals only with the Listeners/Controllers, which in turn deal only with the UI elements and ActionCommands. ActionCommands are the requests made by the UI (and hence the User). The ActionCommands only deal with the Data Services which fetch the actual data.

That's it. This is the basic premise on which I have created a few very basic classes. Oh and please don't get bored by the word "Basic" here - I just renamed my Domain Names to "Basic" for avoiding confusion for any unfamiliar person reading the code.

Note: For now, I have only used ActionListener and hence could use an ArrayList to store and register the Listener target objects. In future, if I need other Listeners, I can very well use a HashMap to store those.

The client application:

package com.horizon.test.framework;

import java.awt.BorderLayout;
import java.awt.Dimension;

import javax.swing.JDesktopPane;
import javax.swing.JFrame;
import javax.swing.JPanel;

import com.horizon.test.framework.listeners.BasicActionListener;
import com.horizon.test.framework.listeners.BasicUiListener;
import com.horizon.test.framework.panels.BasicDataPanel;


public class GUIDesignClientTestFrame {

    private JFrame frame;


    public GUIDesignClientTestFrame() {

        int width = 1024;
        int height = 768;

        this.frame = new JFrame();
        this.frame.setTitle("Enter your test title");
        this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.frame.setContentPane(new JPanel());
        this.frame.getContentPane().setLayout(new BorderLayout());

        this.frame.setSize(new Dimension(width, height));
        this.frame.setLocationRelativeTo(null);

        JDesktopPane deskTopPane = new JDesktopPane();
        this.frame.add(deskTopPane);

        JPanel basicPanel = this.addBasicPanel();
        this.frame.getContentPane().add(basicPanel);

        this.frame.setVisible(true);
    }


    private JPanel addBasicPanel() {

        BasicUiListener<BasicDataPanel> listener = new BasicActionListener(new BasicDataPanel());
        listener.initializeUIComponents();

        return listener.getPanel();
    }


    public static void main(String[] args) {

        new GUIDesignClientTestFrame();
    }
}

The Listener Interface:

package com.horizon.test.framework.listeners;

import java.awt.event.ActionListener;


public interface BasicUiListener<T> extends ActionListener {

    public void initializeUIComponents();


    public T getPanel();

}

The Listener Impl:

package com.horizon.test.framework.listeners;

import java.awt.event.ActionEvent;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JOptionPane;

import com.horizon.test.framework.command.BasicFetchActionCommand;
import com.horizon.test.framework.command.BasicActionCommand;
import com.horizon.test.framework.model.BasicDataModel;
import com.horizon.test.framework.panels.BasicDataPanel;


public class BasicUiActionListener implements BasicUiListener<BasicDataPanel> {

    private BasicDataPanel              panel;
    private BasicActionCommand actionCommand;


    public BasicUiActionListener(BasicDataPanel basicDataPanel) {

        this.panel = basicDataPanel;
    }


    @Override
    public void initializeUIComponents() {

        System.out.println("initializeUIComponents...");
        this.panel.createUI();
        List<JComponent> componentsToRegister = this.panel.getComponentsToRegister();
        if (componentsToRegister != null && componentsToRegister.size() != 0) {
            for (JComponent jComponent : componentsToRegister) {
                if (jComponent instanceof JButton) {
                    JButton button = (JButton) jComponent;
                    button.addActionListener(this);
                }
            }
        }
    }


    @Override
    public void actionPerformed(ActionEvent e) {

        System.out.println("actionCommand: " + e.getActionCommand());
        if (this.panel == null) {
            throw new RuntimeException("Target panel is null");
        }

        String fetchID = null;
        if ("Submit".equals(e.getActionCommand())) {
            fetchID = this.panel.getInputField().getText();
            System.out.println("fetchID: " + fetchID);
            this.actionCommand = new BasicFetchActionCommand(fetchID);
        }

        Set<String> inputKeys = new HashSet<String>();
        inputKeys.add(fetchID);
        try {
            Map<String, BasicDataModel> dataMap = this.actionCommand.perform();
            System.out.println("dataMap: " + dataMap);
            this.panel.setUIData(inputKeys, dataMap);

        } catch (Exception ex) {
            JOptionPane.showMessageDialog(this.panel, ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
            ex.printStackTrace();
        }
    }


    @Override
    public BasicDataPanel getPanel() {

        return this.panel;
    }
}

The UI Interface:

package com.horizon.test.framework.panels;

import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.JComponent;

import com.horizon.test.framework.model.BasicDataModel;


public interface BasicUiPanel {

    public BasicUiPanel createUI();


    public List<JComponent> getComponentsToRegister();


    public void setUIData(Set<String> inputKeys, Map<String, BasicDataModel> dataMap);

}

The UI Impl:

package com.horizon.test.framework.panels;

import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;

import com.horizon.test.framework.model.BasicDataModel;


public class BasicDataPanel extends JPanel implements BasicUiPanel {

    private static final long serialVersionUID = 7117728205975476509L;

    private JTextField        inputField;

    private List<JComponent>  components;

    private JTextField        resultsField;


    public BasicDataPanel() {

        this.setBorder(BorderFactory.createLineBorder(Color.RED));
        this.setLayout(new GridBagLayout());
    }


    @Override
    public BasicUiPanel createUI() {

        System.out.println("createUI...");
        GridBagConstraints gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.NONE;
        gbc.insets = new Insets(4, 2, 4, 2);
        gbc.anchor = GridBagConstraints.CENTER;

        gbc.weightx = 0.5;
        gbc.ipadx = 4;
        gbc.ipady = 10;

        gbc.gridx = 1;
        gbc.gridy = 0;

        JLabel label = new JLabel("Provide Fetch ID: ");
        this.add(label, gbc);

        gbc.weightx = 1;
        gbc.gridx = 1;
        gbc.gridy = 1;

        this.inputField = new JTextField(36);
        this.add(this.inputField, gbc);

        gbc.weightx = 0.5;
        gbc.gridx = 1;
        gbc.gridy = 2;

        JButton button = new JButton("Submit");
        this.add(button, gbc);
        this.getComponentsToRegister().add(button);

        gbc.gridx = 1;
        gbc.gridy = 3;

        JLabel resultLabel = new JLabel("Result: ");
        this.add(resultLabel, gbc);

        gbc.weightx = 1;
        gbc.gridx = 1;
        gbc.gridy = 4;

        this.resultsField = new JTextField(36);
        this.add(this.resultsField, gbc);

        return this;
    }


    @Override
    public List<JComponent> getComponentsToRegister() {

        if (this.components == null) {
            this.components = new ArrayList<JComponent>();
        }

        return this.components;
    }


    @Override
    public void setUIData(Set<String> inputKeys, Map<String, BasicDataModel> dataMap) {

        System.out.println("setUIData...");
        if (inputKeys != null && inputKeys.size() > 0) {
            if (dataMap != null && dataMap.size() > 0) {
                for (String key : inputKeys) {
                    this.resultsField.setText(dataMap.get(key).toString());
                }
            }
        }
    }


    public JTextField getInputField() {

        return this.inputField;
    }
}

The Model Interface:

package com.horizon.test.framework.model;

import java.io.Serializable;


public interface BasicDataModel extends Serializable {

}

The Model Impl:

package com.horizon.test.framework.model;

public class NameData implements BasicDataModel {

    private static final long serialVersionUID = 5583130086376137920L;

    private String            name;


    public NameData(String text) {

        this.name = text;
    }


    public String getName() {

        return this.name;
    }


    public void setName(String name) {

        this.name = name;
    }


    @Override
    public String toString() {

        return "NameData [name=" + this.name + "]";
    }

}

The Service Interface:

package com.horizon.test.framework.command;

import java.util.Map;

import com.horizon.test.framework.exception.InvalidDataException;
import com.horizon.test.framework.exception.BasicServiceException;
import com.horizon.test.framework.model.BasicDataModel;
import com.horizon.test.framework.services.BasicDataService;


public interface BasicActionCommand {

    public void setService(BasicDataService service);


    public Map<String, BasicDataModel> perform() throws InvalidDataException, BasicServiceException;

}

The Service Impl:

package com.horizon.test.framework.command;

import java.util.Map;

import com.horizon.test.framework.exception.InvalidDataException;
import com.horizon.test.framework.exception.BasicServiceException;
import com.horizon.test.framework.model.BasicDataModel;
import com.horizon.test.framework.services.BasicFetchDataService;
import com.horizon.test.framework.services.BasicDataService;


public class BasicFetchActionCommand implements BasicActionCommand {

    private BasicDataService service;

    private String                fetchID;


    public BasicFetchActionCommand(String fetchID) {

        this.fetchID = fetchID;
    }


    @Override
    public Map<String, BasicDataModel> perform() throws InvalidDataException, BasicServiceException {

        System.out.println("perform...");
        if (this.fetchID == null || "".equals(this.fetchID)) {
            throw new InvalidDataException("FetchID cannot be null");
        }

        if (this.service == null) {
            this.service = new BasicFetchDataService();
        }
        System.out.println("Performing for fetchID: " + this.fetchID);
        return this.service.fetchData(this.fetchID);
    }


    @Override
    public void setService(BasicDataService service) {

        this.service = service;
    }
}

Other referenced classes:

package com.horizon.test.framework.exception;

public class BasicServiceException extends Exception {

    private static final long serialVersionUID = -8814978741764323765L;


    public BasicServiceException(String message) {

        super(message);
    }
}


package com.horizon.test.framework.exception;

public class InvalidDataException extends Exception {

    private static final long serialVersionUID = 783547201407264505L;


    public InvalidDataException(String message) {

        super(message);
    }
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Your current title is too generic. Please state what your code does in the title and leave the concerns to the body of the question. \$\endgroup\$ – Mast Jun 21 '18 at 7:32
  • \$\begingroup\$ @Mast Thanks. Does it sound better now? \$\endgroup\$ – Horizon Jun 21 '18 at 7:40
  • \$\begingroup\$ Not what I had in mind. I've modified the title for you to show what kind of titles we prefer here. The title prompt of Code Review says: "State the task that your code accomplishes. Make your title distinctive." Feel free to edit the title again if you don't like it, keeping that advice in mind. \$\endgroup\$ – Mast Jun 21 '18 at 8:11
  • \$\begingroup\$ @Mast, Okay, I get the idea. But the code sample that I posted doesn't even even have any reference to Stellar or Geographic data. it is just a bare minimum custom framework code that attempts to show how the different Class Entities can be wired to avoid a cyclic dependencies typically seen in a Java Swing application. "Removing cyclic dependency between the UI and the Controller classes in a Java Swing application" is more like what the code does. What do you say? :) Thanks! \$\endgroup\$ – Horizon Jun 21 '18 at 8:33
  • 1
    \$\begingroup\$ Perfect. That's more like it. Now, I just hope people run the code and comment how well/unwell it is. Thanks Mast. \$\endgroup\$ – Horizon Jun 21 '18 at 9:30

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.