Implementieren Sie eine abstrakte Shape-Klasse

132416
Phrancis

Um mehr über OOP zu erfahren, forderte @nhgrif mich auf, eine abstrakte Shape-Klasse zu implementieren (weitere Details in den Codeanmerkungen unten. So habe ich es gemacht. Alle Ratschläge waren dankbar!)

Shape.java

/* nhgrif says:
 * Phrancis ready for inheritance/polymorphism?
 * Given the following abstract class:
 *
 *  public abstract class Shape {
 *      public abstract double area();
 *      public abstract double perimeter();
 *  }
 *
 * Implement a Circle, Triangle, and Rectangle class which extend the class Shape.
 * Ex: public class Circle extends Shape ... etc
 */

public abstract class Shape {
    public abstract double area();
    public abstract double perimeter();
}

Rechteck.java

public class Rectangle extends Shape {
    private final double width, length; //sides

    public Rectangle() {
        this(1,1);
    }
    public Rectangle(double width, double length) {
        this.width = width;
        this.length = length;
    }

    @Override
    public double area() {
        // A = w * l
        return width * length;
    }

    @Override
    public double perimeter() {
        // P = 2(w + l)
        return 2 * (width + length);
    }

}

Kreis.java

public class Circle extends Shape {
    private final double radius;
    final double pi = Math.PI;

    public Circle() {
        this(1);
    }   
    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        // A = π r^2
        return pi * Math.pow(radius, 2);
    }

    public double perimeter() {
        // P = 2Ï€r
        return 2 * pi * radius;
    }
}

Dreieck.java

public class Triangle extends Shape {
    private final double a, b, c; // sides

    public Triangle() {
        this(1,1,1);
    }
    public Triangle(double a, double b, double c) {
        this.a = a;
        this.b = b;
        this.c = c;
    }

    @Override
    public double area() {
        // Heron's formula:
        // A = SquareRoot(s * (s - a) * (s - b) * (s - c)) 
        // where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle 
        double s = (a + b + c) / 2;
        return Math.sqrt(s * (s - a) * (s - b) * (s - c));
    }

    @Override
    public double perimeter() {
        // P = a + b + c
        return a + b + c;
    }
}

Und so habe ich alles getestet, was alles wie vorgesehen funktioniert:

TestShape.java

