patterns

Design Patterns

1. Wat zijn Design Patterns

Met het uitbrengen van het book Design Patterns: Elements of Reusable Object-Oriented Software werd in het object geörienteerd programmeren een nieuwe manier van programmeren ingebracht. De term zelf, Design Patterns, bestond al sinds 1977 in de architecture. Een Design Pattern is een algemene oplossing voor een specifiek probleem.  Een voorbeeld hiervan is maken van een tafel. Het pattern van een tafel is als volgt:

  • Er is een blad om iets op te zetten. De afmetingen van het blad is heel verschillend. Een tafel kan zowel 7 meter als 70 cm lang zijn.
  • Er zijn poten om het blad op een bepaalde hoogte te houden. Meestal bestaat een tafel uit vier poten maar meer of minder poten kan ook.

Een voorbeeld van een bijzonder pattern wordt hieronder gegeven. In onderstaande afbeelding zijn de kleinere poten geïntegreerd in het tafelblad waarme de tafel kleiner kan worden gemaakt.

https://images.computational.nl/galleries/patterns/tafel.gif

Alhoewel bovenstaande tafel afwijkt van de standaard, is het pattern nog steeds geldig namelijk een blad met meerdere poten om het blad op een bepaalde hoogte te brengen. Er bestaan ontelbaar veel tafels maar het ontwerp, het pattern, is telkens hetzelfde. Een vergelijkbaar voorbeeld is het pattern "fiets". Deze bevat twee wielen, een stuur, een aandrijving etc. Er bestaan ook van dit pattern weer ontelbaar veel soorten zoals een racefiets, een mountainbike etc. Een bijzonder pattern is bijvoorbeeld een ligfiets. Je zou ook kunnen zeggen dat het pattern bestaat uit één of meer wielen zoals het geval is bij een circusfiets of een driewieler.

Ook in de software zijn op een gegeven moment oplossingen bedacht voor vaak voorkomende problemen. Veel van deze oplossingen zijn in de loop van de tijd ontstaan door "try and error". Daarnaast liep men aan tegen problemen zoals:

  1. Er staat binnen de code veel herhalende (dezelfde) code in. Dat maakt een applicatie slechter onderhoudbaar. Bedenk dat een applicatie wel een miljoen softwareregels kan bevatten.
  2. De code is rommelig en slecht leesbaar. Het is niet duidelijk welke verantwoordelijkheden er zijn.
  3. In Java wordt code in classes geplaatst. Een Klasse bestaat uit variabelen en methoden met een bepaalde verantwoordelijkheid. Hoe meer verantwoordelijkheden een class bevat, hoe lastiger het is om de code te onderhouden. Hiervoor wordt ook wel de Engelse term Tight Coupling gebruikt omdat er vaak ook verwijzingen zijn naar andere klassen.
  4. Een andere effect van veel verantwoordelijkheden is Weak Cohesion. De methoden met dezelfde verantwoordelijkheden staan niet bij elkaar en er is weinig samenhang.

Een goed ontwerp bevat precies het tegenovergestelde van bovenstaande punten dus Loose Coupling en Strong Cohesion. In de verdere lessen zal dit punt nader worden uitgewerkt en komen we er op terug.

2. Abstract klassen en overervering

In de methode Greenfoot hebben we kennis gemaakt met het programmeren van de Roodkapje game. Hierin gebruikten we het principe overerving zoals in onderstaande afbeelding.

https://images.computational.nl/galleries/patterns/2019-11-24_15-42-44.png

Je kunt zeggen dat Roodkapje (LittleRedCap) een Baseclass is. Je kunt ook zeggen dat ze een Actor is, evenals House en Flower. We noemen dit een IS-A relatie. Wie in zijn ontwerp alleen maar IS-A relaties tussen classes heeft, heeft waarschijnlijk een slecht ontwerp. We zullen dit demonstreren met een voorbeeld.

Stel dat Roodkapje in het bos loopt. In het bos zijn gevaarlijke dieren zoals de beer, de wolf en de slang. De uil is er ook maar die is niet gevaarlijk. De dieren kunnen zichzelf in de game bekend maken met de methode reveal(). We willen dat elk dier dit kan doen.  De methode is dus verplicht. Dit kunnen we bereiken met behulp van een abstracte klasse.

Een abstracte klasse is een klasse die ongedefinieerde en gedefinieerde methoden kan bevatten. In een ongedefineerde methode staat geen code (zie bijvoorbeeld de code van de klas Animal hieronder). Ongedefinieerde methoden worden geërfd in een onderliggende klasse en daar gedefinieerd. Gedefenieerde methoden kunnen gewoon worden gebruikt in onderliggende klassen door het principe van overerving. Het is niet mogelijk om een object te maken van abstracte klassen.

