Najlepszy Java Developer – Zadanie 6: OOP

Zadanie

Póki co nie mam żadnych wieści o nowym zadaniu od Pablo, ale to nie znaczy, że mamy się wszyscy lenić.

Zadanie 6 będzie dosyć elastyczne i zarazem proste. Chcę sprawdzić Waszą wiedzę z obiektowości i zobaczyć jak zaprojektowalibyście kilka klas.

Zadaniem jest stworzenie czterech klas:

  • Square,
  • Rectangle,
  • Triangle
  • Rhombus

Każda z tych klas ma przechowywać w sobie informację potrzebne do obliczenia jej pola oraz obwodu. Oznacza to też, że każda z tych klas powinna mieć w sobie metodę:

  • getArea() – odpowiedzialną za policzenie pola powierzchni figury,
  • getCircuit() – odpowiedzialną za obliczenie obwodu figury.

Dla każdej z tych metod dla każdej z klas należy napisać po dwa testy jednostkowe np. przy użyciu JUnit (Maven + JUNIT).

Mając 4 klasy po 2 metody oraz każda z nich ma mieć po 2 testy – co daje nam łącznie 16 testów.

Punktacja

Za każdy napisany test otrzymasz 25 punktów, za stworzenie klas otrzymasz 600 punktów. Nie każde rozwiązanie, które będzie działać otrzyma 600 punktów, ocena będzie indywidualna.

Porady

  • Skorzystaj z polimorfizmu,
  • DRY – don’t repeat yourself,
  • Zacznij od napisania testów jednostkowych,
  • Pamiętaj o hermetyzacji,
  • Zastanów się najpierw nad rozkładem klas i ich zależnościami (cechami wspólnymi),
  • Pytaj, gdy coś nie jest dla Ciebie jasne.

Czas

Zadanie zostało opublikowane 8 stycznia, a jego rozwiązania można przesyłać do 18 stycznia do godziny 23.59. Zadania wysłane później będą automatycznie usuwane.

Format

W tym zadaniu nie ma żadnego szablonu projektu, jest to zadanie, w którym możesz pokazać swoje umiejętności projektowania klas.

Zadanie należy wysłać na email: njd@1024kb.pl z tematem: TWÓJ-NICK_OOP.

Zadanie przesyłamy jako repozytorium Git – może być hostowany na GitHub, GitLab, Bitbucket – gdzie tylko chcesz. W wiadomości podajemy tylko link do repozytorium projektu. 😉

Pamiętajcie, aby do pliku .gitignore dodać:

  • /target
  • /out
  • /.idea
  • *.iml

I inne pliki/katalogi, które nie powinny być na zdalnym repozytorium.

W razie jakichkolwiek wątpliwości pytajcie jak ma wyglądać finalna aplikacja. 😉

Rozwiązanie

Na początku rozwiązywania skupiłem się na zaprojektowaniu tylko poziomu samej abstrakcji – interfejsy, puste klasy. W tym momencie zastanowiłem się, które klasy mają ze sobą coś wspólnego – jeśli mają to co to jest.

Zaczęło się wszystko od stworzenia interfejsu Figure, który ma metody getArea() oraz getCircuit() – czemu tak do tego podszedłem? Jednym z wymogów zadania było to, aby każda figura miała właśnie te metody. Tworząc taki interfejs zapewniłem to sobie.

Następnie utworzyłem cztery klasy: Square, Rectangle, Triangle, Rhombus – każda z nich implementowała powyższy interfejs. Utworzyłem ciała metod, a samą implementację odpuściłem i wróciłem do szukania kolejnych cech wspólnych.

Możliwe, że sporo osób ze szkoły podstawowej wyniosło taką o to prawdę:

Kwadrat jest prostokątem, zaś prostokąt nie jest kwadratem.

Mając taką wiedzę mogłem zaimplementować dziedziczenie, które w końcu może być czytane: obiekt typu A jest typu B, ale typ B nie jest typu A. No brzmi to tak samo jak prawda powyżej. I owe dziedziczenie prezentuje się tak:

public class Square extends Rectangle {
    public Square(double sideA) {
        super(sideA, sideA);
    }
}

Warto zauważyć, że wszystkie stworzone klasy mają już utworzone potrzebne pola oraz konstruktory. Brak jedynie implementacji metod.

Mając już stworzone wszystkie potrzebne klasy mogę przejść do napisania testów jednostkowych zgodnie z TDD.

Testy

