Blog Acensi
Cubes blancs en 3d dont un bleu

Comment réutiliser des représentations de données identiques dans une IHM

L’objectif de cet article est de montrer comment mutualiser la définition des propriétés d’une valeur pour divers composants graphiques (table, arbre, formulaire, …) dans les différentes vues d’une application.
Pour cela nous allons montrer comment représenter des données fonctionnelles dans une table de façon classique puis en inversant la configuration pour l’accès aux données et enfin en mutualisant ces configurations.
 
 

1. Modèle de données fonctionnel

Le modèle de données fonctionnel que nous allons utiliser tout au long de cet article est composé d’un instrument financier caractérisé par son sous-jacent et son prix :

public class Instrument {
	private final int id;
	private final String name;
	public Instrument(final int id, final String name) {
		this.id = id;
		this.name = name;
	}
	public int getId() {
		return this.id;
	}
	public String getName() {
		return this.name;
	}
}
public class Price {
	private final double bid;
	private final double ask;
	public Price(final double bid, final double ask) {
		this.bid = bid;
		this.ask = ask;
	}
	public double getBid() {
		return this.bid;
	}
	public double getAsk() {
		return this.ask;
	}
}
public class Option {
	private final Instrument underlying;
	private final Price price;
	private String comment = "";
	public Option(final Instrument underlying, final Price price) {
		this.underlying = underlying;
		this.price = price;
	}
	public Instrument getUnderlying() {
		return this.underlying;
	}
	public Price getPrice() {
		return this.price;
	}
	public String getComment() {
		return this.comment;
	}
	public void setComment(final String comment) {
		this.comment = comment;
	}
}

2. Implémentation classique

Dans un premier temps, nous allons partir d’une implémentation souvent présentée dans les exemples et tutoriaux basiques.
Cette implémentation montre l’accès aux données par rapport au modèle de données de la vue (getColumnClass, getColumnName, getValueAt, …) puis l’accès au modèle de données fonctionnel (Vue de type table -> Modèle de donnée de la table -> Modèle de donnée fonctionnel).

public class OptionTableModel extends AbstractTableModel {
	private static final long serialVersionUID = 83868589246675834L;
	private final List<Option> dataList = new ArrayList<Option>();
	public void addData(final Option data) {
		this.dataList.add(data);
	}
	@Override
	public int getRowCount() {
		return this.dataList.size();
	}
	@Override
	public Class<?> getColumnClass(final int columnIndex) {
		switch (columnIndex) {
		case 0:
			return Integer.class;
		case 1:
			return String.class;
		case 2:
			return Double.class;
		case 3:
			return Double.class;
		case 4:
			return String.class;
		}
		return null;
	}
	@Override
	public String getColumnName(final int columnIndex) {
		switch (columnIndex) {
		case 0:
			return "Underlying Id";
		case 1:
			return "Underlying name";
		case 2:
			return "Bid";
		case 3:
			return "Ask";
		case 4:
			return "Comment";
		}
		return null;
	}
	@Override
	public int getColumnCount() {
		return 5;
	}
	@Override
	public Object getValueAt(final int rowIndex, final int columnIndex) {
		switch (columnIndex) {
		case 0:
			return this.dataList.get(rowIndex).getUnderlying().getId();
		case 1:
			return this.dataList.get(rowIndex).getUnderlying().getName();
		case 2:
			return Double.valueOf(this.dataList.get(rowIndex).getPrice().getBid());
		case 3:
			return Double.valueOf(this.dataList.get(rowIndex).getPrice().getAsk());
		case 4:
			return this.dataList.get(rowIndex).getComment();
		}
		return null;
	}
	@Override
	public boolean isCellEditable(final int rowIndex, final int columnIndex) {
		switch (columnIndex) {
		case 4:
			return true;
		}
		return false;
	}
	@Override
	public void setValueAt(final Object value, final int rowIndex,
            final int columnIndex) {
            switch (columnIndex) {
            case 4:
                if (value instanceof String) {
                    this.dataList.get(rowIndex).setComment((String) value);
                }
                break;
        }
    }
}

L’avantage de cette implémentation est qu’elle est orientée modèle de données de la vue en implémentant directement les méthodes de l’interface ‘TableModel’ (facile à suivre pour le debug).
Les inconvénients sont nombreux dès que le modèle de données fonctionnel devient plus important, lorsqu’il faut modifier l’ordre par défaut des colonnes, ajouter ou supprimer une colonne. Et ce coût est surtout visible lors de la maintenance du code.
Une évolution simple est d’inverser la configuration pour l’accès aux données.

3. Orientation modèle de données fonctionnel

Dans un deuxième temps, nous allons définir les caractéristiques de chaque donnée, ce qui permettra de configurer le modèle de données de la vue.
Nous définissons deux interfaces, une pour les caractéristiques de base et une autre pour celles liées à l’édition d’une donnée :

public interface IDataDescriptor<C, T> {
	String getDataName();
	Class<?> getDataClass();
	C getValue(final T data);
}
public interface IEditableDataDescriptor<C, T> {
	boolean isEditable(final T data);
	void setValue(final T data, final C value);
}