Hieronder het schema van dieren in het bos. Dit schema is gemaakt met behulp van UML. UML is ontworpen  om objectgeoriënteerde ontwerpen te kunnen weergeven. Er kunnen verschillende soorten diagrammen mee worden gemaakt. In de volgende les leer je meer over UML.

De schuin gedrukte methoden in de klasse Animal zijn niet gedefinieerd en worden dus gedefinieerd in onderliggende klassen.

https://images.computational.nl/galleries/patterns/2019-11-25_12-52-20.png

De code van Animal, met een niet gedefinieerde methode, is:

public abstract class Animal {

    abstract void reveal();

}

Nu zijn de overige klassen verplicht om de methode te definiëren zoals bijvoorbeeld in de klasse Bear:

public class Bear extends Animal {

    @Override
    public void reveal() {
        System.out.println("I am a bear and I can eat you!");
    }

}

Je ziet ook de volgende code: @Override. Dat is een zogenaamde Annotation. Een Annotation is metadata over een bepaalde klasse of methode. In dit geval betekent het dat de methode, en alleen maar die methode dus een typefout is hierbij niet mogelijk, wordt overschreven.

Een gedefinieerde methode

We kunnen aan de klasse Animal nu eenvoudig een methode toevoegen. De onderliggende klassen kunnen deze nu gebruiken.

package designpatterns;

public abstract class Animal {

    abstract void reveal();

    public void swim() {
        String className = this.getClass().getSimpleName().toLowerCase();
        System.out.println("This "  + className+  " is swimming.");
    }
}

Het uiteindelijke schema is dan:

https://images.computational.nl/galleries/patterns/2019-11-27_20-31-50.png

3. PlantUML

De in les 2 vertoonde schema's worden gemaak met PlantUML. Het is een tool om UML diagrammen te maken. Deze "taal" is een standaard om allerlei diagrammen te maken die in software worden gebruikt. PlantUML kun je installeren in Netbeans. We zullen laten zien hoe.

Ga naar http://plugins.netbeans.org/plugin/49069/plantuml en download de plugin van Netbeans 8.2. Het is een zogenaamde .nbm file.

https://images.computational.nl/galleries/patterns/2019-11-29_15-12-47.png

Ga nu naar Netbeans (11.2) en open de plugins tab.

https://images.computational.nl/galleries/patterns/2019-11-29_15-13-39.png

In de tab Downloaded kun je de .nbm file openen en installeren.

https://images.computational.nl/galleries/patterns/2019-11-29_15-14-12.png

Hierna kun je de plugin installeren. Klik op installeren en volg verder de instructies.

Om plantUML werkend te krijgen dien je ook nog de zogenaamde visualization software te installeren. Hiermee worden de diagrammen getekend. PlantUML heeft Graphviz nodig die je hier kunt downloaden. Kies voor een stable release en kies daarna voor de msi file. Installeer de file.

Maak nu in je Netbeansproject een nieuwe PlantUML file. Noem de file Diagram(.puml). Hierin plaats je de code van het schema uit les 2.

@startuml
 abstract class Animal { 
  {abstract} reveal() 
  swim()
}
class Bear {
 reveal()
}
class Wolf {
 reveal() 
}
class Snake {
 reveal()
}
class Owl{
 reveal()
}

Animal <|--  Bear
Animal <|--  Wolf
Animal <|--  Snake
Animal <|--  Owl
@enduml

De uitleg over PlantUML vind je in de PlantUML guide.

4. Het probleem van het ontwerp tot nu toe

Misschien was je in de veronderstelling dat je nu in staat bent om goede software te maken. Helaas. Met het klassendiagram zoals tot nu toe is ontworpen is een probleem. Kan een uil zwemmen? Misschien, maar we denken dat de uil hier niet vrijwillig voor kiest. Om dit probleem op te lossen moeten we de methode swim() overschrijven.

package designpatterns;

public class Owl extends Animal {

    public Owl() {
    }

    @Override
    public void reveal() {
        System.out.println("I am an owl. You don't have to be afraid of me.");
    }
    
    @Override
    public void swim() {
        String className = this.getClass().getSimpleName().toLowerCase();
        System.out.println("This " + className + " cannot swim or doesn't like it.");
    }
}

In plantUML zien we nu het volgende schema (wijzig dit zelf in Netbeans en bekijk de output).

