Preface: I am trying to solve my problem without the need for boilerplate code. I could easily solve this problem by copy/pasting and refactoring code, but this kind of problem should be solvable with a model-driven approach because of the obvious recurrence of the design.
I have a database application with four different object types that will be used as parameters for another, central object.
All four fields are structurally the same: one auto-generated primary key, and one VARCHAR
field. These fields need to be editable by the end-user, so I designed this screen:
Rather than making four different screens for this, I thought it would be good to make this one screen that is used by the four different object types. To accomplish this, I (obviously, right?) need to make an abstract Object (what I called) StringField
which can be implemented by four subtypes: Status
, Type
, Classification
, and Designation
. As mentioned before, because every object is structurally identical and will read/write to the database the same exact way, an abstract object seemed logical. So, I made this object:
public abstract class StringField {
private final int id;
private final String value;
private final String fieldname;
private final String tablename;
public StringField(int id, String value, String fieldname, String tablename) {
this.id = id;
this.value = value;
this.fieldname = fieldname;
this.tablename = tablename;
}
public String getValue() {
return value;
}
public int getId() {
return id;
}
@Override
public int hashCode() {
int hash = 5;
hash = 59 * hash + this.id;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return this.id == ((StringField)obj).id;
}
@Override
public String toString() {
return value;
}
public int insertValue(DB db, String value) throws WrappedSQLException {
... // DB code here
}
public void updateValue(DB db, String value, int id) throws WrappedSQLException {
... // DB code here
}
public void deleteValue(DB db, int id) throws WrappedSQLException {
... // DB code here
}
}
With subclasses implementing as follows (for example):
public class Type extends StringField {
public Type(int id, String value) {
super(id, value, "type", "customer_type");
}
}
public class Classification extends StringField {
public Classification (int id, String value) {
super(id, value, "classification", "customer_classification");
}
}
An aside: the reason for
String fieldname
andString tablename
in the constructor is that Java does not allow for static abstract fields. So, they'll need to be supplied manually by the classes which use the super type. This, unfortunately, prevents theinsert
,update
, anddelete
methods from being declaredstatic
as well. If Java allowedstatic abstract
, I doubt I'd be asking this question at all because I would just make theinsert
,update
, anddelete
methods static withfieldname
andtablename
as abstract static fields, which would be forced to be implemented by the subtypes in static fashion and could then be referenced statically by the superclass, which could then be called statically through each subtype. Basically, it could look like this:Supertype:
public abstract class StringField { private final int id; private final String value; public StringField(int id, String value) { this.id = id; this.value = value; } public static abstract String fieldname; public static abstract String tablename; public static int insertValue(String value) { // Insert code, returns auto-generated primary key // ... // This code will use the static fields `fieldname` and `tablename`. // ... return -1; } public static void updateValue(String value, int id) { // Update code... // ... // This code will use the static fields `fieldname` and `tablename`. // ... } public static void deleteValue(int id) { // Delete code... // ... // This code will use the static fields `fieldname` and `tablename`. // ... } }
Subtype:
static class Type extends StringField { public Type(int id, String value) { super(id, value); } public static String fieldname = "type"; public static String tablename = "customer_type"; }
And used like:
Type.insertValue(newValue);
Ok, enough complaining about how Java doesn't allow static abstract
. Here is the full, (almost) compile-able code I have so far that illustrates my approach:
@SuppressWarnings("serial")
public class StringFieldEditor extends JPanel {
final Collection<StringField> fields;
String fieldname;
public StringFieldEditor(Collection<StringField> fields, String fieldname) {
super(new GridBagLayout());
this.fields = fields;
this.fieldname = fieldname;
GridBagConstraints gridBagConstraints;
JButton addNew_button = new JButton();
JButton delete_button = new JButton();
JButton edit_button = new JButton();
JLabel jLabel1 = new JLabel();
JList<StringField> jList = new JList<>();
jList.setModel(new DefaultListModel<>());
jList.setListData(fields.toArray(new StringField[fields.size()]));
JScrollPane jScrollPane1 = new JScrollPane();
jScrollPane1.setViewportView(jList);
jLabel1.setText("Customer " + fieldname + " Editor");
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 0;
gridBagConstraints.gridwidth = 2;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(11, 10, 0, 0);
add(jLabel1, gridBagConstraints);
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 1;
gridBagConstraints.gridwidth = 5;
gridBagConstraints.fill = GridBagConstraints.BOTH;
gridBagConstraints.ipadx = 178;
gridBagConstraints.ipady = 206;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.weightx = 1.0;
gridBagConstraints.weighty = 1.0;
gridBagConstraints.insets = new Insets(6, 10, 0, 10);
add(jScrollPane1, gridBagConstraints);
addNew_button.setText("Add New");
addNew_button.addActionListener((ActionEvent evt) -> {
// Add New Task
});
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 2;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(6, 10, 11, 0);
add(addNew_button, gridBagConstraints);
edit_button.setText("Edit");
edit_button.addActionListener((ActionEvent evt) -> {
// Edit Task
});
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 2;
gridBagConstraints.gridwidth = 3;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(6, 6, 11, 0);
add(edit_button, gridBagConstraints);
delete_button.setText("Delete");
delete_button.addActionListener((ActionEvent evt) -> {
// Delete Task
});
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 4;
gridBagConstraints.gridy = 2;
gridBagConstraints.anchor = GridBagConstraints.NORTHWEST;
gridBagConstraints.insets = new Insets(6, 6, 11, 10);
add(delete_button, gridBagConstraints);
}
static class Type extends StringField {
public Type(int id, String value) {
super(id, value, "type", "customer_type");
}
}
public abstract class StringField {
private final int id;
private final String value;
private final String fieldname;
private final String tablename;
public StringField(int id, String value, String fieldname, String tablename) {
this.id = id;
this.value = value;
this.fieldname = fieldname;
this.tablename = tablename;
}
public String getValue() {
return value;
}
public int getId() {
return id;
}
@Override
public int hashCode() {
int hash = 5;
hash = 59 * hash + this.id;
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return this.id == ((StringField)obj).id;
}
@Override
public String toString() {
return value;
}
public int insertValue(String value) {
// Insert code, returns auto-generated primary key
// ...
// ...
return -1;
}
public void updateValue(String value, int id) {
// Update code...
// ...
// ...
}
public void deleteValue(int id) {
// Delete code...
// ...
// ...
}
}
public static void main(String[] args) {
LinkedList<Type> types = new LinkedList<>();
types.add(new Type(1, "Type 1"));
types.add(new Type(2, "Type 2"));
types.add(new Type(3, "Type 3"));
JFrame frame = new JFrame();
frame.add(new StringFieldEditor(types, "type"));
frame.pack();
frame.setLocationByPlatform(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
This, however, throws an error:
incompatible types: LinkedList<Type> cannot be converted to Collection<StringFieldEditor.StringField>
But changing:
final Collection<StringField> fields;
String fieldname;
public StringFieldEditor(Collection<StringField> fields, String fieldname) {
to
final Collection<? extends StringField> fields;
String fieldname;
public StringFieldEditor(Collection<? extends StringField> fields, String fieldname) {
causes issues when trying to load the data into the JList
(toArray
requires a type T
value, but if I don't know the exact type I'm working with, I can't supply it a new T
type array.
Also, I forsee the problem of trying to reference the non-static insert, update, and delete methods in a non-subclass specific way.
My questions:
- Is this approach viable? Am I taking this whole model-driven development approach too far?
- Is there a better way to achieve this functionality without creating boilerplate code?
- Is there a good workaround to the fact that there are no
static abstract
methods/fields in Java for this? - Is there a way to supply the
Collections::toArray(T[])
method a usable array when you don't know which subtype you'll be using?