Voici une implémentation de base pour une donnée non éditable :

public abstract class DefaultDataDescriptor<T>
    implements IDataDescriptor<Object, T>, IEditableDataDescriptor<Object, T> {
	private final String dataName;
	private final Class<?> dataClass;
	public DefaultDataDescriptor(final String dataName, final Class<?> dataClass) {
		this.dataName = dataName;
		this.dataClass = dataClass;
	}
	@Override
	public String getDataName() {
		return this.dataName;
	}
	@Override
	public Class<?> getDataClass() {
		return this.dataClass;
	}
	@Override
	public boolean isEditable(final T data) {
		return false; // default is not editable
	}
	@Override
	public void setValue(final T data, final Object value) {
		// default is not editable
	}
}

Pour chaque donnée du modèle de donnée fonctionnel, nous allons définir ses caractéristiques :

private Collection<DefaultDataDescriptor<Option>> buildDataDescriptors() {
	final Collection<DefaultDataDescriptor<Option>> dataDescriptors =
		new ArrayList<DefaultDataDescriptor<Option>>();
	dataDescriptors.add(
		new DefaultDataDescriptor<Option>("Underlying Id", Integer.class) {
			@Override
			public Integer getValue(final Option data) {
				return data.getUnderlying().getId();
			}
		});
	dataDescriptors.add(
		new DefaultDataDescriptor<Option>("Underlying name", String.class) {
			@Override
			public String getValue(final Option data) {
				return data.getUnderlying().getName();
			}
		});
	dataDescriptors.add(
		new DefaultDataDescriptor<Option>("Bid", Double.class) {
			@Override
			public Double getValue(final Option data) {
				return Double.valueOf(data.getPrice().getBid());
			}
		});
	dataDescriptors.add(
		new DefaultDataDescriptor<Option>("Ask", Double.class) {
			@Override
			public Double getValue(final Option data) {
				return Double.valueOf(data.getPrice().getAsk());
			}
		});
	dataDescriptors.add(
		new DefaultDataDescriptor<Option>("Comment", String.class) {
			@Override
			public String getValue(final Option data) {
				return data.getComment();
			}
			@Override
			public boolean isEditable(final Option data) {
				return true;
			}
			@Override
			public void setValue(final Option data, final Object value) {
				if (value instanceof String) {
					data.setComment((String) value);
				}
			}
		});
	return dataDescriptors;
}

Et maintenant l’implémentation de l’interface ‘TableModel’ va devenir plus générique et ne dépendra pas du modèle de données fonctionnel. Pour cela une liste permet de définir les titres des colonnes et une autre les lignes de données :

public class DataTableModel<T> extends AbstractTableModel {
	private static final long serialVersionUID = 83868589246675834L;
	private final List<T> dataList = new ArrayList<T>();
	private final List<DefaultDataDescriptor<T>> dataDescriptorList =
        new ArrayList<DefaultDataDescriptor<T>>();
	public void addData(final T data) {
		this.dataList.add(data);
	}
	public void addDataDescriptor(final DefaultDataDescriptor<T> dataDescriptor) {
		this.dataDescriptorList.add(dataDescriptor);
	}
	@Override
	public int getRowCount() {
		return this.dataList.size();
	}
	public void addColumn(final DefaultDataDescriptor<T> dataDescriptor) {
		this.dataDescriptorList.add(dataDescriptor);
	}
	@Override
	public Class<?> getColumnClass(final int columnIndex) {
        return this.dataDescriptorList.get(columnIndex)
                                      .getDataClass();
	}
	@Override
	public String getColumnName(final int columnIndex) {
        return this.dataDescriptorList.get(columnIndex)
                                      .getDataName();
	}
	@Override
	public int getColumnCount() {
		return this.dataDescriptorList.size();
	}
	@Override
	public Object getValueAt(final int rowIndex, final int columnIndex) {
        return this.dataDescriptorList.get(columnIndex)
                                      .getValue(this.dataList.get(rowIndex));
	}
	@Override
	public boolean isCellEditable(final int rowIndex, final int columnIndex) {
        return this.dataDescriptorList.get(columnIndex)
                                      .isEditable(this.dataList.get(rowIndex));
	}
	@Override
	public void setValueAt(final Object value, final int rowIndex,
        final int columnIndex) {
        this.dataDescriptorList.get(columnIndex)
                               .setValue(this.dataList.get(rowIndex), value);
	}
}

La construction de la vue se fait juste en ajoutant la description des colonnes :

private JTable buildTable(final DataTableModel<Option> tableModel) {
	final Collection<DefaultDataDescriptor<Option>> dataDescriptors =
        buildDataDescriptors();
	for (final DefaultDataDescriptor<Option> dataDescriptor : dataDescriptors) {
		tableModel.addColumn(dataDescriptor);
	}
	final JTable table = new JTable(tableModel);
	return table;
}