@startuml
 abstract class Animal { 
  {abstract} reveal() 
  swim () 
}
class Bear {
 reveal()
}
class Wolf {
 reveal() 
}
class Snake {
 reveal()
}
class Owl{
 reveal()
 swim()
}

Animal <|--  Bear
Animal <|--  Wolf
Animal <|--  Snake
Animal <|--  Owl
@enduml

We hebben dus nu op twee plekken een methode swim(). We willen nu ook een methode run() toevoegen. Echter, een slang en een uil kunnen niet rennen. We krijgen daarom het volgende diagram:

@startuml
 abstract class Animal { 
  {abstract} reveal() 
  swim ()
  run ()
}
class Bear {
 reveal()
}
class Wolf {
 reveal() 
}
class Snake {
 reveal()
 run()
}
class Owl{
 reveal()
 swim()
 run()
}

Animal <|--  Bear
Animal <|--  Wolf
Animal <|--  Snake
Animal <|--  Owl
@enduml

https://images.computational.nl/galleries/patterns/2019-11-29_16-10-26.png

Nu moeten we de methode run() op drie plekken bijhouden! Deze code wordt ingewikkeld! We moeten een andere manier verzinnen om dit op te lossen. In de volgende les zul je leren wat een interface is.

5. Het gebruik van een interface

We hebben in de vorige les al gezien wat een abstracte klasse was. Een interface is daar een speciale vorm van. Net als een abstracte klasse kun je van een interface ook geen object maken maar is het de bedoeling dat je deze in een klasse implementeert. Je mag een interface niet initiëren. Dit geeft een foutmelding. Daarom heeft een interface geen constructor of klassevariabelen (er is wel een uitzondering maar dat laten we nu voor wat het is).

Verder zijn we ook in de vorige les tegen een probleem aangelopen: Op teveel plekken moeten wij code gaan invoeren voor methoden met dezelfde naam. De denkfout die we daarmee eigenlijk maken is dat we uitgaan van de eigenschappen van een individueel dier en die telkens aanpassen. Dit heeft ingewikkelde en slecht leesbare code tot gevolg. We kunnen dit ook wel definieeren als een lage cohesie en een hoge koppeling. Met lage cohesie wordt bedoeld dat er teveel verantwoordelijkheden door elkaar lopen en met hoge koppeling wordt bedoeld dat teveel klassen van elkaar afhankelijk zijn. Het moet precies andersom zijn namelijk hoge cohesie en lage koppeling. Concreet: we gaan de verantwoordelijkhed voor een bepaalde taak  delegeren naar een aparte klasse. We gaan de eigenschap los van het dier definiëren en programmeren. Dat is precies waar een interface heel geschikt voor is. Dit principe heet programmeren naar een interface.

We liepen tegen het probleem aan dat een uil niet kan zwemmen maar de rest van de dieren wel. Deze eigenschap kunnen we met behulp van een interface apart gaan definieren. We beginnen met het definiëren van een interface voor deze eigenschap. De code hiervan is als volgt:

package designpatterns;

public interface SwimAction {

    public void swim();

}

Merk op dat we voor een class altijd een zelfstandig naamwoord gebruiken en niet een werkwoord.

We maken ook een interface RunAction op dezelfde manier. Deze klasse is ook nodig omdat we al eerder hebben gezien dat een slang en een uil niet kunnen rennen.

package designpatterns;

public interface RunAction {

    public void run();

}

We veranderen nu de class Animal als volgt:

package designpatterns;

public abstract class Animal {
    
    SwimAction swimAction;
    RunAction runAction;

    abstract void reveal();

    public void swim() {
        this.swimAction.swim();
    }
    
    public void run() {
       this.runAction.run();
    }
}

Je ziet dat we beide interfaces hebben toegevoegd aan de class Animal. Deze hebben echter nog geen waarde. Dit kunnen we zien als we Demo class draaien. We krijgen dan een nullpointer. Zie hieronder de uitvoer van Demo.java:

I am an owl. You don't have to be afraid of me.
Exception in thread "main" java.lang.NullPointerException

Dit kunnen we oplossen door beide interfaces te implementeren in een gewone Java class en deze dan toevoegen aan Animal. Het schema hiervan is als volgt:

https://images.computational.nl/galleries/patterns/2019-12-01_19-58-31.png

De twee interfaces maken dat alle onderliggende klassen een methode run() of swim() verplicht moeten definiëren. De code van Runner is dan als volgt:

package designpatterns;

public interface RunAction {

    public void run();

}

De klassen Runner en NoRunner zijn dan als volgt:

package designpatterns;

public class Runner implements RunAction {

