2

Intro

I am working on an open source project Treez where I organize so called "Atoms" in a tree view. Those Atoms sometimes have a lot of attributes and those attributes are modified either through user actions in a tree view or through an API in an Eclipse code editor.

The attributes of my Atoms themselves are represented by reusable "AttributeAtoms". Those hold the actual attribute value and provide additional functionality like validation (other possible terms for "Atom" might be "widget", "bean", "property" or "tree node").

Question(s)

In the past I provided a getter/setter pair for each of my Atom attributes. This is a lot of extra work and it blows up the size of my Atom classes (see code examples below). Now I am looking for an alternative solution that

  • makes it less work to create new Atoms (and less work to maintain them).
  • avoids the "redundant" getter/setter boiler plate code.

I'm going to describe a few options below. Which of the options would you use? Have you suggestions on how to improve those options? Do you know even further options?

Getter/Setter code example

    private AttributeAtom<String> myAttribute = new FilePathAttributeAtom("myAttribtue");

    public String getMyAttribute() {
        return myAttribute.getValue();
    }    

    public void setMyAttribute(String value) {
        this.myAtrribute.setValue(value);
    }

Related articles

Considered options

A. Auto generated getters/setters with IDE

Eclipse provides the possibility to auto generate getters/setters.

  • Does not work for my AttributeAtoms since the gettter/setter code looks slightly different.
  • Does not avoid the extra "redundant" code.

If I decide to keep the getters/setters, I could try to create something similar for my AttributeAtoms. Also see this post about (not working) automatic getter/setter creation for JavaFx Properties: http://www.eclipse.org/forums/index.php/t/781816/

B. Annotations for generating getters/setters (Project Lombok)

Lombok provides the possibility to use Annotations for the automatic generation of getters and setters.

  • Does neither work for my AttributeAtoms
  • I tried to use Lombok with Eclipse. The code completion in the editor worked but I got "method not found" warnings. I might need to invest some more time to get Lombok working for classical attributes.
  • Also see Is it safe to use Project Lombok?

If I decide to use Annotations to define getters/setters, it might be possible to extend Lombok to work for my AttributeAtoms.

C. One generalized getter/setter for all attributes

I could use a single getter/setter pair for all Atom attributes like

Object get(String attributeName)
void set(String attriuteName, Object value)
  • Type safety could be improved by passing additional type arguments.
  • However, the code completion for my Atom would only suggest the single getter/setter and a user would not see which attributes are available. (Maybe this could be addressed by using Enums instead of Strings to identify the attributes. But those Enums need to be created somehow. Also see next option.)

D. Custom Eclipse editor and code processing

Maybe I could write an extra Eclipse plugin for my open source project that "allows to access private attributes" by suggesting corresponding fake methods for the code completion. Before compiling the user source code, fake calls like

myAtom.setMyAttribue(newValue);

would be translated to code for the really existing generalized getter (option C):

myAtom.set("myAttribute", newValue);

E. Public attributes

If I make my Atom attributes public, I do not need getters/setter code in each Atom. Instead, the reusable AttributeAtoms would provide get/set methods. The usage would for example look like this

myAtom.myAttribute.get();
myAtom.myAttribute.set(newValue);

instead of

myAtom.getMyAttribute();
myAtom.setMyAttribute(newValue);

Some disadvantages:

  • Users need to get used to this "unconventional approach". Java users might expect setMyAttribute(newValue) and C# users might expect myAtom.myAttribute = newValue.
  • It is possible to exchange the whole AttributeAtom, which I do not want to allow:

    myAtom.myAttribute = completelyDifferentAttribute
    

Any strategies to improve this?

  • Is there a way to allow the access to the methods of an attribute while not allowing to exchange the attribute itself? I would need a new access modifier like

    private *publicMethodAccess* AttributeAtom<String> myAttribute;
    

Atom code example

Here is an example Atom class. If you scroll to the bottom you will find many lines of code that are consumed by the getters/setters. This is ugly, isn't it?

package org.treez.results.atom.probe;

import java.util.ArrayList;
import java.util.List;

import org.apache.log4j.Logger;
import org.eclipse.swt.graphics.Image;
import org.treez.core.atom.attribute.AttributeRoot;
import org.treez.core.atom.attribute.ModelPath;
import org.treez.core.atom.attribute.ModelPathSelectionType;
import org.treez.core.atom.attribute.Section;
import org.treez.core.atom.attribute.base.AttributeAtom;
import org.treez.core.atom.variablerange.VariableRange;
import org.treez.core.data.column.ColumnType;
import org.treez.data.column.Columns;
import org.treez.data.output.OutputAtom;
import org.treez.data.table.Table;
import org.treez.results.Activator;

