Take the 2-minute tour ×
Programmers Stack Exchange is a question and answer site for professional programmers interested in conceptual questions about software development. It's 100% free, no registration required.

I have a list box with several elements, let it be web servers (tomcat, iis etc). For each list box value, UI must have different views. For example, if we choose IIS, user name and password fields appear. If we choose tomcat, some additional fields appear depending on user OS - IP and port for linux and path for windows.

My current approach is the following: - I created two enums. The first one for different OS (windows, linux, mac os x. The second one for servers: iis, nginx, tomcat etc. - I have a method that returns the OS enum value. - When user makes a choice, I determine it and basing on this choice I check user's OS. And after it I determine what UI elements to hide/add.

Here's some sort of pseudo-code for what I have:

//OS enum
public enum OS {
    WINDOWS, LINUX, MAC_OS_X;
}

//Servers enum
public enum SERVERS {
    TOMCAT, IIS, NGINX;
}

//Server Listbox Handler
private void chooseServer() {
        //...get value from list box
        switch(server) {
            case TOMCAT:
                changeUI(getOS()); //getOS() returns OS enum value
                break;
            case IIS:
                //IIS is for windows only
                changeUI(OS.WINDOWS); 
                break;
            case NGNIX:
                //Temporary support only linux
                changeUI(OS.LINUX);
                break;
        }
    }

//UI changes here
private void changeUI(OS os) {
        switch (os) {
            case WINDOWS:
                userNameField.setEnabled(true);
                userPasswordField.setEnabled(true);
                ipField.setVisible(false);
                portField.setVisible(false);
                serverPathField.setVisible(true);
                break;
            case LINUX:
                userNameField.setEnabled(true);
                userPasswordField.setEnabled(true);
                ipField.setVisible(true);
                portField.setVisible(true);
                serverPathField.setVisible(false);
                break;
            case MAC_OS_X:
                userNameField.setEnabled(false);
                userPasswordField.setEnabled(false);
                ipField.setVisible(false);
                portField.setVisible(false);
                serverPathField.setVisible(false);
                break;
        }
    }

I don't like the moment when other OS or new servers will be added. These switch operators will grow more and become more complicated, I don't speak about maintainability of this code. Are there any standard approaches (patterns) for such tasks that help to avoid switch/if...else grow and to make code more generic, expandable, readable and supportable?


I tried to implement Adapter pattern suggested by Neil and ran at some obscure moment. Here's some pseudo-code:

public abstract class EditorAdapter {
    protected boolean firstInit = true;
    public abstract void adapt(Panel panel);
}

//--------

public class IISWindowsEditorAdapter extends EditorAdapter {
    @Override
    public void adapt(Panel panel) {
        if(firstInit) {
            firstInit = false;

            panel.add(userNameField);
            //add other elements
        }
    }
}

//--------

public class TomcatEditorAdapter extends EditorAdapter {
    private OS os = getOS();

    @Override
    public void adapt(Panel panel) {
        if(firstInit) {
            firstInit = false;

            if(OS == OS.LINUX) {
                panel.add(userNameField);
                //add other elements
            } else if (OS == OS.WINDOWS) {
                //add elements
            } //... else if
        }
    }
}

//--------

public class Editor extends DialogEditor { //extends Dialog with protected Panel field (subPanel)
    public Editor() {
        initialize();
    }

    public void initialize() {
        //...
        main.add(createPanel()); //protected VerticalPanel main;
        //...
    }

    public void createPanel() {
        //listBox initialization
        commonPanel.add(listBox); //panel with common elements
        listBox.addClickHandler(new ChangeHandler() {
            @Override
            public void onChange(ChangeEvent event) {
                selectServer();
            });
        }
        //... add other common elements
    }

    private void selectServer() {
        //... get selection
        switch(server) {
            //Here something should be done in order to control different layouts.
            //At the moment, each choice adds more elements in addition to existing ones
            //As a variant, one can create class fields for each adapter and manipulate with them using lazy initialization
            case IIS:
                new WindowsEditorAdapter().adapt(subpanel);
                break;
            case TOMCAT:
                new TomcatEditorAdapter().adapt(subpanel);
                break;
        }
    }
}

The problem is that I have several tables/panels in my Editor that at the end are added to main panel. In general, it looks like:

  • we open a dialog

  • choose web-server

  • some elements change inside the dialog according to our choice (and some choices are affected with OS user is using).

That's why first UI initialization with adapter doesn't work here. At least I didn't catch how to implement this adapter for logic above.

share|improve this question
    
Why does changeUI only take one parameter, but you call it with two? –  Doc Brown Apr 23 '13 at 15:54
    
@DocBrown, Oh, I accidentaly mixed two approaches I tried to implement. –  Dragon Apr 23 '13 at 18:28
add comment

3 Answers

up vote 3 down vote accepted

Note: Edited to reflect Dragon's modified requirement to make it work upon selection.

A common pattern to generalize this sort of thing is an adapter. Have an abstract class called GUIAdapter which can tinker with the individual gui aspects of your program according to the settings:

public abstract class GUIAdapter {
    public abstract void adapt(GUIFrame frame);
    public abstract void remove(GUIFrame frame);
}

Then override GUIAdapter with adapters suitable for each type of situation:

public class WindowsGUIAdapter extends GUIAdapter {
    public void adapt(GUIFrame frame) {
        // Things to do when windows operating system
    }
    public void remove(GUIFrame frame) {
        // Remove trace of WindowsGUIAdapter
    }
}

public class LinuxGUIAdapter extends GUIAdapter {
    public void adapt(GUIFrame frame) {
        // Things to do when linux operating system
    }
    public void remove(GUIFrame frame) {
        // Remove trace of LinuxGUIAdapter 
    }
}

public class TomcatGUIAdapter extends GUIAdapter {
    public void adapt(GUIFrame frame) {
        // Things to do when using tomcat server engine
    }
    public void remove(GUIFrame frame) {
        // Remove trace of TomcatGUIAdapter 
    }
}

// et cetera ...

These adapters have as much or as little control as you let them have over your frame. When the operating system or server is known, you call "remove" on all existing adapters, add the appropriate adapter for the current selection, and then call "adapt" to apply the configuration for current selection. I would recommend that you would add a panel to the frame and remove it when "remove" is called. Keep a reference to JPanel as a private member of your extended class of GUIAdapter in order to be able to remove it easily afterwards.

Notice that I didn't make a WindowsTomcatGUIAdapter. The idea is that your GUIFrame class (I'll assume that's what it's called) houses a list of adapters which get called to modify the state of the frame according to the selection without knowing how they work.

