Anteckningar från föreläsning 1

Detta är en kort sammanfattning av vad vi gjorde under föreläsningen, OH-bilderna finns här -- det finns länkar till git-repositories med koden nere i texten.

Olika kriterier på vad som är god programkod

Vi började med att diskutera olika termer för att beskriva kod-kvalitet (se OH-bilderna), och gick därefter igenom en teknik, top down, som kan hjälpa oss att att skriva program som undviker olika slags 'code smells'.

Exempel på 'Top Down' design

Källkoden till nedanstående exempel kan laddas hem med:

git clone git://vm55.cs.lth.se/omd/lect/sum-of-primes.git

Vi ville ha ett program som skriver ut summan av alla de kommandoradsparametrar som är primtal, och jag skrev först huvudprogrammet (efter att ha skapat ett program-objekt, och anropat run på det):

class SumOfPrimes {

    public static void main(String[] args) {
        new SumOfPrimes().run(args);
    }

    void run(String[] args) {
        List<Integer> numbers = findNumbers(args);
        List<Integer> primes = findPrimes(numbers);
        int total = sum(primes);
        System.out.println("The sum is " + total);
    }
}

Det återstår nu 'bara' att implementera findNumbers, findPrimes och sum, och redan kan vi se några av poängerna med top-down-metoden:

Vi grep oss sedan an findNumbers, som alltså skall returnera en lista med alla tal som förekom på kommandoraden (dvs som finns i vektorn args i huvudprogrammet). Även här blev vår uppgift enklare om vi önsketänkte, denna gång hade det varit bra om vi hade haft en isNumber-funktion:

    List<Integer> findNumbers(String[] strings) {
        List<Integer> found = new LinkedList<Integer>();
        for (String s : strings) {
            if (isNumber(s)) {
                found.add(Integer.parseInt(s));
            }
        }
        return found;
    }

... och även när vi skriver vår isNumber-funktion kan vi önsketänka, uppgiften bli enklare om vi har en isDigit-funktion tillhands:

    boolean isNumber(String s) {
        for (int k = 0; k < s.length(); k++) {
            if (!isDigit(s.charAt(k))) {
                return false;
            } 
        }
        return true;
    }

Att bestämma om ett tecken är en siffra eller inte är så enkelt att vi inte längre behöver önsketänka -- vi skriver bara:

    boolean isDigit(char c) {
        return '0' <= c && c <= '9';
    }

Resten av programmet blev:

    int sum(List<Integer> numbers) {
        int s = 0;
        for (int k : numbers) {
            s += k;
        }
        return s;
    }

    List<Integer> findPrimes(List<Integer> numbers) {
        List<Integer> found = new LinkedList<Integer>();
        for (int k : numbers) {
            if (isPrime(k)) {
                found.add(k);
            }
        }
        return found;
    }

    boolean isPrime(int n) {
        if (n < 2) {
            return false;
        }
        if (n == 2) {
            return true;
        }
        if (isEven(n)) {
            return false;
        }
        for (int k = 3; k * k <= n; k += 2) {
            if (n % k == 0) {
                return false;
            }
        }
        return true;
    }

    boolean isEven(int n) {
        return n % 2 == 0;
    }
}

Det finns en del att säga om koden ovan, på torsdag kommer jag att visa sätt att förenkla den (om jag hinner, jag var alldeles för långsam under den första föreläsningen, och skyller det på ringrost...).

Observera att top-down-metoden inte nödvändigtvis har med objektorienterad programmering att göra, den är användbar i många sammanhang. Och man kan mycket väl även önsketänka fram nya klasser och metoder i dessa klasser.

Om att gömma objekts interna tillstånd

Källkoden till nedanstående exempel kan laddas hem med:

git clone git://vm55.cs.lth.se/omd/lect/points.git

Vi gav oss sedan på ett exempel som var mer direkt kopplat till objektorientering, nämligen följande två klasser:

class Point {

    private double x, y;

