1
\$\begingroup\$

I am making a sentiment analysis library targeted first at social media text (like Twitter) and then I will expand it for more general use.

For the first step I am making a simple interface and a class that is going to be used for storing some data about the text to be analyzed like the text itself, it's manually annotated sentiment and the calculated sentiment that must be set only once (that's my current design anyway).

ObjectForAnalysis.java interface:

package com.github.dkanellis.sa_for_socialmedia.sentiment.objects;

public interface ObjectForAnalysis {

    double getManuallyAnnotatedMeanSentiment();

    double getCalculatedMeanSentiment();

    boolean isCalculated();

    void setCalculatedMeanSentiment(double calculatedMeanSentiment);
}

TwitterStatus.java implementation:

package com.github.dkanellis.sa_for_socialmedia.sentiment.objects;

public class TwitterStatus implements ObjectForAnalysis {

    private final long id;
    private final String text;
    private final double manuallyAnnotatedMeanSentiment;
    private double calculatedMeanSentiment;
    private boolean calculated;

    public TwitterStatus(long id, String text, double manuallyAnnotatedMeanSentiment) {
        this.id = id;
        this.text = text;
        this.manuallyAnnotatedMeanSentiment = manuallyAnnotatedMeanSentiment;
        this.calculated = false;
    }

    @Override
    public double getManuallyAnnotatedMeanSentiment() {
        return manuallyAnnotatedMeanSentiment;
    }

    @Override
    public double getCalculatedMeanSentiment() {
        if (isCalculated()) {
            return calculatedMeanSentiment;
        } else {
            throw new IllegalStateException("Mean sentiment has not been calculated yet.");
        }
    }

    @Override
    public boolean isCalculated() {
        return calculated;
    }

    @Override
    public void setCalculatedMeanSentiment(double calculatedMeanSentiment) {
        if (isCalculated()) {
            throw new IllegalStateException("Mean sentiment has already been calculated.");
        } else {
            this.calculatedMeanSentiment = calculatedMeanSentiment;
            calculated = true;
        }
    }

    @Override
    public String toString() {
        StringBuilder buffer = new StringBuilder();
        buffer.append("Tweet: ").append(id).append("\n");
        buffer.append("Text: ").append(text).append("\n");
        buffer.append("Manual Annotation: ").append(manuallyAnnotatedMeanSentiment).append("\n");
        buffer.append("Calculated Annotation: ")
                .append(isCalculated() ? "Not calculated" : calculatedMeanSentiment).append("\n");

        return buffer.toString();
    }

}

Feel free to critique anything from package naming to class naming to the way I implemented my constraint of having the mean sentiment set only once.

\$\endgroup\$

2 Answers 2

3
\$\begingroup\$

Your concrete TwitterStatus class would, in a single-threaded application, be OK for the purpose of having a single-set ("lazy set", or "set and freeze") mechanism. Two concurrent threads each setting the value, or one thread setting, and another thread getting the value may cause problems though.

The situation can be relatively easily resolved with an AtomicReference value though...

Consider the following value that replaces both the calculatedMeanSentiment and calculated fields:

private final AtomicReference<Double> calculatedMeanSentiment = new AtomicReference<>(null);

Then, in your methods, you can have:

@Override
public double getCalculatedMeanSentiment() {
    Double val = calculatedMeanSentiment.get();
    if (val == null) {
        throw new IllegalStateException("Mean sentiment has not been calculated yet.");
    }
    return val;
}

@Override
public void setCalculatedMeanSentiment(double calculatedMeanSentiment) {
    if (!this.calculatedMeanSentiment.compareAndSet(null, calculatedMeanSentiment)) {
        throw new IllegalStateException("Mean sentiment has already been calculated.");
    }
}

@Override
public boolean isCalculated() {
    return calculatedMeanSentiment.get() != null;
}

Notice also that when you throw, or return from the 'if' part of an if statement, there is no need to have the 'else' part. Specifically, you have structures like this:

    if (isCalculated()) {
        return calculatedMeanSentiment;
    } else {
        throw new IllegalStateException("Mean sentiment has not been calculated yet.");
    }

that return in the if. They can be easily rewritten as 'guard conditions':

    if (!isCalculated()) {
        throw new IllegalStateException("Mean sentiment has not been calculated yet.");
    }
    return calculatedMeanSentiment;
\$\endgroup\$
3
  • \$\begingroup\$ Regarding the AtomicReference I like this solution, I will study the API first and then apply it. As for the if, I have been following as closely as I can the guidelines from the book Clean Code - A Handbook of Agile Software Craftsmanship and there it said that it is better to have positive conditionals in place of negative ones (hence the way I coded). Is that maybe not the case with guard conditions? \$\endgroup\$
    – Aki K
    Commented Nov 1, 2014 at 17:54
  • \$\begingroup\$ when you have an if and an else block, and neither of them return, then I would agree that yes, the conditions hould be a positive one, and the positive result should be in the if-side of the statement. If there's only one conditional block, then having if(positive){} else {do something} is senseless. Having nested blocks just to have a positive condition is too broad a 'rule' to make sense. Does the book really say that? See List.equals() for example. \$\endgroup\$
    – rolfl
    Commented Nov 1, 2014 at 18:20
  • \$\begingroup\$ The book says: Negatives are just a bit harder to understand than positives. So, when possible, conditionals should be expressed as positives. For example: if (buffer.shouldCompact()) is preferable to if (!buffer.shouldNotCompact()). But I am pretty sure the book also says to prefer guard conditions over multiple if blocks so it must have been a misinterpretation on my part. \$\endgroup\$
    – Aki K
    Commented Nov 1, 2014 at 18:55
2
\$\begingroup\$

The code is good, but there is something that I dislike, this bit

 @Override
public double getCalculatedMeanSentiment() {
    if (isCalculated()) {
        return calculatedMeanSentiment;
    } else {
        throw new IllegalStateException("Mean sentiment has not been calculated yet.");
    }
}

I don't like the idea of using exceptions to handle program's state, it assumes that the user of the class is always aware of them. I would simply use a monadic type like Optional

 @Override
public Optional<Double> getCalculatedMeanSentiment() {
    if (isCalculated()) {
        return Optional.of(calculatedMeanSentiment);
    } else {
       return Optional.absent();
    }
}

This gives a clearer hint to class users that the value they asking for might not be there.

If using Java 8 then Optional is there, otherwise Guava is your friend

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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