    @Override
    public void run() {
        System.out.println("I am running.");
    }
}
package designpatterns;

public class NoRunner implements RunAction {

    @Override
    public void run() {
        System.out.println("I can't run.");
    }
}

We zullen de klassen nu gaan gebruiken. We beginnen bij Bear. Deze kan rennen. We stellen dit in, in de constructor door de runAction variabele een waarde te geven met de klasse Runner. De code wordt dan:

package designpatterns;

public class Bear extends Animal {

    public Bear() {
        super.runAction = new Runner();
    }

    @Override
    void reveal() {
        System.out.println("I am a bear and I can eat you!");
    }
}

Omdat de variabele runAction, die is gemaakt vanuit de interface RunAction (met een hoofdletter) wordt geïnitieerd met een concrete klasse kunnen we nu ook de code daarvan gebruiken. In Demo.java gaat dit zo:

package designpatterns;

public class Demo {

    public static void main(String[] args) {
        Animal bear = new Bear();
        bear.reveal();
        bear.run();
      
    }
}

En de output is dan:

I am a bear and I can eat you!
I am running

6. De klasse Animal uitbreiden met getters en setters

We kunnen de klasse Animal ook nog uitbreiden met setters zodat we het gedrag on the fly (tijdens het gebruik van de code) kunnen veranderen. Bijvoorbeeld als volgt:

package designpatterns;

public class Demo {

    public static void main(String[] args) {
        Animal owl = new Owl();
        owl.reveal();
        owl.run();
        owl.swim();
        owl.setRunAction(new Runner());
        owl.run();
    }
}

We kunnen de methode setRunAction gebruiken doordat deze is toegevoegd aan de klasse Animal.

package designpatterns;

public abstract class Animal {

    SwimAction swimAction;
    RunAction runAction;

    abstract void reveal();

    public void swim() {
        this.swimAction.swim();
    }

    public void run() {
        this.runAction.run();
    }

    public void setSwimAction(SwimAction swimAction) {
        this.swimAction = swimAction;
    }

    public void setRunAction(RunAction runAction) {
        this.runAction = runAction;
    }
}

De uitvoer in Demo.java is dan als volgt:

I am an owl. You don't have to be afraid of me.
I can't run.
I can't swim.
I am running.

7. Relaties

We hebben het al gehad over een IS-A en een HAS-A relatie. Een HAS-A relatie kun je verdelen in twee soorten:

  • een compositie (composition)
  • een agregatie (aggregation)

Het verschil zit hem in het feit of een object er afhankelijk van is of niet. Een cirkel of een polygoon kan worden opgebouwd met behulp van puntjes op een scherm. Je kunt zeggen dat de cirkel of de polygoon niet bestaat zonder deze puntjes. In dat geval is er sprake van een compositie.

We kunnen de cirkel en de polygoon ook opmaken door ze bijvoorbeeld in te kleuren. We kunnen dit doen met een klasse Style. Omdat cirkel en polygoon ook zonder deze klasse kunnen bestaan is er sprake van een agregatie.

Een agregatie is dus een minder sterke relatie.

Schematisch geven we dat als volgt weer:

https://images.computational.nl/galleries/patterns/2019-12-04_20-21-16.png

Je ziet het verschil tussen de open en de opgevulde diamant. Een aggregatie heeft een open diamant. Een compositie een gevulde diamant.

Dan is er nog de zwakste relatie: de afhankelijkheid (dependency). Deze geven we als volgt aan:

https://images.computational.nl/galleries/patterns/2019-12-07_19-44-15.png

De klasse Style bevat de methode fill en deze gebruikt de klasse Color (usage). Dit geven we aan met een stippellijn vanuit de klasse die de afhankelijk is.

We kunnen nu een volgorde maken van sterkste naar zwakste relatie met de Engelse termen:

  1. Generalisation: Dit is een IS-A relatie.
  2. Composition, het object kan anders niet bestaan. Het is een HAS_A relatie.
  3. Aggragetion, het object is er niet persé van afhankelijk en kan zonder ook bestaan. Het is een HAS_A relatie.
  4. Dependency, dit is de zwakste relatie.

8. Het Bridge pattern

Wat je nu hebt geleerd heet het Bridge pattern. Dit heet zo omdat het schema op een soort brug lijkt. SuperClass en AlgorithmInterface vormen de brug met concrete klassen als pijlers van de brug:

https://images.computational.nl/galleries/patterns/2019-12-09_20-37-49.png

