EDAF60 - Lösningsförslag till konstruktionsuppgifterna på tentamen den 24 oktober, 2022

Problem 3a

Denna uppgift är en ren tillämpning av Template Method Pattern. Vi kan börja med att samla all kod som är gemensam för villkorliga hopp i en gemensam "template class" (där execute är vår "template method"):

abstract class ConditionalJump implements Instruction {

    private int target;
    private Operand left, right;

    public ConditionalJump  (int target, Operand left, Operand right) {
        this.target = target;
        this.left = left;
        this.right = right;
    }

    protected abstract boolean shouldJump(Word left, Word right);

    public void execute(Memory memory, PC pc) {
        if (shouldJump(left.getWord(memory), right.getWord(memory))) {
            pc.jumpTo(target);
        } else {
            pc.step();
        }
    }
}

Våra subklasser behöver nu bara en konstruktor, och en implementation av den metod som gör den jämförelse som hoppet skulle baseras på (shouldJump):

class JumpLess extends ConditionalJump {

    public JumpLess  (int target, Operand left, Operand right) {
        super(target, left, right);
    }

    protected boolean shouldJump(Word left, Word right) {
        return left.lessThan(right);
    }
}

class JumpGreater extends ConditionalJump {

    public JumpGreater  (int target, Operand left, Operand right) {
        super(target, left, right);
    }

    protected boolean shouldJump(Word left, Word right) {
        return right.lessThan(left);
    }
}

Utvikning: När vi har ConditionalJump så kan vi även passa på att skriva om JumpEq som fanns i uppgiftstexten (men det behövde ni inte göra), och lägga till JumpNotEq (det behövde ni inte heller göra):

class JumpEq extends ConditionalJump {

    public JumpEq  (int target, Operand left, Operand right) {
        super(target, left, right);
    }

    protected boolean shouldJump(Word left, Word right) {
        return right.equals(left);
    }
}

class JumpNotEq extends ConditionalJump {

    public JumpNotEq  (int target, Operand left, Operand right) {
        super(target, left, right);
    }

    protected boolean shouldJump(Word left, Word right) {
        return !right.equals(left);
    }
}

Problem 3b

Som vi sett i kursen kan man uttrycka Template Method Pattern enklare med hjälp av lambda-uttryck – vi kan börja med att introducera ett lambda-uttryck för att göra den typ av jämförelser som vi vill kunna göra:

interface JumpCondition {
    public boolean shouldJump(Word left, Word right);
}

Vi kan nu introducera en mall-klass, ConditionalJump, som har en JumpCondition som ett attribut:

class ConditionalJump implements Instruction {

    private int target;
    private Operand leftOp, rightOp;
    private JumpCondition condition;

    public ConditionalJump  (int target,
                             Operand leftOp,
                             Operand rightOp,
                             JumpCondition condition) {
        this.target = target;
        this.leftOp = leftOp;
        this.rightOp = rightOp;
        this.condition = condition;
    }

    public void execute(Memory memory, PC pc) {
        if (condition.shouldJump(leftOp.getWord(memory),
                                 rightOp.getWord(memory))) {
            pc.jumpTo(target);
        } else {
            pc.step();
        }
    }
}

Nu räcker det med:

class JumpLess extends ConditionalJump {

    public JumpLess  (int target, Operand leftOp, Operand rightOp) {
        super(target,
              leftOp, rightOp,
              (left, right) -> left.lessThan(right));
    }
}

class JumpGreater extends ConditionalJump {

    public JumpGreater  (int target, Operand leftOp, Operand rightOp) {
        super(target,
              leftOp, rightOp,
              (left, right) -> right.lessThan(left));
    }
}

för att implementera klasserna i uppgiften.

Utvikning: Som ovan kan vi väldigt enkelt skapa nya villkorliga hopp (det är till och med enklare nu):

class JumpEq extends ConditionalJump {

    public JumpEq  (int target, Operand leftOp, Operand rightOp) {
        super(target,
              leftOp, rightOp,
              (left, right) -> left.equals(right));
    }
}

class JumpNotEq extends ConditionalJump {

    public JumpNotEq  (int target, Operand leftOp, Operand rightOp) {
        super(target,
              leftOp, rightOp,
              (left, right) -> !left.equals(right));
    }
}

class JumpRandomly extends ConditionalJump {

    private Random rng = new Random();

    public JumpRandomly  (int target, Operand leftOp, Operand rightOp) {
        super(target,
              leftOp, rightOp,
              (left, right) -> rng.nextBoolean());
    }
}

Problem 4a