public class TestShape {
    public static void main(String[] args) {

        // Rectangle test
        double width = 5, length = 7;
        Shape rectangle = new Rectangle(width, length);
        System.out.println("Rectangle width: " + width + " and length: " + length
                + "\nResulting area: " + rectangle.area()
                + "\nResulting perimeter: " + rectangle.perimeter() + "\n");

        // Circle test
        double radius = 5;
        Shape circle = new Circle(radius);
        System.out.println("Circle radius: " + radius
            + "\nResulting Area: " + circle.area()
            + "\nResulting Perimeter: " + circle.perimeter() + "\n");

        // Triangle test
        double a = 5, b = 3, c = 4;
        Shape triangle = new Triangle(a,b,c);
        System.out.println("Triangle sides lengths: " + a + ", " + b + ", " + c
                + "\nResulting Area: " + triangle.area()
                + "\nResulting Perimeter: " + triangle.perimeter() + "\n");
    }
}
Antworten
32
Now do the tricky ones: ellipse and square. Pete Becker vor 4 Jahren 6
Nun, Platz könnte Rechteck gerade verlängern. Ellipse wäre aber sicher interessant. Legato vor 4 Jahren 1
@Legato - oder Rechteck kann sich quadratisch erstrecken. Pete Becker vor 4 Jahren 0
@PeteBecker Nicht jedes Rechteck ist ein Quadrat. Legato vor 4 Jahren 0
@Legato - manchmal. Nicht immer. Pete Becker vor 4 Jahren 0
Ja, und ein Rechteck, das sich aus einem Quadrat erstreckt, würde immer bedeuten. Legato vor 4 Jahren 4
Lassen Sie uns [diese Diskussion im Chat fortsetzen] (http://chat.stackexchange.com/rooms/21825/discussion-between-legato-and-pete-becker). Legato vor 4 Jahren 0
Auf Ihrer `Circle.java` haben Sie ein` @ Override` auf der `perimeter ()` Methode vergessen. Ismael Miguel vor 4 Jahren 0
Ein Quadrat sollte niemals ein Rechteck erweitern oder umgekehrt. Dies ist eine direkte Verletzung des Liskov-Substitutionsprinzips und kann zu unerwarteten Nebenwirkungen führen. Darren Young vor 4 Jahren 0
@ DarrenYoung Ist ein Quadrat kein Rechteck? Koray Tugay vor 4 Jahren 0
@PeteBecker Square Rechteck erweitern ist nur zulässig, wenn sie unveränderlich sind. Der umgekehrte Weg ist nicht erlaubt. RobAu vor 4 Jahren 1
Ich liebe die Ordentlichkeit des Codes, nur ein Gedanke / Zweifel seit `` `Rectangle````` `Circle```` `` Triangle``` ist ein `` Shape``` und sie haben alle Seiten. sollten Seiten also nicht Teil von `` Shape``` sein. Shahbaz Khan vor 2 Jahren 0
Ein "Circle" hat unendlich viele Seiten. Denk darüber nach. Wie würde also "Shape" die Seiten eines Kreises darstellen? Tamoghna Chowdhury vor 2 Jahren 0
Sieht sehr nach https://github.com/adsouza/Euclidean-Geometry/tree/master/Java aus quikchange vor 2 Jahren 0

5 Antworten auf die Frage

21
rolfl

Ich bin im Allgemeinen beeindruckt von der Konsistenz der Implementierungen, der Sauberkeit usw.

Ich habe einen Kommentar zur Grundannahme. Die Shapesollte keine abstrakte Klasse sein, sondern eine Schnittstelle. Es gibt keine konkreten Implementierungen für irgendeine Methode, und wenn man es zu einer abstrakten Klasse macht, ist es auch schwierig, von anderen Orten zu erben. Betrachten Sie das stattdessen:

public interface Shape {
    public double area();
    public double perimeter();
}

Darüber hinaus sollten Sie Folgendes beachten:

  • Das piFeld in Ihrer CircleKlasse sollte privat sein. Es ist nicht notwendig, eine bereits öffentliche Konstante auf andere Weise wieder freizulegen.

  • Ich würde die Standardkonstruktoren "unit" vermeiden. Sie helfen mit nichts. Wann möchte jemand anrufen new Triangle()und die Abmessungen nicht haben?

  • Ihren Klassen fehlt eine toString()Methode. Diese sind aus vielen Gründen nützlich, insbesondere beim Debugging.

  • Sie haben keinen Plan für out-of-bound-Dimensionen. Was machen Sie mit negativem, NaN oder unendlichem Input?

Ich würde sogar vorschlagen, kein `pi`-Feld zu haben und` Math.PI` direkt zu verwenden. Oder führen Sie einen `import static java.lang.Math.PI;` aus und verwenden Sie `PI` direkt Ivo Beckers vor 4 Jahren 2
`interface`-Methoden sind standardmäßig` public`. (Einer meiner Pet Peeves sind redundante Modifikatoren - Java ist bereits ausführlich genug). Boris the Spider vor 4 Jahren 0
@BoristheSpider - Ja, sie sind standardmäßig öffentlich, ich verstehe Ihre Bedenken und Sie haben Recht, dass darauf hingewiesen werden sollte. Zufällig lernte ich Java in einem Job, in dem die Richtlinien für den Codestil dies erforderten, und es ist eine Gewohnheit, die ich beibehalten habe, und ich ziehe es jetzt vor, es explizit zu machen. Ich behaupte nicht, dass ich Recht habe, aber ich behaupte, dass es nicht "meine" Antwort wäre, wenn ich sie ausschneide ;-) rolfl vor 4 Jahren 2
12
Aleksandr Dubinsky