Wat is nu eigenlijk het doel van dit pattern. De bedoeling van het bridge patroon is om de abstractie en de implementatie los te koppelen van elkaar zodanig dat de twee onafhankelijk van elkaar kunnen variëren. Dus links heb ik als abstracte klasse Animal (de Superclass) en als concrete klassen kunnen we daar dieren van maken zoals een uil, een beer enz. De abstractie is dan dus Animal. De implementatie is dat een dier ook al of niet kan zwemmen met bijvoorbeeld de methode swim() die we definiëren in de interface en concreet  maken in de onderliggende klasse. We kunnen nu dus de abstractie met de implementatie combineren en op deze wijze hoeven we bijvoorbeeld voor een dier wat kan zwemmen maar één keer deze methode te creëren. In de onderstaande lessen wordt dit verder uitgelegd en gerealiseerd.

9. LittleRedCap level 1 opnieuw ontwerpen

Met bovenstaande lessen kunnen we nu een poging wagen om de game zoals is uitgelegd in de Greenfootlessen opnieuw te ontwerpen. De uitleg kenmerkt zich door vrijwel alleen maar IS-A relaties. Op zich is dat goed. Je leerst eerst de basisprincipes van object georienteerd programmeren zoals overerving, parameters etc. Het is dan ook niet verwonderlijk dat een ingeleverde game een ontwerp heeft zoals onderstaande afbeelding.

https://images.computational.nl/galleries/patterns/2019-12-23_09-34-40.png

De game is voor een beginnende programmeur zeer goed gemaakt (cijfer 9,5) maar toch kan het ontwerp een stuk eenvoudiger. Dit kunnen we bereiken met het design patterns zoals Bridge. Laten we starten met het ontwerp van de interfacekant. Hierin komen worden de verschillende gedragingen ontworpen zoals de manier waarop een speler (actor) beweegt of waarop een speler omgaat met een andere actor zoals het opeten van Roodkapje door de wolf of het plukken van bloempjes door Roodkapje.

Concreet komen we tot een eerste ontwerp:

@startuml

interface MoveInterface {
  +move()
}

class MoveRandom {
  +void move()
  -boolean atWorldEdge()
  -void turnAtEdge()
  -void walkRandom()
  -int getRandomNumber(int min, int max)
}

class MoveBackForth {
  +void move(Actor actor)
  -boolean atWorldEdge()
  -void turnAtEdge()
}

MoveInterface <|.. MoveBackForth
MoveInterface <|.. MoveRandom

@enduml

De klasse MoveRandom is voor de random bewegende wolf. De klasse MoveBackForth is voor de wolf die de wacht houdt door heen en weer te lopen voor het huisje van oma.

Een opmerking over de naamgeving. In principe geef je de klasse zoveel mogelijk een zelfstandig naamwoord. Omdat we echter beginnen met het woord Move<en dan de naam van de soort beweging>, geeft dit een beter overzicht in Greenfoot omdat dan de klassen bij elkaar worden geplaatst. Een betere naam zou echter zijn RandomMover o.i.d.

Hier zitten we nu met een probleem. Neem de methode atWorldEdge(). De code daarvan is als volgt:

private boolean atWorldEdge() {
    if(getX() < 20 || getX() > getWorld().getWidth() - 20)
      return true;
    if(getY() < 20 || getY() > getWorld().getHeight() - 20)
      return true;
    else
     return false;
}

Hierin gebruiken we de methode getX() en getWorld(). Als in de Greenfoot API kijken naar de klasse Actor dan is het toegestaan deze methode buiten de klasse te gebruiken omdat deze public is (ga dit na!). Maar hoe krijgen we deze methode nu binnen een concrete klasse zoals MoveRandom? Dit kan maar op een manier namelijk door de klasse Actor als parameter binnen de methode move() te gebruiken. Het ontwerp wijzigen we nu als volgt:

@startuml


abstract class Actor {
  +move(int speed)
  +int getX()
  +int getY()
  +World getWorld()
}

interface MoveInterface {
  move(Actor actor);
}

class MoveRandom {
  -Actor actor;
  +void move();
  -boolean atWorldEdge()
  -void turnAtEdge()
  -void walkRandom()
  -int getRandomNumber(int min, int max)
}

class MoveBackForth {
  -Actor actor;
  #boolean atWorldEdge()
  -void turnAtEdge()
}


Actor  <.. MoveInterface
MoveInterface <|... MoveBackForth
MoveInterface <|... MoveRandom

@enduml

We maken een afhankelijkheidsrelatie tussen de klassen MoveInterface en Actor door deze als parameter in de methode move te gebruiken. In de concrete klassen maken we ook een variabele actor en vullen deze met de parameter actor (Actor actor = actor). Hierna zijn we in staat om de methoden getX() en getY() te gebruiken. We gebruiken ook de klasse Random in de methode getRandomNumber() door deze te importeren. Voor het overzicht nemen we deze niet op in het klassendiagram.

