Here is an implementation of Circles and Ellipses in java. Please see CircleAndEllipseProblem for more information.
Could people please try and explain what could be wrong with this implementation? I have some ideas, but I won't scribble on your TabulaRasa? yet. --AalbertTorsius
There's nothing inherently wrong, however your example suffers from an update anomaly that can be arguably lead to 2 alternatives: either your Circle is not a proper subtype of Ellipse, or your Ellipse has an unintuitive contract that doesn't quite model the real world.
Ellipse e = new Ellipse(p,p, diameter); Circle c = new Circle (p,diameter);Now if Circle was a proper subtype of Ellipse the objects e, and c would be equivalent. They represent basically the same geometrical object. However after
c.setFocus1(p1) e.setFocus1(p1)The two no longer represent the same object as the setFocus1 implementation in Circle side-effects the focus2 which is an unexpected behavior. Imagine you pass your object to a an extrenal algorithm like translate(Ellipse e), and that algorithm calls
translate(Ellipse x, Point delta) { x.setFocus1 ( translate(x.getFocus1(), delta); x.setFocus2 ( translate( x.getFocus2(), delta); }Now given the e and c objects above:
translate(e); // works translate(c); //it does not workSo there are contexts in which your version of circles cannot impersonate your version of ellipses. --CostinCozianu
Hmm, I see. Part of the problem seems to be that, while the method used below being a common way of defining an ellipse, it's not in NormalForm, so to speak; the foci define both the position as well as the shape of the ellipse. How about the following?
shape eccentricity size length of the major axis (which is equal to the somewhat odd definition of diameter given below) location midpoint orientation angle of the major axis with the horizontalA Circle then has an eccentricity of 0.0; setting it to another value would have no effect, just as trying to set the eccentricity of any Ellipse to anything bigger than 1.0 would have no effect either.
or equivalently
Point midpoint; // location Point focus_relative_to_midpoint; // combines eccentricity and orientation; zero for circles. double size; // length of major axis(eccentricity, orientation_angle) is kind of like the polar notation compared to rectangular notation of focus_relative_to_midpoint (x,y).
The problem isn't one of NormalForms. The problem is one of closure. In order for an update method to work it has to be closed under it's own type (for obvious reasons). However, those same reasons also require that the method is closed for it's subtypes. In this particular case, SetFocus?() is closed under ellipses, but not under circles. Now this can be "fixed" by replacing SetFocus?() for circles with a different function (which is what is done in the original example), but this results in the update anomaly pointed out by Costin. Since the proposed alternative does this (just for SetEccentricity? instead of SetFocus?) as well, it also suffers from the same problem.
package geometry; public class Point { public double x; public double y; public Point() { this(0.0, 0.0); } public Point(double x, double y) { this.x = x; this.y = y; } public double distance(Point p) { return Math.hypot(p.x - x, p.y - y); } public boolean equals(Object o) { try { return equals((Point) o); } catch (ClassCastException cce) { return false; } } public boolean equals(Point p) { return this.x == p.x && this.y == p.y; } }
package geometry; // see http://mathworld.wolfram.com/Ellipse.html public class Ellipse { private Point focus1; private Point focus2; // The diameter is the distance from one focus, to the edge of the ellipse, // to the other focus, which is equal to the length of the major axis. private double diameter; public Ellipse(Point focus1, Point focus2, double diameter) { this.focus1 = focus1; this.focus2 = focus2; this.diameter = diameter; } public Ellipse(Ellipse e) { this.focus1 = e.focus1; this.focus2 = e.focus2; this.diameter = e.diameter; } public Point getFocus1() { return this.focus1; } public void setFocus1(Point focus1) { this.focus1 = focus1; } public Point getFocus2() { return this.focus2; } public void setFocus2(Point focus2) { this.focus2 = focus2; } public double getDiameter() { return this.diameter; } public void setDiameter(double diameter) { this.diameter = diameter; } public double getPerimeter() { if (isCircularShaped()) { return new Circle(this.focus1, this.diameter).getPerimeter(); } else { throw new CantCalculateThatException?("Sorry, don't know how"); // see http://mathworld.wolfram.com/Ellipse.html and http://mathworld.wolfram.com/Perimeter.html // return 2 * this.diameter * SuperMath?.ellipticIntegral(getEccentricity()); } } public double getDistanceBetweenFocii() { return this.focus1.distance(focus2); } public double getEccentricity() { return getDistanceBetweenFocii() / getDiameter(); } public boolean isCircularShaped() { return focus1.equals(focus2); } } class CantCalculateThatException? extends ArithmeticException? { public CantCalculateThatException?() { super(); } public CantCalculateThatException?(String s) { super(s); } }
package geometry; public class Circle extends Ellipse { public Circle(double diameter) { this(new Point(), diameter); } public Circle(Point focus, double diameter) { super(focus, focus, diameter); } public void setFocus(Point f) { super.setFocus1(f); super.setFocus2(f); } public void setFocus1(Point f) { setFocus(f); } public void setFocus2(Point f) { setFocus(f); } public void setRadius(double radius) { setDiameter(radius * 2.0); } public double getRadius(double radius) { return getDiameter() / 2.0; } public double getPerimeter() { return getCircumference(); } public double getCircumference() { return Math.PI * getDiameter(); } }