Ein Nitpick auf Namen: Die Abmessungen eines Rechtecks ​​sollten widthund heightnicht widthund sein length. Der Konstruktor von Triangle sollte beschreibende Parameternamen annehmen, z side1Length. Sie haben mehr Entschuldigung für die Verwendung aund bfür private Felder (insbesondere in diesem kurzen, reinen mathematischen Code), aber Namen mit einem Zeichen sind im Allgemeinen verpönt.

Fügen Sie Javadoc-Kommentare hinzu, zumindest zu den Methoden in Ihrer abstrakten Klasse / Schnittstelle. Der Kommentar zu Ihrer ShapeKlasse ist fast ein Javadoc, aber es fehlt ein Kommentar *.

Ersetzen Sie Ihre main()Methode durch 3 Unit-Tests (1 Testklasse mit 3 Methoden), die problemlos von der IDE aus ausgeführt werden können.

Ich würde eine Einheitstestklasse für die _each_-Implementierung vorschlagen - dies ist der akzeptierte Standardansatz. Ansonsten alles sehr gute Punkte. Boris the Spider vor 4 Jahren 0
@BoristheSpider Sie haben zwar Recht, aber persönlich würde ich mich in einem solch trivialen Fall nicht an diese Regel halten, sondern die Anzahl der Testklassen reduzieren und die Wiederverwendung von Testcode zu einer einfachen Sache der privaten Methode `testShape (Shape Shape, double) machen expectedArea, double expectedPerimiter) `in` class ShapeTest` statt eine Erbschafts-Hierarchie zu erstellen. Aleksandr Dubinsky vor 4 Jahren 0
7
Legato

Ich bin froh, dass Sie das getan haben, und gibt mir die Chance, einige Fehlinformationen weiter durchzusetzen / zu korrigieren.

final double pi = Math.PI;Dies ist ein Umstand, bei dem der Wert pi statisch sein sollte und entsprechend als GROSSBUCHSTABEN geschrieben werden würde PI.

  • Statisch, da der Wert von pi nicht von einer Instanz von Circle abhängig ist
  • Schlussendlich, da Sie natürlich nichts daran ändern wollen
  • Großbuchstaben, da es beides ist, wird es als Konstante betrachtet, also die Namenskonvention.

Es ist jedoch nicht erforderlich, dass Sie Namespace oder Speicher belegen, wenn Sie sie direkt verwenden können Math.PI . Beispielsweise double area()könnte die Implementierung Ihrer Methode Folgendes sein:

return Math.PI * Math.pow(radius, 2);

Aus demselben Grund anstatt Platzhaltervariablen zu haben, die nicht anderweitig verwendet werden:

double a = 5, b = 3, c = 4;
Shape triangle = new Triangle(a,b,c);

Verwenden Sie sie einfach

Shape triangle = new Triangle(5, 3, 4);

a, b und c sind nicht wirklich passende Namen, wenn Sie sie verwenden möchten, versuchen Sie, in diesem Fall beschreibende Namen wie base, side1 und side2 zu verwenden.

Ich mag es, dass Sie die abstrakte Klasse verwenden, um in Ihren Tests zu deklarieren. Dadurch erhalten Sie in Zukunft eine gewisse Flexibilität. Vergewissern Sie sich, dass Sie sich auf bestimmte Methoden verlassen, die nur in einer Unterklasse vorhanden sind, die Sie deklarieren müssen diese Unterklasse.