Aan de kant van de superklasse creëren we een klasse Being met de concrete klassen Wolf en BackForthWolf. Beide wolven gedragen zich anders en dat is te zien aan hun loopgedrag. Ook aan de interfacekant sleutelen we nog wat. Uiteindelijk komen we tot het volgend ontwerp.

@startuml


abstract class Actor {
  +move(int speed)
  +int getX()
  +int getY()
  +World getWorld()
}
abstract class Being {
  Move moveInterface
  void act() 
}
class Wolf {
  +Wolf()
}

class BackForthWolf {
 +BackFortWolf() 
}

interface MoveInterface {
  move(Actor actor);
}

class MoveRandom {
  -Actor actor;
  -final int RANDOMWALK=10;
  -final int MOVESPEED=5;
  +void move();
  -boolean atWorldEdge()
  -void turnAtEdge()
  -void walkRandom()
  -int getRandomNumber(int min, int max)
}

class MoveBackForth {
  -Actor actor;
  #boolean atWorldEdge()
  -void turnAtEdge()
}


Actor <|--  Being
Actor  <.. MoveInterface
Being <|---  Wolf
Being <|---  BackForthWolf
MoveInterface <|... MoveBackForth
MoveInterface <|... MoveRandom
Being *- MoveInterface

@enduml

10. LittleRedCap level 1 - de concrete code

Hieronder de concrete code van de game. Hierover nog twee opmerkingen:

  1. In de concrete klassen MoveBackFort en MoveRandom is de variabele Actor actor gemaakt. Deze wordt gevuld met de parameter actor (Actor actor = actor) in de methode move. Deze methode functioneert tevens als een soort constructor.
  2. De klassen BackFortWolf en Wolf kennen weinig code. Ze maken gebruik van de methode act die in Being gedefinieerd is.

UML ontwerp

@startuml


abstract class Actor {
  +move(int speed)
  +int getX()
  +int getY()
}
abstract class Being {
  Move moveInterface
  void act() 
}
class Wolf {
  +Wolf()
}

class BackForthWolf {
 +BackFortWolf() 
}

interface MoveInterface {
  move(Actor actor);
}

class MoveRandom {
  -Actor actor;
  -final int RANDOMWALK=10;
  -final int MOVESPEED=5;
  +void move();
  -boolean atWorldEdge()
  -void turnAtEdge()
  -void walkRandom()
  -int getRandomNumber(int min, int max)
}

class MoveBackForth {
  -Actor actor;
  #boolean atWorldEdge()
  -void turnAtEdge()
}


Actor <|--  Being
Actor  <.. MoveInterface
Being <|---  Wolf
Being <|---  BackForthWolf
MoveInterface <|... MoveBackForth
MoveInterface <|... MoveRandom
Being *- MoveInterface

@enduml

class Being

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)

public abstract class Being extends Actor
{
    protected MoveInterface moveInterface;

    public void act() 
    {       
        moveInterface.move(this);
    } 
}

class BackFortWolf

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)

public class BackFortWolf extends Being
{
    public BackFortWolf() {
        super.moveInterface = new MoveBackFort();
    }
}

class Wolf

import greenfoot.*;  

public class Wolf extends Being
{
    public Wolf() {
        super.moveInterface = new MoveRandom();
    }
}

import greenfoot.*; 

public interface MoveInterface
{
    public void move(Actor actor);
}

class MoveBackFort

import greenfoot.*; 

public class MoveBackFort implements MoveInterface
{
    private Actor actor;

    public void move(Actor actor){
        this.actor=actor;
        actor.move(3);
        atWorldEdge();
        turnAtEdge();
    }

    private boolean atWorldEdge()
    {
        if(actor.getX() < 20 || actor.getX() > actor.getWorld().getWidth() - 20)
            return true;
        if(actor.getY() < 20 || actor.getY() > actor.getWorld().getHeight() - 20)
            return true;
        else
            return false;
    }

    private void turnAtEdge()
    {
        if(atWorldEdge())
        {
            //eastside
            if(actor.getX()>570)
            {
                //resetting image
                actor.setImage("wolfbackandforth.gif");
                actor.setRotation(180);
                actor.getImage().mirrorVertically();
            }
            //westside
            if(actor.getX()<30)
            {
                //resetting image
                actor.setImage("wolfbackandforth.gif");
                actor.setRotation(360);
            }
        }
    }
}