public class GUIFrame extends JFrame {
    // Other protected variables here
    private List<GUIAdapter> adapters = new ArrayList<GUIAdapter>();

    private JList serverList, osList;

    private SERVERS selectedServer = null;
    private OS selectedOS = null;


    public GUIFrame() {
        initialize();
    }

    private void initialize() {
        // Do things common to all here

        // Operating system select event
        ListSelectionListener refreshOptionsListener = new ListSelectionListener() {
            public void valueChanged(ListSelectionEvent listSelectionEvent) {
                selectedOS = (OS)osList.getSelectedValue();
                selectedServer = (SERVERS)serverList.getSelectedValue();

                refreshAdapters();
            }
        };

        // Instantiate osList
        osList = new JList();
        // ...
        osList.addListSelectionListener(refreshOptionsListener);

        // Instantiate serverList
        serverList = new JList();
        // ...
        serverList.addListSelectionListener(refreshOptionsListener);
    }

    private void refreshAdapters() {
        for(GUIAdapter adapter : adapters) {
            adapter.remove(this);
        }
        adapters.clear();

        if(os == OS.WINDOWS) {
            adapters.add(new WindowsGUIAdapter());
        } else if (os == OS.LINUX) {
            adapters.add(new LinuxGUIAdapter());
        } // else if () ...

        if(server == SERVERS.TOMCAT) {
            adapters.add(new TomcatGUIAdapter());
        } // else if () ...

        for(GUIAdapter adapter : adapters) {
            adapter.adapt(this);
        }    
    }

    // Other class logic here
}

Now you've decoupled all logic pertaining to operating system or engine with the classes which perform this logic. Whenever a new operating system or a new server is selected, current adapters are removed and new adapters are added. If no operating system or server is selected, then no adapter is added (no condition is met in if/else chain since selectedOS or selectedServer would be null), which is fine. The frame is not adapted since no adaptation is required as you would expect.

This pattern works well only if server adapters do not really need to know what operating system is being used or inversely, if operating system adapters do not really need to know what server is being used. However, if you find yourself in that situation, you should create a WindowsTomcatGUIAdapter which deals with just that particular case rather than attempt to make WindowsGUIAdapter and TomcatGUIAdapter work well together (as that would create coupling once again). Future additions are literally as simple as adding a new class and plugging them in on initialization.

Presumably, you could extend this logic to include validation or whatever other type of logic particular to that particular adapter.

share|improve this answer
    
I don't know whether it is normal, but I had goosebumps while reading your explanation - it is great. Going to try it right now. If you don't mind, I'll ask here some questions if any. –  Dragon Apr 23 '13 at 11:03
    