Die Platzhaltervariablen erhöhen die Lesbarkeit. Aleksandr Dubinsky vor 4 Jahren 0
Ich stimme nicht zu, dass a, b und c keine guten Namen sind. Die Verwendung mehrerer aufeinanderfolgender Einzelbuchstaben für Felder wäre normalerweise ein Codegeruch. In diesem Fall sind sie jedoch angemessen. Sie sind die Standardnamen für die drei Seiten eines Dreiecks und sind in der Dreiecksgeometrie allgegenwärtig. Wenn sie als Basis, Seite1 und Seite2 bezeichnet werden, deutet dies fälschlicherweise an, dass die drei unterschiedliche Bedeutungen haben. jwg vor 4 Jahren 1
6
Simon Forsberg

Ein kleiner Nitpick, wirklich:

// where s = (a + b + c) / 2, or 1/2 of the perimeter of the triangle 
double s = (a + b + c) / 2;

In dem Kommentar beziehen Sie sich auf den Umfang des Dreiecks, aber in Ihrem Code verwenden Sie nicht den tatsächlichen Umfang. Dies ist eine sehr kleine Code-Duplizierung. Ihr Code wäre etwas selbstdokumentierender, wenn Sie Folgendes tun würden:

double s = perimeter() / 2;

Ich persönlich habe nichts gegen die Namen a, b, c. Die Seiten werden in Mathematik oft so genannt, ich glaube nicht, dass es furchtbar falsch ist, sie auch hier zu nennen.

Dies ist ein untergeordneter Punkt, aber ein sehr guter Punkt. jwg vor 4 Jahren 1
4
juunas

Ich persönlich denke, dass der Standard-Konstruktor no-arg sehr unnötig ist, wenn es sich um unveränderliche Klassen handelt. Ist ein Rechteck mit der Breite 1 und der Höhe 1 standardmäßig sinnvoll?

Wenn die Klassen unveränderlich sind, können Sie auch die Flächen und Perimeter gleich berechnen, sodass Sie sie einfach mit den entsprechenden Methoden zurückgeben können.

Ich denke auch, dass die Benennung von Methoden besser sein könnte. Anstelle area()würde ich verwenden getArea().

Das geänderte Rechteck würde folgendermaßen aussehen (Beachten Sie, dass die Shape-Klasse auch bearbeitet werden müsste):

public class Rectangle extends Shape {
  private final double width, height, area, perimeter;

  public Rectangle(double width, double height) {
    this.width = width;
    this.height= height;
    this.area = width * height;
    this.perimeter = 2 * (width + height);
  }

  @Override
  public double getArea() {
    return this.area;
  }

  @Override
  public double getPerimeter() {
    return this.perimeter;
  }

}
"getX" ist normalerweise ein Zeichen dafür, dass die Methode einen vordefinierten Wert zurückgibt. Das ist hier der Fall, macht es aber für seine Implementierung ungeeignet. Ich sehe nicht wirklich ein Problem mit dem Namen, aber wenn er es ändern müsste, würde ich wahrscheinlich 'computeX' wählen Legato vor 4 Jahren 0
Richtig .. Oder vielleicht `berechne '(?)? juunas vor 4 Jahren 0
Funktioniert genauso gut. Legato vor 4 Jahren 0
Aus * Effective Java *: `Methoden, die eine nicht-boolesche Funktion oder ein Attribut des Objekts zurückgeben, für das sie aufgerufen werden, werden normalerweise mit einem Nomen, einer Nomen-Phrase oder einer Verb-Phrase benannt, die beispielsweise mit dem Verb get beginnen , size, hashCode oder getTime. Es gibt ein stimmliches Kontingent, das behauptet, dass nur die dritte Form (beginnend mit get) akzeptabel ist, aber es gibt wenig Grundlage für diese Behauptung. Die ersten beiden Formen führen normalerweise zu besser lesbarem Code. `Eine Wertklasse mit wenigen Nicht-Getter-Methoden kann ein Substantiv wie` area () `verwenden. "berechneFläche" deutet an, dass die Funktion Nebenwirkungen hat oder merklich langsam ist. Aleksandr Dubinsky vor 4 Jahren 2