/**
 * Collects data from a sweep and puts it in a single (probe-) table. That table can easier be used to produce plots
 * than the distributed sweep results.
 */
public class SweepProbe extends AbstractProbe {

    /**
     * Logger for this class
     */
    @SuppressWarnings("unused")
    private static Logger sysLog = Logger.getLogger(SweepProbe.class);

    //#region ATTRIBUTES

    private AttributeAtom<String> xLabel;

    private ModelPath xRange;

    private AttributeAtom<String> yLabel;

    private AttributeAtom<String> firstFamilyLabel;

    private ModelPath firstFamilyRange;

    private AttributeAtom<String> secondFamilyLabel;

    private ModelPath secondFamilyRange;

    private AttributeAtom<String> probeName;

    private ModelPath sweepOutputModel;

    private ModelPath firstProbeTable;

    private AttributeAtom<String> probeColumnIndex;

    private AttributeAtom<String> probeRowIndex;

    //#end region

    //#region CONSTRUCTORS

    /**
     * Constructor
     *
     * @param name
     */
    public SweepProbe(String name) {
        super(name);
        createPropertyModel();
    }

    //#end region

    //#region METHODS

    /**
     * Creates the model for the property control
     */
    private void createPropertyModel() {

        //root
        AttributeRoot root = new AttributeRoot("root");

        //page
        org.treez.core.atom.attribute.Page page = root.createPage("page");

        //x section
        Section xSection = page.createSection("xSection", "X");
        xSection.createSectionAction("action", "Run probe", () -> execute(treeViewRefreshable));

        xLabel = xSection.createTextField("xLabel", "Label for x-Axis", "x");

        xRange = xSection.createModelPath("xRange", "Range for x-Axis", "", VariableRange.class, this);
        xRange.setSelectionType(ModelPathSelectionType.FLAT);
        xRange.setValue("root.studies.sweep.threshold");

        //y section
        Section ySection = page.createSection("ySection", "Y");
        yLabel = ySection.createTextField("yLabel", "Label for y-Axis", "y");

        //first family section
        Section firstFamilySection = page.createSection("firstFamily", "First family");
        firstFamilySection.setExpanded(false);

        firstFamilyLabel = firstFamilySection.createTextField("firstFamilyLabel", "Label for first family", "family1");

        firstFamilyRange = firstFamilySection.createModelPath("firstFamilyRange", "Range for first family", "",
                VariableRange.class, this);

        //second family section
        Section secondFamilySection = page.createSection("secondFamily", "Second family");
        secondFamilySection.setExpanded(false);

        secondFamilyLabel = secondFamilySection.createTextField("secondFamilyLabel", "Label for second family",
                "family2");

        secondFamilyRange = secondFamilySection.createModelPath("secondFamilyRange", "Range for second family", "",
                VariableRange.class, this);

        //probe section
        Section probeSection = page.createSection("probe", "Probe");

        probeName = probeSection.createTextField("propeName", "Name", "MyProbe");

        sweepOutputModel = probeSection.createModelPath("sweepOutput", "SweepOutput", "", OutputAtom.class, this);

        firstProbeTable = probeSection.createModelPath("tablePath", sweepOutputModel, Table.class);
        firstProbeTable.setLabel("First probe table");

        probeColumnIndex = probeSection.createTextField("probeColumnIndex", "Column index", "0");

        probeRowIndex = probeSection.createTextField("probeColumnIndex", "Row index", "0");

        setModel(root);

    }

    /**
     * Provides an image to represent this atom
     */
    @Override
    public Image provideBaseImage() {
        Image baseImage = Activator.getImage("sweep.png");
        return baseImage;
    }

    //#region CREATE TABLE COLUMNS

    /**
     * Creates the required columns for the given table
     *
     * @param table
     */
    @Override
    protected void createTableColumns(Table table) {
        //TODO

    }

    //#end region

    //#region COLLECT PROBE DATA

    @Override
    protected void collectProbeDataAndFillTable() {
        // TODO Auto-generated method stub

    }

    //#end region

    //#end region

    //#region ACCESSORS

    //#region X LABEL

    /**
     * @return
     */
    public String getXLabel() {
        return xLabel.getValue();
    }

    /**
     * @param label
     */
    public void setXLabel(String label) {
        xLabel.setValue(label);
    }

    //#end region

    //#region X RANGE

    /**
     * @return
     */
    public String getXRange() {
        return xRange.getValue();
    }