Actually, I think this solution is fine when you need completely different code for your GUI initialization for each different case, but as long as the task is just to enable/disable a list of given controls, this seems IMHO totally over-engineered to me. –  Doc Brown Apr 23 '13 at 11:19
    
@DocBrown I think all patterns are a bit "over-engineering" when a simple solution will suffice. The point was to find a possible pattern for this. –  Neil Apr 23 '13 at 12:36
    
@Neil, I think I described the situation in a wrong way. I have a list box and after choosing a server from a list, I determine OS and then show/hide UI elements. I'm trying to adapt this pattern to my schema, but at the moment I didn't manage to do it. I mean that main window is already initialized, so code like: GUIFrame frame = new GUIFrame(); if(os == OS.WINDOWS) { won't do. –  Dragon Apr 23 '13 at 15:00
    
@Dragon, then add the appropriate adapter when the choice is made which modifies the state as you deem fit. Alternatively, rather than use an adapter, you could follow a similar pattern, except that GUIAdapter extends JPanel so that based on the choice for the operating system/server engine, you could append the appropriate fields afterwards by simply adding the adapter to your JFrame. –  Neil Apr 23 '13 at 15:16
show 6 more comments

I guess what you are looking for is some kind of decision table. For each possible combination of OS, server and GUI element store a boolean flag indicating the visibility. You could simply utilize a 3 dimensional boolean array (bool[][][] uiVisibility) for that task, indexed by enums indicating the OS, the server and the particular UI element. And you GUI initialization code then will just look like this:

    userNameField.setEnabled(uiVisibility[osIndex][serverIndex][UIelem.userNameField.getValue()]);
    userPasswordField.setEnabled(uiVisibility[osIndex][serverIndex][UIelem.userPasswordField.getValue()]);

As you see - no switch/case any more, and the visibility setting code is in just one place.

Note that it is much more likely that your list of GUI elements will grow than the list of OSs or servers - at least as long as you don't have to distinguish between different OS versions or server versions.

EDIT: if this is the right tool for the job depends. First, a decision table is definitely much better maintainable then to duplicate the same setEnabled code 12 times, with minor modifications between that blocks (note that Neil's suggestion does not solve that problem, it just hides it because he does not show the // Things to do part in his answer)

The key is how you initialize the table. You have different options for that, for example, using standard array initialization. If you have only minor differences between the 12 combinations, there is also the option to create a default initialization for all blocks in the decision table (or a default initilization for each OS) and add some small code writing only that modifications for specific combinations of server/OS.

share|improve this answer
    
Isn't it too complicated? I mean, let's imagine we have 4 UI elements common for all combinations of OS/Servers. And the difference is the following: Windows/IIS has 3 additional elements, Windows/Tomcat has also 3 additional elements, but Linux/Tomcat has 5 elements. Is UI grows further, it'll be too difficult to understand such table, I think. Or I didn't catch your idea? –  Dragon Apr 23 '13 at 11:02
    
@Dragon: see my edit. But this suggestion is not complicated, it is indeed a very simple approach - maybe simpler than you expected. –  Doc Brown Apr 23 '13 at 11:11
    
Hm, it is interesting too and I like it in some way. I'll try this approach too in order to compare the result with Neil's approach. Because everything is relative :) –  Dragon Apr 23 '13 at 11:27
add comment

If your configuration is composed of boolean values, I think what you could have is the following structure comprising a BitSet:

class OSConfiguration {
  private BitSet configurationFields;

  public OSConfiguration() {
    configurationFields = new BitSet();
  }

  // you can allow checking if a given bitset index is set or not
  public boolean isFieldEnabled(int index) { return configurationFields.get(index); }

  // ... or create proper getter/setters
  public boolean isUserNameFieldEnabled() { return configurationFields.get(3); }
  public void setUserNameFieldEnabled(boolean value) { configurationFields.set(3, value); }
  // all other fields could be done like that
}

And the following TreeMap:

TreeMap map = new TreeMap<SERVERS, Map<OS, OSConfiguration>>();

// to check if an OS configuration is available for a given server:
boolean isAvailable = map.get(SERVERS.TOMCAT).containsKey(OS.WINDOWS);

// to get an entire configuration:
OSConfiguration configuration = map.get(SERVERS.TOMCAT).get(OS.WINDOWS);
// to check if a given configuration is enabled:
boolean isEnabled = map.get(SERVERS.TOMCAT).get(OS.WINDOWS).isUserNameFieldEnabled();

Now, even if your OS configuration needs to have more than just boolean values, since you've hidden that in the OSConfiguration class, you can do whatever you want.

In any case, I suggest to keep it simple.

share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.