Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

Inspired by this question, I played a bit with the idea of modeling a Triangle, and a utility method to classify it:

class Triangle {

    private final int a;
    private final int b;
    private final int c;

    enum Type {
        EQUILATERAL, INVALID, ISOSCELES, SCALENE
    }

    public Triangle(int a, int b, int c) {
        if (!isSaneArguments(a, b, c)) {
            throw new IllegalArgumentException("All sides of a Triangle must be > 0");
        }
        if (!isTriangle(a, b, c)) {
            throw new IllegalArgumentException("Not a triangle: no side must be longer than the sum of the other sides");
        }
        this.a = a;
        this.b = b;
        this.c = c;
    }

    private static boolean isSaneArguments(int a, int b, int c) {
        return a > 0 && b > 0 && c > 0;
    }

    private static boolean isTriangle(int a, int b, int c) {
        return a < b + c && b < a + c && c < a + b;
    }

    public static Type classifyValidTriangle(int a, int b, int c) {
        final Type type;
        if (a == b && b == c) {
            type = Type.EQUILATERAL;
        } else if (b == c || a == b || c == a) {
            type = Type.ISOSCELES;
        } else {
            type = Type.SCALENE;
        }
        return type;
    }

    public static Type classify(int a, int b, int c) {
        if (!isSaneArguments(a, b, c) || !isTriangle(a, b, c)) {
            return Type.INVALID;
        }
        return classifyValidTriangle(a, b, c);
    }

    public Type classify() {
        return classifyValidTriangle(a, b, c);
    }
}

Unit tests:

public class TriangleTest {
    @Test(expected = IllegalArgumentException.class)
    public void testInvalidTriangleWithTooLongSide() {
        int a = 1;
        int b = a + 1;
        new Triangle(a, b, a + b + 1);
    }

    @Test(expected = IllegalArgumentException.class)
    public void testInvalidTriangleWithNegativeSide() {
        new Triangle(1, 2, 0);
    }

    @Test
    public void testScalene() {
        assertEquals(Triangle.Type.SCALENE, new Triangle(2, 3, 4).classify());
        assertEquals(Triangle.Type.SCALENE, Triangle.classify(2, 3, 4));
    }

    @Test
    public void testEquilateral() {
        assertEquals(Triangle.Type.EQUILATERAL, new Triangle(3, 3, 3).classify());
        assertEquals(Triangle.Type.EQUILATERAL, new Triangle(2, 2, 2).classify());
        assertEquals(Triangle.Type.EQUILATERAL, Triangle.classify(5, 5, 5));
    }

    @Test
    public void testIsosceles() {
        assertEquals(Triangle.Type.ISOSCELES, new Triangle(3, 3, 2).classify());
        assertEquals(Triangle.Type.ISOSCELES, new Triangle(3, 2, 3).classify());
        assertEquals(Triangle.Type.ISOSCELES, new Triangle(2, 3, 3).classify());
        assertEquals(Triangle.Type.ISOSCELES, Triangle.classify(2, 3, 3));
        assertEquals(Triangle.Type.ISOSCELES, Triangle.classify(3, 3, 2));
        assertEquals(Triangle.Type.ISOSCELES, Triangle.classify(3, 2, 3));
    }
}

What do you think? What would you do differently? How can it be better?

share|improve this question
1  
Both classifyValidTriangle and classify are public, that's strange. –  maaartinus yesterday

5 Answers 5

You need to pick a model, and stick with it. You have duplicated much of your logic at the static level and the instance level. Specific cases are the classify() instance method, and the classify(int,int,int) static method. There is no need for both.

Additionally, the logic split between the two validation methods isSaneArguments and isTriangle should all be in one method (I recommend isTriangle). There is no place you call them separately, so there is no reason to split them. isSaneArguments is a poor name anyway.... Finally, it's not exactly 'yoda conditions', but I prefer to do calculations on the left-side of a comparison operator like < or >. Bracing them makes it more readable in a chain of arguments...:

isTriangle(final int a, final int b, final int c) {
    // positive lengths, and sides that can meet
    return a > 0 && b > 0 && c > 0  && (a + b) > c && (a + c) > b && (b + c) > a;
}

The way your class is laid out lends itself to having static calls to get the properties of the triangles. It appears that the class is designed to regularly encounter invalid triangles, (hence Type.INVALID), so having a constructor throw an exception is unpleasant. I would make the constructor private, and do the validation outside the constructor, and would keep the methods static...

Note, classifyValidTriangle is public, but does no validation, (like the name says), this is odd.... to trust the caller that much. I would still expect validation of arguments, and an IllegalArgumentException if they are not met.

The classify method would also be better if written as an early-return mechanism, instead of holding the type variable....

Note that the actual Triangle instance does not do much, and there is no apparent purpose for it.

Neither your class, nor the embedded enum are public, this seems odd.

Finally, I rearranged the code to put the enum at the top, the static methods next, then the fields, and constructor:

public class Triangle {

    public enum Type {
        EQUILATERAL, INVALID, ISOSCELES, SCALENE
    }

    public static Triangle buildTriangle(int a, int b, int c) {
        if (isTriangle(a, b, c)) {
            return new Triangle(a, b, c);
        }
        throw new IllegalArgumentException("input values are not able to make a triangle");
    }

    public static Type classify(int a, int b, int c) {
        if (!isTriangle(a, b, c)) {
            return Type.INVALID;
        }
        if (a == b && b == c) {
            return Type.EQUILATERAL;
        }
        if (b == c || a == b || c == a) {
            return Type.ISOSCELES;
        }
        return Type.SCALENE;
    }

    private static boolean isTriangle(int a, int b, int c) {
        // positive lengths, and sides that can meet
        return a > 0 && b > 0 && c > 0  && (a + b) > c && (a + c) > b && (b + c) > a;
    }