class MoveRandom

import greenfoot.*;
import java.util.Random; 

public class MoveRandom implements MoveInterface
{
    private Actor actor;
    private final int RANDOMWALK=10;
    private final int MOVESPEED=5;

    public void move(Actor actor){
        this.actor=actor;
        actor.move(MOVESPEED);
        turnAtEdge();
        walkRandom();
    }

    private void turnAtEdge()
    {
        int turn=0;
        if(atWorldEdge())
        {
            //east
            if(actor.getX()>570)
            {
                turn=getRandomNumber(150-RANDOMWALK,210+RANDOMWALK);
            }
            //south
            if(actor.getY()>570)
            {
                turn=getRandomNumber(240-RANDOMWALK,300+RANDOMWALK);
            } 
            //weast
            if(actor.getY()<30)
            {
                turn=getRandomNumber(60-RANDOMWALK,120+RANDOMWALK);
            }
            //north
            if(actor.getX()<30)
            {
                
                turn=getRandomNumber(330-RANDOMWALK,390+RANDOMWALK);
            }

            actor.setRotation(turn);
        }
    }

    /**
     * causes a random walk
     */
    private void walkRandom()
    {
        int randomNumber = getRandomNumber(-RANDOMWALK, RANDOMWALK+1);
        actor.setRotation (actor.getRotation()+randomNumber);
    }

    public boolean atWorldEdge()
    {
        if(actor.getX() < 20 || actor.getX() > actor.getWorld().getWidth() - 20)
            return true;
        if(actor.getY() < 20 || actor.getY() > actor.getWorld().getHeight() - 20)
            return true;
        else
            return false;
    } 

    public int getRandomNumber(int min, int max)
    {
        Random random= new Random();
        int n= random.nextInt(max-min) + min;
        return n;
    }
}

11. Het Adapter pattern

Het volgende pattern wat we gaan behandelen is het Adapter pattern. Dit werkt als volgt:

Stel we zijn in het Verenigd Koninkrijk en willen graag gebruik maken van het 220 volt netwerk. Een stekker in Nederland ziet er meestal zo uit:

https://images.computational.nl/galleries/patterns/2019-12-30_09-42-19.png

Een stekker in het Verenigd Koninkrijk heeft echter deze vorm:

https://images.computational.nl/galleries/patterns/2019-12-30_09-44-06.png

Dit past niet op elkaar. Om toch gebruik te kunnen maken van dit netwerk zullen we een adapter moeten gebruiken. Als volgt:

https://images.computational.nl/galleries/patterns/2019-12-30_09-47-58.png

De adapter zorgt ervoor dat we een Nederlandse stekker in het netwerk kunnen gebruiken. Dit systeem gaan we ook in onze game gebruiken. Het heet het Adapter pattern.

Voeg de volgende klassen toe aan je game:

interface KeyInterface

import greenfoot.*; 

public interface KeyInterface  
{
    public void keys(Actor actor);
}

class KeyArrows

import greenfoot.*; 

public class KeyArrows implements KeyInterface {
    private Actor actor;
    private final int SPEED=5;

    public void keys(Actor actor)
    {
        this.actor=actor;
        if (Greenfoot.isKeyDown("left"))
        {
            actor.setLocation(actor.getX() -SPEED, actor.getY());
            actor.setImage("redcapleft.gif");
        }

        if (Greenfoot.isKeyDown("down"))
        {
            actor.setLocation(actor.getX(), actor.getY() + SPEED);
            actor.setImage("redcapfront.gif");

        }

        if (Greenfoot.isKeyDown("up"))
        {
            actor.setLocation(actor.getX(), actor.getY() - SPEED);
            actor.setImage("redcapbehind.gif");

        }

        if (Greenfoot.isKeyDown("right"))
        {
            actor.setLocation(actor.getX()+SPEED, actor.getY());
            actor.setImage("redcapright.gif");

        }

    }
}

Je ziet dat deze klasse de code bevat om Roodkapje met toetsen te kunnen besturen. We zitten echter met een interface die we niet direct kunnen gebruiken. Als we het wel proberen krijgen we direct een NullpointerException. Hieronder de poging:

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)

public abstract class Being extends Actor
{
    protected MoveInterface moveInterface;
    protected FindInterface findInterface;
    protected KeyInterface keyInterface;

    public void act() 
    {       
        moveInterface.move(this);
        findInterface.find(this);
        keyInterface.keys(this);
    }
}
java.lang.NullPointerException
	at Being.act(Being.java:13)
	at greenfoot.core.Simulation.actActor(Simulation.java:567)
	at greenfoot.core.Simulation.runOneLoop(Simulation.java:530)
	at greenfoot.core.Simulation.runContent(Simulation.java:193)
	at greenfoot.core.Simulation.run(Simulation.java:183)