Optional och Stream ingår egentligen i de förväntade förkunskaperna till denna kurs, men vi har pratat om dem vid några tillfällen, och de blir viktigare och viktigare, så det känns angeläget att repetera dem.

I uppgiften hade vi de två gränssnitten:

interface ArticleDB {
    public Optional<Article> findArticle(String ean);  // returnerar artikel med given artikelkod
    // ... omissions ...
}

interface Article {
    public Optional<Double> price();    // returnerar priset på en artikel, om priset är känt
    // ... omissions ...
}

och vår uppgift var att implementera en metod:

double priceOfExpensiveArticles(List<String> eans,
                                ArticleDB articleDB,
                                double priceLimit) ...

som beräknar summan av priserna på de artiklar i listan som finns i vår databas, och har ett känt pris som är högre än priceLimit.

Man kan lösa uppgiften på lite olika sätt, ett sätt (som antyddes i problemtexten) är följande:

  • skapa en Stream över artikelkoderna i listan
  • för varje artikelkod:
    • leta i databasen upp artikeln med motsvarande kod, om den finns
    • hämta priset, om det finns, annars returnera 0
  • välj ut de priser som inte är lägre än lägstapriset
  • beräkna summan:
class PriceChecker {

    double priceOfExpensiveArticles(List<String> eans,
                                    ArticleDB articleDB,
                                    double priceLimit) {
        return
            eans
            .stream()
            .mapToDouble(ean ->
                         articleDB
                         .findArticle(ean)
                         .flatMap(article -> article.price())
                         .orElse(0.0))
            .filter(price -> price >= priceLimit)
            .sum();
    }
}

Lösningen ovan är det egentliga lösningsförslaget.

Utvikning: Det finns andra sätt att beräkna denna summa, en teknik som vi inte diskuterade i kursen i år (men som jag funderar på att diskutera nästa år), är att använda stream()-metoden på våra Optional-objekt – den gör om en Optional<T> till en Stream<T>: om vår Optional är tom så får vi en tom Stream, annars så får vi en Stream som bara innehåller värdet i vår Optional. Med hjälp av detta stream()-anrop på vår Optional kan vi skriva metoden ovan som (men det förväntar vi oss inte att ni skall kunna):

class PriceChecker {

    double priceOfExpensiveArticles(List<String> eans,
                                    ArticleDB articleDB,
                                    double priceLimit) {
      return
          eans
          .stream()
          .flatMap(ean ->
                   articleDB
                   .findArticle(ean)
                   .flatMap(article -> article.price())
                   .stream())
          .mapToDouble(price -> price)   // för att kunna göra sum() nedan
          .filter(price -> price >= priceLimit)
          .sum();
    }
}

Till skillnad från i lösningen ovan tar vi nu bort artiklar vi saknar information om (snarare än att sätta deras priser till 0, och se till att deras priser inte räknas in).

Problem 4b

Vi hade alltså:

class OMDTimer {
    public static long now();   // ger antalet mikrosekunder sedan 1 januari 1970 (UTC)
}

och skulle nu använda Decorator Pattern för att hålla reda på hur lång tid vår ArticleDB tar på sig att hämta artiklar – vi gör det genom att dekorera ArticleDB:

class TimedArticleDB implements ArticleDB {

    private ArticleDB articleDB;
    private long totalTime;

    public TimedArticleDB  (ArticleDB articleDB) {
        this.articleDB = articleDB;
        resetTime();
    }

    public Optional<Article> findArticle(String ean) {
        var startedOn = OMDTimer.now();
        var found = articleDB.findArticle(ean);
        totalTime += OMDTimer.now() - startedOn;
        return found;
    }

    public void resetTime() {
        totalTime = 0;
    }

    public long totalTime() {
        return totalTime;
    }
}

Med hjälp av vår dekorerade artikeldatabas kan vi nu ta reda på den eftersökta informationen:

void checkDBLatency(PriceChecker pc,
                    List<String> eans,
                    ArticleDB articleDB) {
    var timedDB = new TimedArticleDB(articleDB);
    var totalPrice = pc.priceOfExpensiveArticles(eans, timedDB, 100);
    System.out.printf("Total price: %.2f\n", totalPrice);
    System.out.printf("Total time: %d\n", timedDB.totalTime());
}

Denna teknik har vi använt vid ett par tillfällen under kursen, bland annat i uppgift 5 på seminarium 3, och i uppgift 5 på tentan i oktober 2018 (som vi gick igenom på tavlan i läsvecka 8), när vi dekorerade en slumptalsgenerator för att se vilka slumpvärden vi fick när vi gjorde en simulering.