    /**
     * @param range
     */
    public void setXRange(String range) {
        xRange.setValue(range);
    }

    //#end region

    //#region Y LABEL

    /**
     * @return
     */
    public String getYLabel() {
        return yLabel.getValue();
    }

    /**
     * @param label
     */
    public void setYLabel(String label) {
        yLabel.setValue(label);
    }

    //#end region

    //#region FIRST FAMILY LABEL

    /**
     * @return
     */
    public String getFirstFamilyLabel() {
        return firstFamilyLabel.getValue();
    }

    /**
     * @param label
     */
    public void setFirstFamilyLabel(String label) {
        firstFamilyLabel.setValue(label);
    }

    //#end region

    //#region FIRST FAMILY RANGE

    /**
     * @return
     */
    public String getFirstFamilyRange() {
        return firstFamilyRange.getValue();
    }

    /**
     * @param range
     */
    public void setFirstFamilyRange(String range) {
        firstFamilyRange.setValue(range);
    }

    //#end region

    //#region SECOND FAMILY LABEL

    /**
     * @return
     */
    public String getSecondFamilyLabel() {
        return secondFamilyLabel.getValue();
    }

    /**
     * @param label
     */
    public void setSecondFamilyLabel(String label) {
        secondFamilyLabel.setValue(label);
    }

    //#end region

    //#region SECOND  FAMILY RANGE

    /**
     * @return
     */
    public String getSecondFamilyRange() {
        return secondFamilyRange.getValue();
    }

    /**
     * @param range
     */
    public void setSecondFamilyRange(String range) {
        secondFamilyRange.setValue(range);
    }

    //#end region

    //#region PROBE

    /**
     * @return
     */
    public String getProbeName() {
        return probeName.getValue();
    }

    /**
     * @param name
     */
    public void setProbeName(String name) {
        probeName.setValue(name);
    }

    //#end region

    //#region SWEEP OUTPUT MODEL

    /**
     * @return
     */
    public String getSweepOutputModelName() {
        return sweepOutputModel.getValue();
    }

    /**
     * @param sweepOutputModel
     */
    public void setSweepOutputModelName(String sweepOutputModel) {
        this.sweepOutputModel.setValue(sweepOutputModel);
    }

    //#end region

    //#region PROBE TABLE

    /**
     * @return
     */
    public String getFirstProbeTable() {
        return firstProbeTable.getValue();
    }

    /**
     * @param firstProbeTable
     */
    public void setFirstProbeTable(String firstProbeTable) {
        this.firstProbeTable.setValue(firstProbeTable);
    }

    //#end region

    //#region COLUMN INDEX

    /**
     * @return
     */
    public String getProbeColumnIndex() {
        return probeColumnIndex.getValue();
    }

    /**
     * @param index
     */
    public void setProbeColumnIndex(String index) {
        probeColumnIndex.setValue(index);
    }

    //#end region

    //#region ROW INDEX

    /**
     * @return
     */
    public String getProbeRowIndex() {
        return probeRowIndex.getValue();
    }

    /**
     * @param index
     */
    public void setProbeRowIndex(String index) {
        probeRowIndex.setValue(index);
    }

    //#end region

    //#end region

}
Community
  • 1
  • 1
Stefan
  • 10,010
  • 7
  • 61
  • 117

3 Answers3

2

For Option E, by using the "final" modifier you could prevent a whole new AttributeAtom being swapped in, whilst still allowing getting/setting:

public final AttributeAtom<String> myAttribute = new FilePathAttributeAtom("myAttribtue");

Then the following will be allowed:

myAtom.myAttribute.get();
myAtom.myAttribute.set(newValue)

But the thing you're worried about won't be:

myAtom.myAttribute = completelyDifferentAttribute
Alyssa
  • 835
  • 1
  • 7
  • 23
  • Well, it would have the disadvantage that I neither can recreate the attribute from within the class but maybe I do not really need that. If there are no more suggestions I will accept it as answer in a few days. – Stefan Jul 12 '15 at 15:05
1