Dit gaat dus niet. Voor dit probleem is het Adapter pattern ontwikkeld.

In plaats van de keyInterface gebruiken maken we een adapter die we MoveWithArrowsAdapter hebben genoemd:

import greenfoot.*;

public class MoveWithArrowsAdapter implements MoveInterface {
    private Actor actor;

    public void move(Actor actor){
        this.actor=actor;
        KeyArrows keyArrows = new KeyArrows();
        keyArrows.keys(actor);
    }
}

Je ziet dat er in de methode move, die verplicht is vanwege de MoveInterface, een object keyArrows wordt gemaakt. Vervolgens wordt met behulp van de methode keys(actor)de code aangeroepen. Schematisch ziet dit er als volgt uit:

https://images.computational.nl/galleries/patterns/2019-12-30_11-00-25.png

De MoveWithArrowsAdapter noemen we, zoals de naam ook al zegt, de adapter. De KeyInterface is in ons geval de geadapteerde. De Engelse term hiervoor is de Adaptee.

12. Het Strategy pattern

Als we nu alleen Roodkapje hadden gehad die bewoog in de game dan hadden we ook het volgende schema kunnen hanteren:

@startuml


abstract class Actor {
  +move(int speed)
  +int getX()
  +int getY()
}
class Wolf {
  +Wolf()
}

class BackForthWolf {
 +BackFortWolf() 
}

class LittleRedCap {
  -Move moveInterface
  +LittleRedCap()
  #void act() 
}

interface MoveInterface {
  +move(Actor actor);
}


class MoveRandom {
  -Actor actor;
  -final int RANDOMWALK=10;
  -final int MOVESPEED=5;
  +void move();
  -boolean atWorldEdge()
  -void turnAtEdge()
  -void walkRandom()
  -int getRandomNumber(int min, int max)
}

class MoveBackForth {
  -Actor actor;
  #boolean atWorldEdge()
  -void turnAtEdge()
}

interface FindInterface {
  +void find(Actor actor)
}

class FindFlower {
  -Actor actor
  +void find(Actor actor)
  -void pickFlowers()
}

class FindLittleRedCap {
  -Actor actor
  +void find(Actor actor)
}

class FindNothing {
  -Actor actor
  +void find(Actor actor)
}

class MoveWithArrowsAdapter {
  Actor actor
  +void move(Actor actor)
}

interface KeyInterface {
  +void keys(Actor actor)
}

class KeyArrows {
  -Actor actor
  -final int SPEED
  +void keys(Actor actor)
}

Actor <|--  LittleRedCap
LittleRedCap *- FindInterface
LittleRedCap *- MoveInterface
Actor  <.. MoveInterface
Actor  <.. FindInterface
MoveInterface <|... MoveBackForth
MoveInterface <|... MoveRandom
MoveInterface <|.... MoveWithArrowsAdapter
FindInterface <|... FindFlower 
FindInterface <|... FindLittleRedCap 
FindInterface <|... FindNothing
KeyInterface <|... KeyArrows
MoveWithArrowsAdapter *- KeyInterface

@enduml

Het verschil is dat we de klasse Being eruit hebben gehaald. Dit heet het Strategy pattern. Het brengt structuur in je code. Kijk bijvoorbeeld naar de code van Roodkapje als we het Strategy pattern gebruiken:

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)


public class LittleRedCap extends Actor
{
    protected MoveInterface moveInterface;
    protected FindInterface findInterface;
    
    public LittleRedCap() {
        moveInterface = new MoveWithArrowsAdapter();
        findInterface = new FindFlower();
    }
    
    public void act() 
    {       
        moveInterface.move(this);
        findInterface.find(this);
    }
}

Je ziet nu dat we de het initieren van een concrete klasse via een interface binnen Roodkapje zullen moeten doen. Verder zie je dat er meer code in de klasse staat ten opzichte van het gebruiken van het Bridge pattern.

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)

public class LittleRedCap extends Being
{
    public LittleRedCap() {
        super.moveInterface = new MoveWithArrowsAdapter();
        super.findInterface = new FindFlower();
    }
}

Het Strategy pattern is dus heel geschikt om een familie van code te maken zoals we hebben gedaan met move() en find(). Je ziet nu ook dat we een lage koppeling hebben gecreëerd en een hoge cohesie - alle verantwoordelijkheid (move en find) staat nu bij elkaar.