    private final int a;
    private final int b;
    private final int c;

    private Triangle(int a, int b, int c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

}
share|improve this answer
    
Thanks for the review! Good points, I agree with most of it. About early returns, I agree, but I have a bit of a dilemma: programmers.stackexchange.com/questions/256301/… About rearranging the code, your suggestion doesn't conform to the recommendation of checkstyle: checkstyle.sourceforge.net/config_coding.html#DeclarationOrder (and probably JLS too, but I can't find the right link now) –  janos yesterday
    
Putting the fields in the middle is IMHO bad as it makes them really hard to find. Some put it on the top just after the inner classes but this makes them also a bit hard to find. So I put them at the bottom. ;) I guess, there are as many styles as people. –  maaartinus yesterday
    
I really disagree with all your comments (there's just one). It shows that it should be written differently (maybe boolean positiveLengths = ...; if (!positiveLengths) return false; ...). –  maaartinus yesterday
    
-1: Contains bugs and false advice. In trying to simplify the boolean conditions you added a bug that renders any valid triangle to be not a triangle. I wouldn't have combined both methods at all precisely for the complications this may cause. –  Pimgd yesterday
    
@Pimgd - Wrong-sense of < to > appreciated, and fixed. No, I did not run the code... but what wrong advice? –  rolfl yesterday

I would restructure the Triangle class as follows

public class Triangle {

    public Triangle(int a, int b, int c) {
        // validation and instantiation
    }

    public static enum Type {
        EQUILATERAL {
            @Override
            boolean matches(Triangle t) {
                // check if the given triangle matches this type
            }
        },
        ISOSCELES {
            @Override
            boolean matches(Triangle t) {
                // check if the given triangle matches this type
            }
        },
        SCALENE {
            @Override
            boolean matches(Triangle t) {
                // check if the given triangle matches this type
            }
        };

        private static Type classify(Triangle t) {
            for (Type type : values()) {
                if (type.matches(t)) {
                    return type;
                }
            }
            throw new IllegalArgumentException();
        }

        abstract boolean matches(Triangle t);
    }

    public Type getType() {
        try {
            return Type.classify(this);
        } catch (IllegalArgumentException e) {
            throw new IllegalStateException(e);
        }
    }
}

Some points to note

  1. The constructor should carry out all the validations so that once you get a Triangle object you have a valid triangle to work with
  2. If you have a valid triangle there is no reason to keep the Type.INVALID type
  3. The triangle classification logic is put into the Type enum itself for a better encapsulation
  4. When uncertain I tend to make the access modifiers as private as possible
share|improve this answer
    
Excellent points, I like this! –  janos yesterday
    
IllegalStateException must be IAE. –  maaartinus yesterday
    
@maaartinus you're absolutely right, updated my answer. –  zafarkhaja yesterday
    
-1: If a == b && b == c then EQUILATERAL. They have three equal sides. First point about classification logic invalid. Second point is flawed, you're not dealing with points. You're dealing with side lengths. Third point is only valid if you don't use else-if... which is only necessary with your re-write. Not a very strong point either. –  Pimgd yesterday
    
@Pimgd, you're right too. That was silly of me to make that false assumption. Sorry to confuse every one. Will remove those points from my answer. –  zafarkhaja yesterday
public static Type classify(int a, int b, int c) {
    if (!isSaneArguments(a, b, c) || !isTriangle(a, b, c)) {
        return Type.INVALID;
    }
    return classifyValidTriangle(a, b, c);
}

It's a classify function that takes a set of integers and if they're not all positive or they don't make a triangle then it's regarded as an invalid triangle.

Then you have an implicit else that works with a "valid" triangle.

It's basically begging for this addition:

public static boolean isValidTriangle(int a, int b, int c){
    return isSaneArguments(a, b, c) && isTriangle(a, b, c);
}

public static Type classify(int a, int b, int c) {
    if (!isValidTriangle(a, b, c)) {
        return Type.INVALID;
    }
    return classifyValidTriangle(a, b, c);
}

Additionally, for any three integers a b c, if a = 0 then a + b > c && a + c > b is false. Because that would imply that b > c and c > b. Thus I'd rename isTriangle to isValidTriangle, opposed to the function I described above.

share|improve this answer

Just a quick detail I haven't seen in the other answers:

if (!isSaneArguments(a, b, c)) {
    throw new IllegalArgumentException("All sides of a Triangle must be > 0");
}

First off, I don't really like the name isSaneArguments. But really, my point is that you shouldn't separate the validation logic from the message.

The information that failure of the validation means that either parameter was <= 0 is information that belongs inside the validation method because the caller should actually not know what exactly happened in that method.

A common approach would be

public void doSomething( int a, int b ) {
    checkArgumentsArePositive( a, b );

    // Do awesome stuff
}

public void checkArgumentsArePositive( int... arguments ) {
    for( int argument : arguments ) {
        if( argument <= 0 ) {
            throw new IllegalArgumentException( "Argument must be positive, but found " + argument );
        }
    }

}

Of course, there are many other ways to do this as well.

share|improve this answer

Others have already covered the major points, but here's an observation that make things a bit simpler. Within the constructor, you could sort the sides from smallest to largest. Your constructor could contain this:

int[] side = new int[3];
side[0] = a;
side[1] = b;
side[2] = c;
Arrays.sort(side);

Now checking for 0 (or negative length) sides is done like so:

if (side[0] <= 0) {
    throw new IllegalArgumentException("All sides of a Triangle must be > 0");
}

Checking for validity:

if (side[0]+side[1] <= side[2]) {
    throw new IllegalArgumentException("These sides cannot form a Triangle.");
}
share|improve this answer

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.