EDAF60: tentamen 2024-10-28 – lösningsförslag

Problemtexten finns här (uppdaterad med några förtydliganden baserade på de frågor jag fick under tentan).

Problem 1

Vi har alltså gränssnitten:

interface Drawing {
    void setDrawMode();
    void setEraseMode();
    void moveTo(int x, int y);
    void circle(int radius);
    void rectangle(int width, int height);
}

och

interface DrawCommand {
    void draw(Drawing drawing);
    void undo();
}

(a)

Klasserna DrawCircle och DrawSquare kommer att ha en hel del gemensamt, så vi kan använda Template Method för att undvika kod-duplicering (det är inte nödvändigt att använda Template Method, men för full poäng på uppgiften måste man göra det):

abstract class DrawShape implements DrawCommand {

    private int x, y;
    private Drawing drawing;

    public DrawShape  (int x, int y) {
        this.x = x;
        this.y = y;
    }

    protected abstract void drawShape(Drawing drawing);

    public void draw(Drawing drawing) {
        this.drawing = drawing;
        drawing.setDrawMode();
        drawing.moveTo(x, y);
        drawShape(drawing);
    }

    public void undo() {
        drawing.setEraseMode();
        drawing.moveTo(x, y);
        drawShape(drawing);
    }
}

Vi skulle även kunna ha en hjälpmetod moveToAndDraw som tar bort kod-dupliceringen i draw och undo:

abstract class DrawShape implements DrawCommand {

    private int x, y;
    private Drawing drawing;

    public DrawShape  (int x, int y) {
        this.x = x;
        this.y = y;
    }

    protected abstract void drawShape(Drawing drawing);

    private void moveToAndDraw() {
        drawing.moveTo(x, y);
        drawShape(drawing);
    }

    public void draw(Drawing drawing) {
        this.drawing = drawing;
        drawing.setDrawMode();
        moveToAndDraw();
    }

    public void undo() {
        drawing.setEraseMode();
        moveToAndDraw();
    }
}

Om vi väljer att private eller protected-deklarera attributen spelar inte någon roll i uppgiften, men om vi protected-deklarerar drawing-attributet så behöver vi inte ha den som parameter till den abstrakta metoden (som jag har ovan).

Med klassen DrawShape till hjälp kan vi nu skriva klasserna DrawCircle och DrawSquare som:

class DrawCircle extends DrawShape {

    private int radius;

    public DrawCircle  (int x, int y, int radius) {
        super(x, y);
        this.radius = radius;
    }

    protected void drawShape(Drawing drawing) {
        drawing.circle(radius);
    }
}

class DrawSquare extends DrawShape {

    private int side;

    public DrawSquare  (int x, int y, int side) {
        super(x, y);
        this.side = side;
    }

    protected void drawShape(Drawing drawing) {
        drawing.rectangle(side, side);
    }
}

(b)

problem-1b.svg

Problem 2

Nu har vi gränssnitten

interface Action {
    void execute(Drawing drawing, Stack<DrawCommand> history);
}

och

interface GUI {
    Action next();
}

(a)

För att utföra ett rit-kommando behöver vi spara undan kommandot i ett attribut, och sedan utföra det i rätt fönster vid execute – vi måste dessutom lägga det överst i historie-stacken:

class DrawAction implements Action {

    private DrawCommand command;

    public DrawAction  (DrawCommand command) {
        this.command = command;
    }

    public void execute(Drawing drawing, Stack<DrawCommand> history) {
        history.push(command);
        command.draw(drawing);
    }
}

För att ångra det senaste kommandot kan vi först se om det finns något kommando att ångra, och i så fall hämta det från stacken och göra undo() på det:

class UndoAction implements Action {

    public void execute(Drawing drawing, Stack<DrawCommand> history) {
        if (!history.isEmpty()) {
            history.pop().undo();
        }
    }
}

Vår action för att avsluta anropar bara System.exit, och avslutar därmed hela den virtuella maskinen (så vi behöver inte städa upp stacken):

class ExitAction implements Action {

    public void execute(Drawing drawing, Stack<DrawCommand> history) {
        System.exit(0);
    }
}

Om vi skriver våra actions på detta sätt så behöver vi i uppgift 1 inte bekymra oss för att någon skall anropa metoderna i våra kommandon i fel ordning, eller med fel parametrar.

(b)

För att knyta ihop detta behöver vi:

void run(GUI gui, Drawing drawing) {
    var history = new Stack<DrawCommand>();
    while (true) {
        gui.next().execute(drawing, history);
    }
}

Problem 3

(a)

Från början har vi

abstract class Word implements Operand {
    public abstract void add(Word left, Word right);
    public abstract void mul(Word left, Word right);
    // . . . diverse utelämnade metoder . . .
}

men för att utföra själva divisionen kommer vi att behöva en divisions-metod på våra ord, så vi kan börja med att lägga till en metod div i Word:

abstract class Word implements Operand {
    public abstract void div(Word left, Word right);
    // . . . diverse utelämnade metoder . . .
}

Med denna till vår hjälp kan vi implementera instruktionen Div som (ni behövde inte skriva någon toString-metod):

public class Div implements Instruction {

    private Operand lhs, rhs;
    private Address address;

    public Div  (Operand lhs, Operand rhs, Address address) {
        this.lhs = lhs;
        this.rhs = rhs;
        this.address = address;
    }

    public void execute(Memory memory, PC pc) {
        address
            .getWord(memory)
            .div(lhs.getWord(memory),
                 rhs.getWord(memory));
        pc.step();
    }
}

Vi behöver nu lägga till div-metoden i vår LongWord-klass, exempelvis så här:

package omd.computer.hardware.longmem;

import omd.computer.hardware.*;

public class LongWord extends Word {

    private long value;

    private long valueOf(Word other) {    // hjälpmetod
        return ((LongWord)other).value;
    }

    public void div(Word lhs, Word rhs) {
        value = valueOf(lhs) / valueOf(rhs);
    }
}

(b)

Vi har OCP med avseende på nya instruktioner som inte kräver något mer av våra ord – exempelvis skulle vi enkelt kunna lägga till en instruktion JumpNotEq, eller en annan instruktion Double som dubblade innehållet i en adress.

Vi har även OCP vad gäller nya ord-typer, och program – det är enkelt att skapa nya Program-klasser, utan att vi behöver ändra någon gammal programkod (vi måste dock lägga till någon/några rader i huvudprogramsklassen för att starta dem).

Däremot har vi inte OCP om vi vill göra något som kräver mer av våra ord, denna uppgift visar att vi måste lägga till ny kod i gamla gränssnitt och klasser om vi vill kunna lägga till exempelvis nya aritmetiska operationer.

Problem 4

Se kurshemsidan.