Wymagałem 16 testów jednostkowych tylko dlatego, aby w Was wymusić ich pisanie i wyrabianie w sobie nawyku pisania testów jednostkowych. Jak wiadomo testowanie metod takich z jakimi my mamy teraz styczność jest raczej bez sensu – na pewno w takiej ilości. Wystarczyłoby po jednym teście dla każdej z metody. No ale zobaczmy jak ja podszedłem do testów.

import org.junit.Assert;
import org.junit.Test;

public class RectangleTest {
    private final double DELTA = 10e-15;

    @Test
    public void testRectangleCircuitOne() {
        double a = 10;
        double b = 20;

        Figure rectangle = new Rectangle(a, b);

        Assert.assertEquals(60, rectangle.getCircuit(), DELTA);
    }

    @Test
    public void testRectangleCircuitTwo() {
        double a = 10000;
        double b = 0.001;

        Figure rectangle = new Rectangle(a, b);

        Assert.assertEquals(20000.002, rectangle.getCircuit(), DELTA);
    }

    @Test
    public void testRectangleAreaOne() {
        double a = 0.001;
        double b = 0.0005;

        Figure rectangle = new Rectangle(a, b);

        Assert.assertEquals(0.0000005, rectangle.getArea(), DELTA);
    }

    @Test
    public void testRectangleAreaTwo() {
        double a = 10000;
        double b = 1000;

        Figure rectangle = new Rectangle(a, b);

        Assert.assertEquals(10000 * 1000, rectangle.getArea(), DELTA);
    }
}

Nie ma tutaj żadnego rocket science, dlatego nie będę wklejał wszystkich testów. Resztę można będzie zobaczyć w repozytorium.

Ważny jest tutaj jeden element – po napisaniu wszystkich testów powinniśmy je uruchomić i wszystkie powinny zaświecić się na czerwono w IDE – po prostu powinny nie przejść.

Implementacja

Teraz naszym celem jest napisanie takiej implementacji, tak aby nasze testy przeszły. I już tutaj przypominam – w trudniejszych przypadkach testowych czasami warto poświęcić trochę więcej czasu na napisanie testów tak, aby nie pisać implementacji do błędnych testów.

Rectangle

public class Rectangle implements Figure {
    protected final double sideA;
    protected final double sideB;

    public Rectangle(double sideA, double sideB) {
        this.sideA = sideA;
        this.sideB = sideB;
    }

    public double getArea() {
        return sideA * sideB;
    }

    public double getCircuit() {
        return 2 * sideB + 2 * sideA;
    }
}

Square

Dzięki dziedziczeniu klasa Square nie musi mieć własnej implementacji.

public class Square extends Rectangle {
    public Square(double sideA) {
        super(sideA, sideA);
    }
}

Triangle

public class Triangle implements Figure {
    private final double sideA;
    private final double sideB;
    private final double sideC;
    private final double heightToSideA;

    public Triangle(double sideA, double sideB, double sideC, double heightToSideA) {
        this.sideA = sideA;
        this.sideB = sideB;
        this.sideC = sideC;
        this.heightToSideA = heightToSideA;
    }

    public double getArea() {
        return .5 * heightToSideA * sideA;
    }

    public double getCircuit() {
        return sideA + sideB + sideC;
    }
}

Rhombus

public class Rhombus implements Figure {
    private final double side;
    private final double height;

    public Rhombus(double side, double height) {
        this.height = height;
        this.side = side;
    }

    public double getArea() {
        return side * height;
    }

    public double getCircuit() {
        return 4 * side;
    }
}

Po zaimplementowaniu tych klas wszystkie testy powinny zaświecić się na zielono – jeśli nie to szukamy błędu i poprawiamy, aż do napotkania zielonego światła. 😉

 

Podsumowanie

Implementacje tych klas są naprawde proste – każdy mógł wybrać z jakich wzorów korzysta. Dla mnie podczas sprawdzania zadań najważniejsze było napisanie testów oraz sposób w jaki podeszliście do zaprojektowania klas.

Otrzymałem 5 rozwiązań każde bardzo się różniło, dlatego do każdego z autorów wysłałem osobne uzasadnienia i propozycje poprawy ich kodu.

Na pewno co często mnie bolało to to, że nie robiliście dziedziczenia Square -> Rectangle. Taki zabieg pozwalał na uniknięcie implementacji metod w klasie Square.

Inne błędy byłe ściśle związane z konkretną implementacją i propozycją autorów. Na pewno bardzo spodobały mi się rozwiązania, gdzie zostały wprowadzone dedykowane wyjątki i walidatory do sprawdzania długości boków.

Moje przykładowe rozwiązanie można znaleźć w tym repozytorium.