    public Point  (double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public double getY() {
        return y;
    }

    public void setX(double x) {
        this.x = x;
    }

    public void setY(double y) {
        this.y = y;
    }
}

class Segment {

    private Point p0, p1;

    public Segment  (Point p0, Point p1) {
        this.p0 = p0;
        this.p1 = p1;
    }

    public double length() {
        double dx = p0.getX() - p1.getX();
        double dy = p0.getY() - p1.getY();
        return Math.hypot(dx, dy);
    }
}

Vi pratade lite om koden, och jag tror att åtminstone några av er höll med mig om att:

Så vi flyttade beräkningen av avstånd till klassen Point, och tog samtidigt bort get- och set-metoderna (de är i sig en 'code smell'):

class Point {

    private double x, y;

    public Point  (double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double distanceTo(Point other) {
        double dx = x - other.x;
        double dy = y - other.y;
        return Math.hypot(dx, dy);
    }
}

class Segment {

    private Point p0, p1;

    public Segment  (Point p0, Point p1) {
        this.p0 = p0;
        this.p1 = p1;
    }

    public double length() {
        return p0.distanceTo(p1);
    }
}

Vi diskuterade sedan hur man kan generalisera detta, och införde ett Point-interface:

interface Point {

    public double distanceTo(Point other);
}

class Point2D implements Point {

    private double x, y;

    public Point2D  (double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double distanceTo(Point other) {
        double dx = x - ref(other).x;
        double dy = y - ref(other).y;
        return Math.hypot(dx, dy);
    }

    private Point2D ref(Point other) {
        return (Point2D)other;
    }
}

Till slut skrev vi även en klass för 3D-punkter:

class Point3D implements Point {

    private double x, y, z;

    public Point3D  (double x, double y, double z) {
        this.x = x;
        this.y = y;
        this.z = z;
    }

    public double distanceTo(Point other) {
        double dx = x - ref(other).x;
        double dy = y - ref(other).y;
        double dz = z - ref(other).z;
        return Math.sqrt(dx*dx + dy*dy + dz*dz);
    }

    private Point3D ref(Point other) {
        return (Point3D)other;
    }
}

Vi kan använda vår klass Segment utan att göra några som helst ändringar (följande väldigt primitiva test finns i det repository som ni kan klona ovan):

class Test implements Testing {

    public static void main(String[] args) {
        new Test().run();
    }

    void run() {
        Point
            pp0 = new Point2D(0, 0),
            pp1 = new Point2D(4, 0),
            pp2 = new Point2D(4, 3),
            ps0 = new Point3D(0, 0, 0),
            ps1 = new Point3D(1, 1, 1),
            ps2 = new Point3D(3, 4, 0);
        Segment
            sp1 = new Segment(pp0, pp1),
            sp2 = new Segment(pp1, pp2),
            sp3 = new Segment(pp2, pp0),
            revsp3 = new Segment(pp0, pp2),
            ss1 = new Segment(ps0, ps1),
            ss2 = new Segment(ps1, ps2),
            ss3 = new Segment(ps2, ps0),
            revss3 = new Segment(ps0, ps2);
        test(close(sp1.length(), 4));
        test(close(sp2.length(), 3));
        test(close(sp3.length(), 5));
        test(close(sp3.length(), revsp3.length()),
             "segment has same length as its reverse");
        test(close(ss1.length(), Math.sqrt(3)));
        test(close(ss2.length(), Math.sqrt(14)));
        test(close(ss3.length(), 5));
        test(close(ss3.length(), revsp3.length()),
             "segment has same length as its reverse");
    }
}

interface Testing {

    double eps = 1e-6;

    default boolean close(double a, double b) {
        return Math.abs(a/b - 1) < eps;
    }

    default void test(boolean condition) {
        test(condition, "(no label)");
    }

    default void test(boolean condition, String label) {
        if (condition) {
            System.out.println("passed: " + label);
        } else {
            throw new RuntimeException("failed: " + label);
        }
    }
}

Nästa föreläsning äger rum på torsdag, klockan 13-15 i V:C (sic!).