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.
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'.
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 har nu ett kort huvudprogram (ingen 'Long Method'-smell)
Huvudprogrammet har en ganska konsekvent konceptuell nivå, vi blandar inte komplicerade saker med trivialiteter
Vi har namn på det som skall göras, bara genom att läsa huvudprogrammet kan man enkelt lista ut vad programmet gör, vi behöver inte känna till detaljerna i vad våra funktioner gör.
Vi har fått ett antal funktioner som gör saker som kan vara användbara även i andra sammanhang.
Vi har fått ett antal funktioner att skriva, men vi vet att de alla verkligen kommer att användas (om vi börjar i andra änden, med att fundera vilka funktioner/metoder som kunde vara användbara, så finns risken att vi skriver någon i onödan).
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.
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:
Klassen Point
är väldigt själ-lös, det enda den gör är att förvara två tal.
Klassen Segment
lägger sig i saker som den egentligen inte har med att göra, exempelvis x- och y-koordinaterna för sina punkter.
Om någon skall bestämma avståndet mellan två punkter, så borde det vara klassen Point
-- det är den som har bäst koll på punkter i största allmänhet.
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!).