I finally decided to use a new pattern which extends option E with a "double wrapping":

  • There is an interface "Attribute" which provides the methods "T get()" and "set(T value)"
  • Another interface "AttributeWrapper" inherits from "Attribute". It provides the additional methods "setAttribute" and "getAttribute" to exchange a wrapped Attribute.
  • The methods get/set of the AttributeWrapper are redirected to the wrapped Attribute.
  • If an "AttributeWrapper" is passed as "Attribute" to the outside world, only the methods "get" and "set" are visible. This provides some (pseudo) encapsulation. (That encapsulation might be nearly as good as the encapsulation with a private modifier. The private modifier can be bypassed with reflection anyway.)
  • Those who know the fact that my Attributes are actually AttributeWrappers are able to cast the Attributes to AttributeWrappers and exchange the wrapped Attributes. This also allows me to create and exchange my public final Attributes not only in the constructor but also in any method of my Atom classes. (Putting all attribute definitions directly in the attribute region or in the constructor would make my code ugly and therefore hard to read.)
  • There is a class "Wrap" which provides a default implementation of the AttributeWrapper.
  • The abstract class AttributeAtom is the base class for all my attribute atoms. It implements Attribute and also provides a helper method "wrap". That method wraps the AttributeAtom in a parent AttributeWrapper that is passed to the method.
  • The final work flow is

    1. Declare a public final Attribute myAttribute and immediately assign a Wrap to it.
    2. Create the actual Attribute newAttribute in an initialization method.
    3. Assign the new Attribute as content of the corresponding Wrap by using the helper method "wrap".
  • All this is going to be more clear with some example code:

Final Usage

MyAtom myAtom = new MyAtom();
String defaultPath = myAtom.myFilePathAttribute.get();
myAtom.myFilePathAttribute.set("D:/newpath.txt")

Usage of Attribute and Wrap for the class definition of MyAtom

public class MyAtom {

    //#region ATTRIBUTES

    public final Attribute<String> myFilePathAttribute = new Wrap<>();

    //#end region

    //...

    //#region METHODS

    private init(){

        //create a new AttributeAtom 
        //(FilePath inherits from AttributeAtom<String>)
        FilePath filePath = new FilePath("C:/defaultpath.txt");

        //set the new AttributeAtom as content of the corresponding Wrap myFilePathAttribute
        filePath.wrap(myFilePathAttribute);

    }

    //#end region
}

Helper method "wrap" in AttributeAtom

/**
 * Wraps this attribute in the AttributeWrapper that is given as Attribute
 *
 * @param wrap
 */
public void wrap(Attribute<T> wrap) {
    Wrap<T> wrapper = (Wrap<T>) wrap;
    wrapper.setAttribute(this);
}

Interface Attribute

package org.treez.core.attribute;

/**
 * Represents an attribute
 *
 * @param <T>
 */
public interface Attribute<T> {

    /**
     * Returns the attribute value
     *
     * @return
     */
    T get();

    /**
     * Sets the attribute value
     *
     * @param value
     */
    void set(T value);

}

Interface AttributeWrapper

package org.treez.core.attribute;

/**
 * Wraps a replaceable attribute. The methods are "hidden" if this AttributeWrapper is passed as its parent interface
 * Attribute
 *
 * @param <T>
 */
public interface AttributeWrapper<T> extends Attribute<T> {

    /**
     * Sets the wrapped Attribute
     *
     * @param attribute
     */
    void setAttribute(Attribute<T> attribute);

    /**
     * Returns the wrapped Attribute
     * 
     * @return
     */
    Attribute<T> getAttribute();

}

Wrap: The implementation of AttributeWrapper

package org.treez.core.attribute;

import java.util.Objects;

/**
 * Default implementation of the AttributeWrapper interface
 */
public class Wrap<T> implements AttributeWrapper<T> {

    //#region ATTRIBUTES

    private Attribute<T> wrappedAttribute;

    //#end region

    //#region CONSTRUCTORS

    /**
     * Constructor
     */
    public Wrap() {}

    /**
     * Constructor with wrapped attribute
     *
     * @param wrappedAttribute
     */
    public Wrap(Attribute<T> wrappedAttribute) {
        this.wrappedAttribute = wrappedAttribute;
    }

    //#end region

    //#region ACCESSORS

    @Override
    public T get() {
        Objects.requireNonNull(wrappedAttribute, "Wrapped attribute must be set before calling this method.");
        T value = wrappedAttribute.get();
        return value;
    }

    @Override
    public void set(T value) {
        Objects.requireNonNull(wrappedAttribute, "Wrapped attribute must be set before calling this method.");
        wrappedAttribute.set(value);
    }

    @Override
    public Attribute<T> getAttribute() {
        return wrappedAttribute;
    }

    @Override
    public void setAttribute(Attribute<T> wrappedAttribute) {
        this.wrappedAttribute = wrappedAttribute;
    }

    //#end region

}
Stefan
  • 10,010
  • 7
  • 61
  • 117
0

Some years later I want to add that I entirely switched to JavaScript. That seems to be the better language for my use case.

Stefan
  • 10,010
  • 7
  • 61
  • 117