Les avantages de cette implémentation sont qu’elle permet de définir de manière concise les caractéristiques des données et de ne pas lier l’implémentation de la vue et du modèle de données de la vue aux données fonctionnelles. Ces caractéristiques peuvent être utilisées quel que soit le composant (JTable, JideTable, GroupTable, tree table, …, et même les formulaires). Le code est plus maintenable, une donnée peut être modifiée, ajoutée ou supprimée en mettant à jour uniquement la liste des caractéristiques des données fonctionnelles.
Un des inconvénients est la nécessité de dupliquer la définition des caractéristiques des données si d’autres données fonctionnelles ont aussi un sous-jacent ou un prix.
Cette inversion de la définition des caractéristiques des données est une bonne pratique, et nous pouvons aller encore plus loin en mutualisant ces définitions.

4. Mutualisation des caractéristiques des données

Dans un troisième temps, nous allons modifier le code précédent, pour donner accès à des agrégats de données ainsi que définir leurs caractéristiques.
L’utilisation d’interfaces permet l’accès à des agrégats de données, et dans notre exemple à un prix et à un sous-jacent :

public interface IUnderlyingHolder {
	Instrument getUnderlying();
}
public interface IPriceHolder {
	Price getPrice();
}

Et en modifiant la classe Option :

public class Option implements IUnderlyingHolder, IPriceHolder {
//...

La description des caractéristiques de ces agrégats de données est aussi externalisée dans des méthodes utilitaires (l’utilisation de constructeur est aussi possible) :

public class DataDescriptorHelper {
	public <T extends IUnderlyingHolder> void appendUnderlyingDataDescriptors(
		final Collection<DefaultDataDescriptor<T>> dataDescriptors) {
			dataDescriptors.add(
				new DefaultDataDescriptor<T>("Underlying Id", Integer.class) {
					@Override
					public Integer getValue(final T data) {
						return data.getUnderlying().getId();
					}
				});
			dataDescriptors.add(
				new DefaultDataDescriptor<T>("Underlying name", String.class) {
					@Override
					public String getValue(final T data) {
						return data.getUnderlying().getName();
					}
				});
		}
	public <T extends IPriceHolder> void appendPriceDataDescriptors(
		final Collection<DefaultDataDescriptor<T>> dataDescriptors) {
			dataDescriptors.add(
				new DefaultDataDescriptor<T>("Bid", Double.class) {
					@Override
					public Double getValue(final T data) {
						return Double.valueOf(data.getPrice().getBid());
					}
				});
			dataDescriptors.add(
				new DefaultDataDescriptor<T>("Underlying name", Double.class) {
					@Override
					public Double getValue(final T data) {
						return Double.valueOf(data.getPrice().getAsk());
					}
				});
		}
}

Et la construction de la liste des caractéristiques des données utilise ces méthodes utilitaires :

private Collection<DefaultDataDescriptor<Option>> buildDataDescriptors() {
	final Collection<DefaultDataDescriptor<Option>> dataDescriptors =
                new ArrayList<DefaultDataDescriptor<Option>>();
	new DataDescriptorHelper().appendUnderlyingDataDescriptors(dataDescriptors);
	new DataDescriptorHelper().appendPriceDataDescriptors(dataDescriptors);
	dataDescriptors.add(new DefaultDataDescriptor<Option>("Comment", String.class) {
		@Override
		public String getValue(final Option data) {
			return data.getComment();
		}
		@Override
		public boolean isEditable(final Option data) {
			return true;
		}
		@Override
		public void setValue(final Option data, final Object value) {
			if (value instanceof String) {
				data.setComment((String) value);
			}
		}
	});
	return dataDescriptors;
}

Les avantages de cette implémentation sont une mutualisation et un regroupement des déclarations des caractéristiques des données fonctionnelles afin d’éviter la duplication de code et améliorer la maintenabilité.
Un des inconvénients est la création d’une API propre au projet et qui doit être connue de tous les membres de l’équipe.
Voici une liste non exhaustive des évolutions possibles :

  • définition de caractéristiques de base complémentaires (getRenderer, getMinWidth, getPreferredWidth, getMaxWidth, …),
  • définition de caractéristiques d’édition complémentaires (getEditor),
  • définition de caractéristiques pour le style (IStyleDataDescriptor : getBackground, getForeground, getFont, …),
  • définition de caractéristiques pour l’export des données (getExportName, getExportValue, …),

Le code source associé à cet article se trouve dans trois fichiers, un pour chaque partie.
SourceCode

Pourquoi ce blog ?

Pour permettre à nos consultants et experts techniques de partager leurs connaissances et retours d’expérience autour des sujets qui les passionnent. Ce blog, intégralement écrit par eux, a pour vocation d’être un véritable lieu d’échanges et d’apprentissage.

Alors n’hésitez pas à commenter nos articles pour rejoindre la conversation !

Une suggestion ?

Si vous avez des idées pour améliorer ce blog, nous sommes à l’écoute de vos remarques. Vous pouvez nous écrire via le formulaire de contact qui se trouve en bas de page.

Bonne visite !