patterns

patterns deel 2

1. Indeling van de Design Patterns

We beginnen in dit deel met het geven van een overzicht hoe Design Patterns zijn ingedeeld. Er zijn drie soorten:

  1. Creatie (Creational): deze gaan we nog leren. Het zijn patterns die iets creëren zoals een familie van bepaalde klassen of er worden bepaalde zaken afgedwongen. Het Singleton pattern, bijvoorbeeld, dwingt af dat er maar één object van een klasse mag worden geïnitieerd. 
  2. Structuur (Structural): Dit pattern zijn we tot nu toe tegen gekomen in de vorm van het Bridge en het Adapter pattern. Het zijn patronen die de structuur van een programma verbeteren en een programma inzichtelijker maken. We hebben al gezien hoe gemakkelijk we nu het bewegen van de verschillende actoren met één regel code kunnen veranderen. De code daarvan staat overzichtelijk op één plaats bij elkaar.
  3. Gedrag (Behavioral): Alhoewel het UML-schema van Bridge en Strategy bijna hetzelfde zijn, is hun intentie anders. Strategy is ontworpen om het gedrag van een Actor te beïnvloeden terwijl Bridge ervoor zorgt dat meerdere actoren hier gebruik van kunnen maken. In de volgende les zullen we meer leren over het Observer pattern wat ook tot deze categorie behoort.

Schematisch geven we de belangrijkste patterns. De patterns met een gele achtergrond zijn tot nu toe behandeld.

Creational Structural Behavioral
Abstract Factory Adapter Chain of Responsibility
Builder Bridge Command
Factory Method Composite Interpreter
Prototype Decorator Iterator
Singleton Facade (via opdracht) Mediator
FlyWeight Memento
Proxy Observer
State
Strategy
Template
Visitor

2. Strong cohesion, loose coupling

De termen Strong Cohesion, Loose Coupling zijn we al eerder tegen gekomen. Wellicht gaan deze termen nu meer leven. Cohesion is de mate waarin verantwoordelijkheden bij elkaar horen. Deze wil je zo sterk mogelijk maken. Waarom? Een van de redenen is dat de code dan makkelijker te onderhouden is. Daarnaast is het dan mogelijk om een volledige module (klasse) te vervangen door een verbeterde klasse. Nog een reden is dat code die op een plek bij elkaar staat, begrijpelijker is.

Loose Coupling is een begrip wat vaak erbij wordt genoemd. Het is een teken van een goed ontworpen systeem. Coupling geeft de onafhankelijkheid van de verschillende klassen aan. We zullen dit nog eens aangeven met een voorbeeld.

Stel dat we een klasse Manager maken met personeelsleden.

public class Manager {

    Teacher teacher;
    SocialWorker socialWorker;

    public Manager(Teacher teacher, SocialWorker socialWorker) {
        this.teacher = teacher;
        this.socialWorker = socialWorker;
    }

    public void manageWork() {
        teacher.work();
        socialWorker.work();
    }
}

En we maken de klasse van de twee personeelsleden.

public class Teacher {

    void work() {
       System.out.println("A teacher is teaching children.");
    }
}
public class SocialWorker {

    void work() {
        System.out.println("A social worker is helping people.");
    }
}

En daarna de demo file om de klassen te testen.

public class Demo {

    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        SocialWorker socialWorker = new SocialWorker();
        Manager manager = new Manager(teacher, socialWorker);
        manager.manageWork();
    }
}

Tot nu toe lijkt het goed te gaan. Nu komt er echter een nieuw personeelslid bij, een brandweerman.

public class FireFighter {
 
    void work() {
       System.out.println("A firefighter is standby for a fire.");
    }
}

We moeten nu veel aanpassen om ook de brandweerman te kunnen laten werken. Het gaat om vrijwel de gehele klasse Manager

public class Manager {

    Teacher teacher;
    SocialWorker socialWorker;
    FireFighter fireFighter; //aanpassing

    //de constructor moet worden aangepast
    public Manager(Teacher teacher, SocialWorker socialWorker, FireFighter fireFighter) {
        this.teacher = teacher;
        this.socialWorker = socialWorker;
        this.fireFighter = fireFighter;
    }

    //deze methode moet worden aangepast
    public void manageWork() {
        teacher.work();
        socialWorker.work();
        fireFighter.work();
    }
}

Zelfs de Demo klasse werkt niet meer!

We kunnen dit ook anders opzetten. Begin met het maken van een interface.

public interface InterfaceWorker {
    public void work();
}

Nu passen we de klasse Manager als volgt aan. Deze veranderen we niet meer.

public class Manager {

    InterfaceWorker interfaceWorker;

    public Manager(InterfaceWorker interfaceWorker) {
        this.interfaceWorker = interfaceWorker;
    }

    public void manageWork() {
        this.interfaceWorker.work();
    }

    public void setInterfaceWorker(InterfaceWorker interfaceWorker) {
        this.interfaceWorker = interfaceWorker;
    }
}

En we kunnen nu probleemloos personeelsleden toevoegen zonder de klasse Manager te veranderen.

public class Teacher implements InterfaceWorker {

    @Override
    public void work() {
       System.out.println("A teacher is teaching children.");
    }
}

public class SocialWorker implements InterfaceWorker {

    @Override
    public void work() {
        System.out.println("A social worker is helping people.");
    }
}

public class FireFighter implements InterfaceWorker   {
    
    @Override
    public void work() {
       System.out.println("A firefighter is standby for a fire.");
    }
}

En de demo.

public class Demo {

    public static void main(String[] args) {
        Teacher teacher = new Teacher();
        Manager manager = new Manager(teacher);
        manager.manageWork();

        SocialWorker socialWorker = new SocialWorker();
        manager.setInterfaceWorker(socialWorker);
        manager.manageWork();
        
        FireFighter fireFighter = new FireFighter();
        manager.setInterfaceWorker(fireFighter);
        manager.manageWork();
    }
}

Ook al komen er meer personeelsleden bij, de code van de klasse Manager blijft hetzelfde.

3. Het Observerpattern

Dit pattern zullen we eerst geïsoleerd uitleggen. De lessen daarna zullen we het gaan integreren in onze Greenfoot game. We beginnen in Netbeans met het maken van een interface, de ObserverInterface:

package simpleObserver;

public interface ObserverInterface {
    public void update(String imageSetting);
    public void display();
}

De ObserverInterface dwingt af dat we een methode update update(int value) en een methode display() in de concrete klasse moeten maken. De methode display() behoort niet tot het pattern. We hebben deze alleen nodig in ons voorbeeld.

We maken nu twee concrete klassen: Wolf en Flower. Dit zijn observanten of, in het Engels, observers.

package simpleObserver;

public class Wolf implements Observer {
	private String imageSetting;
	
	public void update(String imageSetting) {
		this.imageSetting = imageSetting;
		display();
	}
	
	public void display() {
		System.out.println("I am a " + this.getClass().toString() + " and my image is + " + this.imageSetting);
	}
}

package simpleObserver;

public class Flower implements Observer {
	private String imageSetting;
	
	public void update(String imageSetting) {
		this.imageSetting = image;
		display();
	}
	
	public void display() {
		System.out.println("I am a " + this.getClass().toString() + " and my image is + " + this.imageSetting);
	}
}

Hiernder het UML-klassediagram.

@startuml
interface Observer {
  +void update(String imageSetting)
  +void display()
}

class Flower {
  -String image
  +void update(String imageSetting)
  +void display()
}
class Wolf {
  -String image
  +void update(String imageSetting)
  +void display()
}


Observer <|... Flower
Observer <|... Wolf
@enduml

We voegen nu een nieuwe interface toe: de SubjectInterface. Deze heeft drie methoden.

package simpleObserver;

public interface SubjectInterface {
	public void registerObserver(Observer observer);
	public void removeObserver(Observer observer);
	public void notifyObservers();
}

De methoden registerObserver() en removeObserver() hebben de de taak om, zoals de naamgeving ook al aangeeft, observers te registreren of te verwijderen uit de lijst van geïnteresseerde observers. De methode notifyObservers() heeft de taak om elke observer op de hoogte te brengen van een nieuwe waarde.

We maken nu een concrete Subject klasse met daarin de lijst en de code voor bovengenoemde methoden. Daarnaast is er de methode setImage() voor het instellen van een waarde voor een afbeelding. Zodra een nieuwe waarde is ingesteld worden alle observers daarvan op de hoogte gebracht.

package simpleObserver;

import java.util.ArrayList;

public class ConcreteSubject implements SubjectInterface {

    private ArrayList<ObserverInterface> observers;
    private String imageSetting;

    public ConcreteSubject() {
        observers = new ArrayList<ObserverInterface>();
    }

    public void registerObserver(ObserverInterface observer) {
        observers.add(observer);
    }

    public void removeObserver(ObserverInterface observer) {
        int i = observers.indexOf(observer);
        if (i >= 0) {
            observers.remove(i);
        }
    }

    public void notifyObservers() {
        for (ObserverInterface observer : observers) {
            observer.update(image);
        }
    }

    public void setImage(String imageSetting) {
        this.imageSetting = imageSetting;
        notifyObservers();
    }
}

In een concrete observer wordt eerst een variabele subject toegevoegd en daarna een constructor gemaakt.

package simpleObserver;

public class Flower implements ObserverInterface {

    private String image;

    private SubjectInterface subject;

    public Flower(SubjectInterface subject) {
        this.subject = subject;
        subject.registerObserver(this);
    }

    public void update(String image) {
        this.image = image;
        display();
    }

    public void display() {
        System.out.println("I am a " + this.getClass().toString() + " and my image is " + this.imageSetting);
    }
}

Je ziet dat in de constructor een object zichzelf registreert met subject.registerObserver(this). In de klasse Wolf gebeurt exact hetzelfde. Deze code geven we niet opnieuw.

We kunnen nu het volledig UML klassendiagram maken.

@startuml
interface ObserverInterface {
  +void update(String imageSetting)
  +void display()
}

class Flower {
  -String image
  +void update(String imageSetting)
  +void display()
}

class Wolf {
  -String image
  +void update(String imageSetting)
  +void display()
}

interface SubjectInterface {
    +void registerObserver(ObserverInterface observer);
    +void removeObserver(ObserverInterface observer);
    +void notifyObservers();
}

class ConcreteSubject {
  -ArrayList<ObserverInterface> observers
  -String image;
  +ConcreteSubject()
  +void registerObserver(ObserverInterface observer)
  +void removeObserver(ObserverInterface observer)
  +void notifyObservers()
  +void setImageSetting(String imageSetting)

}

ObserverInterface <|... Flower
ObserverInterface <|... Wolf
SubjectInterface <|... ConcreteSubject
SubjectInterface o- ObserverInterface
Flower o-- SubjectInterface
Wolf o-- SubjectInterface
ConcreteSubject *-- ObserverInterface
@enduml

Als laatste kunnen we de code gaan uitproberen met een Demo file. Eerst registeren Flower en Wolf zichzelf. ConcreteSubject kan Flower weer van de lijst te verwijderen.

package simpleObserver;

public class Demo {

    public static void main(String[] args) {
        ConcreteSubject subject = new ConcreteSubject();
        Flower flower = new Flower(subject);
        Wolf wolf = new Wolf(subject);
        subject.setImage("dark");
        subject.setImage("light");
        subject.removeObserver(flower);
        subject.setImage("dark");
    }
}

De output is als volgt:

I am a class simpleObserver.Flower and my image is dark
I am a class simpleObserver.Wolf and my image is dark
I am a class simpleObserver.Flower and my image is light
I am a class simpleObserver.Wolf and my image is light
I am a class simpleObserver.Wolf and my image is dark

4. Het Observerpattern in Greenfoot: de observerkant

We gaan nu het Observer pattern integreren in onze Roodkapje game. We willen hiermee het volgende bereiken. Roodkapje is op weg naar het huisje van oma en plukt onderweg bloempjes. Het bos is behekst. Zodra ze één bloempje plukt worden alle bloempjes geel. Ze kan nu niet meer de vergiftigde bloempjes van de niet-vergiftigde bloempjes onderscheiden. Ze had het moeten onthouden.

Ook de wolven dwalen rond. Die kunnen in het begin van de game nog niet zo hard lopen. Zodra Roodkapje een bloempje plukt wordt hun tempo hoger.

Dan is er ook nog het scorebord. In de Greenfootcursus moest dit object telkens opnieuw worden aangemaakt met een nieuwe score. Je kunt je voorstellen dat dit invloed heeft op de snelheid van de game. Een beter ontwerp is om van dit ontwerp een observer te maken waarbij telkens de waarde van de score wordt aangepast.

Je ziet dus dat met bovenstaand ontwerp alle actoren ook observers worden. Dit vraagt op de eerste plaats om een ObserverInterface. We geven hierin het argument GameController gameController mee. Dit is het subject. In de volgende les komen we hierop terug. Je krijgt hiervan in eerste instantie foutmelding.

import greenfoot.*;

public interface ObserverInterface {
    
    public void update(GameController gameController);
}

Vervolgens maken we van de klasse Being een observer. Alle klassen die van Being erven worden daarmee ook een observer.

Daarnaast een constructor waarmee we de variabele gamecontroller (subject) een waarde gaan geven. Verder een getter en een setter en de al bekende methode update(). Binnen deze methode zie je dat we de methode speedup() van de interface MoveInterface gebruiken. Deze methode komt nog aan bod.

import greenfoot.*;  

public abstract class Being extends Actor implements ObserverInterface
{
    protected MoveInterface moveInterface;
    protected FindInterface findInterface;
    protected GameController gameController;

    public Being(GameController gameController) {
        this.gameController=gameController;
    }

    public Being() {
    }

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

    public GameController getGameController() {
        return gameController;
    }

    public void setGameController(GameController gameController) {
        this.gameController = gameController;
    }

    public void update(GameController gameController) {
        this.moveInterface.speedup();
    }
}

?

De geïnteresseerde observers Wolf, BackForthWolf en Flower krijgen de constructor public <naam van de klasse> (GameController gameController). Hierbinnen krijgt gameController een waarde. Ook kun je zien dat een observer zichzelf registreert met behulp van de code this.gameController.registerObserver(this).

import greenfoot.*; 

public class BackFortWolf extends Being
{

    public BackFortWolf(GameController gameController) {
        this.moveInterface = new MoveBackFort();
        this.findInterface = new FindLittleRedCap();
        this.gameController=gameController;
        this.gameController.registerObserver(this);
    }
}

Roodkapje is niet geïnteresseerd en houdt dus grotendeels dezelfde code. Merk op dat ze de standaard constructor, dus onder parameters. gebruikt. Deze standaard constructor zit ook in de klasse Being.

import greenfoot.*; 

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

5. Het Observerpattern: de subjectkant

We hebben de Observerkant nu min of meer rond. In deze les komt de subjectkant aan bod. Het subject is de nieuw te maken klasse GameController. Als eerste maken we weer een interface met de inmiddels bekende drie methoden van het subject.

import greenfoot.*;  

public interface GameControllerInterface {
    
   	public void registerObserver(ObserverInterface observer);
	public void removeObserver(ObserverInterface observer);
	public void notifyObservers();
}

Merk op dat de ObserverInterface als parameter is ingesteld.

We kunnen nu de GameController gaan maken. Deze heeft de volgende code.

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
import java.util.ArrayList;
import java.util.List;

public class GameController implements GameControllerInterface  {

    private List<ObserverInterface> beings= new ArrayList();
    private String image;
    private boolean poisonous;

    public void notifyObservers() {
        for (ObserverInterface being : beings) {
            being.update(this);
        }
    }

    public void removeObserver(ObserverInterface being) {
        int i = beings.indexOf(being);
        if (i >= 0) {
            beings.remove(i);
        }
    }

    public void registerObserver(ObserverInterface observer) {
        beings.add(observer);
    }

    public void setImage(String image) {
        this.image = image;
    }

    public String getImage() {
        return this.image;   
    }

    public void setPoisonous(boolean poisonous) { 
        this.poisonous = poisonous;
    }

    public boolean isPoisonous() {
        return this.poisonous;
    }

}

GameController is voorlopig verantwoordelijk of een bloem wel of niet vergiftigd is. Daarnaast is GameController in staat om van alle bloemen de afbeelding te wijzigen.

We geven nu ook de vernieuwde code van Level1. Als we daarin een bloempje aanmaken moeten we via GameController aangeven of deze wel of niet vergiftigd is.  Vervolgens wordt een instantie van gameController als argument meegegeven.

import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
import java.util.Random;
import java.util.List;

public class Level1 extends World
{

    private final int PINKFLOWERS = 20;
    private final int BLUEFLOWERS = 2;
    private int margin = 50;
    GameController gameController;

    public Level1()
    {    
        super(600, 600, 1); 
        gameController=new GameController();       
        prepare();
    }

    /**
     * Prepare the world for the start of the program. That is: create the initial
     * objects and add them to the world.
     */
    private void prepare()
    {
        addObject (new BackFortWolf(this.gameController), 300, 120);
        addObject (new Wolf(this.gameController), 200, 300);
        addObject (new LittleRedCap(), 300, 500);
        addFlowers(true, BLUEFLOWERS);
        addFlowers(false, PINKFLOWERS);
    }

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

    public void addFlowers(boolean poisonous, int flowers) {
        for (int i = 0; i < flowers; i++) {
            int x = getRandomNumber(margin,600-margin);
            int y = getRandomNumber(margin*3,600-margin);
            List list = getObjectsAt(x, y, Flower.class);
            //
            if (list.size()>0){
                //get a new random number
                x = getRandomNumber (margin,600-margin);
                y = getRandomNumber (margin*3,600-margin);
            }
            gameController.setPoisonous(poisonous);
            addObject(new Flower(gameController),x, y);
        }
    }
}

Merk ook op dat we het aanmaken van bloempjes wat handigerer hebben ingericht met een methode addFlowers() in plaats van twee keer deze code opschrijven. Deze methode houdt ook in de gaten of er geen bloempjes dubbel op elkaar staan.

6. Het algoritme FindFlower, MoveRandom en MoveBackFort aanpassen

Om het Observer algoritme nu werkend te maken moeten we enkele zaken aanpassen. Op de eerste plaats moeten alle observers weten wanneer Roodkapje een bloempje plukt. De klasse FindFlower is daarom als volgt aangepast.

import greenfoot.*;
import java.util.List;

public class FindFlower implements FindInterface
{
    private Actor actor;

    public void find(Actor actor){
        this.actor=actor;
        pickFlowers();
    }

    private void pickFlowers()
    {
        List list = actor.getWorld().getObjectsAt?(actor.getX(), actor.getY(), Flower.class);
        if(list.size()==1) {
            World world = actor.getWorld();
            Flower flower = (Flower) list.get(0);
            flower.getGameController().setImage("sm-flower-yellow.gif");
            flower.getGameController().notifyObservers();
            if(flower.isPoisonous()) {
                world.addObject (new Announcement("Game Over"), 300,300);
                Greenfoot.stop();
            }
            actor.getWorld().removeObject(flower);

        }
    }
}

Het enige wat  hier dus in is gebeurd is dat zodra een bloempje wordt geplukt de afbeelding wordt gewijzigd in sm-flower-yellow.gif. Vervolgens worden alle bloempjes (observers) gewijzigd met deze afbeelding. Roodkapje weet nu niet meer welke bloempjes vergiftigd zijn en welke niet.

De wolven krijgen bij elk bloempje wat wordt geplukt meer speed. Deze code is ook niet al te ingewikkeld. We geven hieronder de code van klasse MoveBackFort en MoveRandom.

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

public class MoveRandom implements MoveInterface {

    private Actor actor;
    private final int RANDOMWALK = 10;
    private int speed = 0;

    public void move(Actor actor) {
        this.actor = actor;
        actor.move(this.speed);
        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);
        }
    }

    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;
    }

    public void speedup() {
        this.speed++;
    }
}

import greenfoot.*;

public class MoveBackFort implements MoveInterface {

    private Actor actor;
    private int speed = 0;

    public void move(Actor actor) {
        this.actor = actor;
        actor.move(speed);
        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);
            }
        }
    }

    public void speedup() {
        this.speed++;
    }
}

Merk op hoe makkelijk het eigenlijk is om code aan te passen!!

7. Het UML-schema tot nu toe.

We geven nu nog een overzicht van wat je tot nu hebt gemaakt. In deze link het volledige plaatje.

8. Het Decorator pattern

Zoals we al eerder hebben gezien behoort het Decorator pattern tot de Structural patterns. Dat houdt in dat het een pattern is wat code inzichtelijker maakt zonder de basis te veranderen. Dit zullen we nu meteen in Greenfoot gaan uitleggen.

We beginnen met het maken van een interface.

import greenfoot.GreenfootImage;


public interface Shape {

    public void draw();

    public GreenfootImage getImage();
    
}

We gaan in Greenfoot code maken wat kan tekenen met behulp van cirkels en rechthoeken.(in de opdracht). Als basisklasse maken we een klasse Figure. Deze kent twee variabelen: image en height en implementeert de klasse Shape.

import greenfoot.*;

public class Figure extends Actor implements Shape {

    private GreenfootImage image;
    private int height;

    public Figure(int height) {
        this.height = height;
        image = new GreenfootImage(height, height);
        draw();
    }

    @Override
    public void draw() {
        setImage(image);
    }

    @Override
    public GreenfootImage getImage() {
        return image;
    }

    public int getHeight() {
        return height;
    }
}

We initiëren dit figuur in Level 1.

Shape shape = new Figure(100);

Het UML-diagram ziet er nu als volgt uit.

@startuml

abstract class Actor {
  ...
}

interface Shape  {
  +void draw();
  +GreenfootImage getImage();
}

class Figure {
  -GreenfootImage image
  -int height
 
  +Figure(int height)
  +void draw()
  +GreenfootImage getImage()
  +int getHeight()
}

Actor  <|-- Figure
Shape <|.. Figure
@enduml

9. Het Decorator pattern 2

Bij het Decorator pattern draait het om klassen die iets toevoegen aan een basisklasse. Deze gaan we nu maken. Eerst maken we een abstracte klasse ShapeDecorator die ook weer de klasse Shape implementeert.

import greenfoot.*;

public abstract class ShapeDecorator extends Actor implements Shape {

    protected Shape decoratedShape;
    GreenfootImage image;
    protected int distance;

    public ShapeDecorator(Shape decoratedShape, int distance) {
        this.decoratedShape=decoratedShape;
        this.image = decoratedShape.getImage();
        this.distance = distance;
    }

    @Override
    public GreenfootImage getImage() {
        return image;
    }
}

Je ziet deze klasse drie variabelen heeft: een decoratedShape, een distance en de image die in de decoratedShape zit. De variabele distance wordt gebruikt om de afstand in te stellen die tussen de tekenen cirkels zit. Hier komen we later op terug.

Vervolgens gaan we nu de concrete Decorators instellen. We beginnen met een kleurendecorator die de kleur groen instelt.

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

public class GreenColorDecorator extends ShapeDecorator
{
    private int colorSetting;

    public GreenColorDecorator(Shape decoratedShape) {
        super(decoratedShape, 0);
        draw();
    }

    @Override
    public void draw() {
        image.setColor(new Color(51, 102, 0));
    }
}

Merk op dat we in deze decorator de variabele distance op 0 hebben gezet. Deze is hier nog niet van belang. De klasse zelf vraagt alleen om de decorated Shape die we in de klasse shapeDecorater gebruiken om de distance en de image op te vragen. In de klasse RedColorDecorator zetten we de kleur op groen.

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

public class RedColorDecorator extends ShapeDecorator {

    public RedColorDecorator(Shape decoratedShape) {
        super(decoratedShape, 0);
        draw();
    }

    @Override
    public void draw() {
        image.setColor(new Color(255, 0, 0));
        setImage(image);
    }
}

Het UML-schema is nu als onderstaand schema.

@startuml

abstract class Actor {
  ...
}

interface Shape  {
  +void draw();
  +GreenfootImage getImage();
}

class Figure {
  -GreenfootImage image
  -int height
 
  +Figure(int height)
  +void draw()
  +GreenfootImage getImage()
  +int getHeight()
}

abstract class ShapeDecorator {
  #Shape decoratedShape
  #GreenfootImage image
  #int distance

  +ShapeDecorator(Shape decoratedShape, int distance)
  +GreenfootImage getImage()
}

class RedColorDecorator {
  +RedColorDecorator(Shape decoratedShape)
  +void draw()
}

Actor  <|-- Figure
Actor  <|-- ShapeDecorator
ShapeDecorator <|-- RedColorDecorator
Shape <|.. Figure
Shape <|.. ShapeDecorator
Shape *-- ShapeDecorator
@enduml

10. Het Decorator pattern 3

De RedColorDecorator maakt nog niet dat het werkt. Er gebeurt nog niets. Dat komt nu in deze les. We gaan nu een klase CircleDecorator maken die  daadwerkelijke cirkels gaat tekenen. We willen namelijk het volgende figuur maken:

https://images.computational.nl/galleries/patterns/2020-02-09_19-38-47.png

Dit zijn 8 cirkels die volgens een bepaald patroon worden getekend. Omdat dit patroon niet tot het Decorator pattern behoort leggen we dit in de volgende les pas uit.

We gaan nu eerst deze decorator maken. De code is als volgt.

public class CircleDecorator extends ShapeDecorator {

    public CircleDecorator(Shape decoratedShape, int distance) {
        super(decoratedShape, distance);
        draw();
    }

    @Override
    public void draw() {
        this.drawBresenhamsCircle(distance*2, distance);
    }

    private void drawBresenhamsCircle(int r, int x) {
        int y = r;
        int height=image.getHeight()/4;
        int width=image.getWidth()/4;
        image.drawOval(x+r, y+r, height, width);
        image.drawOval(-x+r, y+r, height, width);
        image.drawOval(x+r, -y+r, height, width);
        image.drawOval(-x+r, -y+r, height, width);
        image.drawOval(y+r, x+r, height, width);
        image.drawOval(-y+r, x+r, height, width);
        image.drawOval(y+r, -x+r, height, width);
        image.drawOval(-y+r, -x+r, height, width);
    }
}

Je ziet dat in deze Decorator de cirkels daadwerkelijk worden getekend met behulp van de methode drawOval(). In Level1 wordt de decorator uit de vorige les, die de cirkels een kleur geeft, samengevoegd met bovenstaande Decorator. Dit gaat in Level1 als volgt.

Shape shape = new Figure(200);
Shape coloredShape = new RedColorDecorator(shape);
Shape figure = new CircleDecorator(coloredShape, 8);
addObject ((Actor) figure, 200, 200);

Wat er eigenlijk gebeurt is dat de Shape telkens opnieuw wordt gebruikt en doorgegeven aan een Decorator. Het kan ook in één regel:

Shape shape=new CircleDecorator(new RedColorDecorator(new Figure(200)), 8);
addObject ((Actor) shape, 200, 200);

Dus van binnen naar buiten voeg je telkens wat toe. De buitenste Decorator is uiteindelijk de klasse die de cirkels tekent.

11. Bresenham’s cirkel

We zullen nu uitleggen hoe we de acht cirkels tekenen. Wellicht was je al opgevallen dat de cirkels die worden getekend zelf ook weer een cirkel vormen. Dit is bijvoorbeeld het geval als we de volgende coderegels invoeren:

Shape shape=new CircleDecorator(new GreenColorDecorator(new Figure(500)), 3);
addObject ((Actor) shape, 400, 400);

Het resultaat is dan:

https://images.computational.nl/galleries/patterns/2020-02-13_20-21-00.png

Je ziet dat de acht cirkels zelf ook weer een cirkel vormen. Dit is mogelijk gemaakt door het Bressenham algoritme. Dit werkt als volgt.

Stel dat we een cirkel nemen en die in acht parts verdelen.

https://images.computational.nl/galleries/patterns/bressemham.png

Bij bovenstaande coderegel krijgt is x=2, y=1. Tegen de klok in worden de cirkels dan op de punten  (2,1)/(1,2)/(-1,2)/(-2,1)/(-2,-1)/(-1,-2)/(x,-2)/(2,-1) geschreven. Bij elkaar vormt dit dus een cirkel met 8 coördinaten. Door het algoritme nog wat te manipuleren zodat het binnen het image wordt getekend krijg je dus mooie figuren (zie algoritme).

Diegene die dit heeft bedacht heeft dit nog verder uitgewerkt.

12. De roodkapje game met alle patterns tot nu toe

Je kunt hier de game downloaden zoals tot nu toe is gemaakt, met alle design patterns. De achtergrond is wit zodat je de cirkels goed kunt zien.

De game is niet echt af in de zin dat je naar een volgend level kunt gaan - zie opdracht.

13. Het complete uml-schema.

Opnieuw geven we nu het UML-diagram. De afbeelding is hier opnieuw te bekijken.

@startuml

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

abstract class Being {
  #MoveInterface moveInterface
  #FindInterface findInterface
  #GameController gameController
  #void act() 
}
class Wolf {
  +Wolf(GameController gameController)
}

class BackForthWolf {
 +BackFortWolf(GameController gameController) 
}

class LittleRedCap {
  +LittleRedCap(GameController gameController)
}

class Flower {
  Flower(GameController gameController)
}

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)
  -void eatLittleRedCap()
}

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)
}

interface GameControllerInterface {
  +void registerObserver(ObserverInterface observer);
  +void removeObserver(ObserverInterface observer);
  +void notifyObservers();
}

class GameController {
  -List<ObserverInterface> beings
  -String image
  -boolean poisonous
  +void notifyObservers()
  +void removeObserver(ObserverInterface being)
  +void registerObserver(ObserverInterface observer)
  +void setImage(String image)
  +String getImage()
  +void setPoisonous(boolean poisonous
  +boolean isPoisonous()
}

interface Shape {
  void draw()
  GreenfootImage getImage()
}

abstract class ShapeDecorator {
  #Shape decoratedShape;
  #GreenfootImage image;
  #int distance;

  +ShapeDecorator(Shape decoratedShape, int distance) 
  +GreenfootImage getImage()
}

class CircleDecorator {
  +CircleDecorator(Shape decoratedShape, int distance)
  +void draw()
  +void drawBresenhamsCircle(int x, int y)
}
class SquareDecorator {
  +SquareDecorator(Shape decoratedShape, int distance)
  +void draw()
  +void drawBresenhamsCircle(int x, int y)
}
class MixedDecorator {
  +MixedDecorator(Shape decoratedShape, int distance)
  +void draw()
  +void drawBresenhamsCircle(int x, int y)
}

class GreenColorDecorator {
  -int colorSetting
  +GreenColorDecorator(Shape decoratedShape)
  +void draw() 
}
class RedColorDecorator {
  -int colorSetting
  +GreenColorDecorator(Shape decoratedShape)
  +void draw() 
}

Actor <|--  Being
Actor  <.. MoveInterface
Actor  <.. FindInterface
Actor  <|-- ShapeDecorator
Being <|--  Wolf
Being <|--  BackForthWolf
Being <|--  LittleRedCap
Being <|--  Flower
Being *- MoveInterface
Being *- FindInterface
Being o- GameController
MoveInterface <|... MoveBackForth
MoveInterface <|... MoveRandom
MoveInterface <|.... MoveWithArrowsAdapter
FindInterface <|... FindFlower 
FindInterface <|... FindLittleRedCap 
FindInterface <|... FindNothing
KeyInterface <|... KeyArrows
GameControllerInterface <|... GameController
MoveWithArrowsAdapter *- KeyInterface
ShapeDecorator <|-- CircleDecorator
ShapeDecorator <|-- SquareDecorator
ShapeDecorator <|-- MixedDecorator
ShapeDecorator <|-- GreenColorDecorator
ShapeDecorator <|-- RedColorDecorator
Shape <|.. ShapeDecorator

@enduml

____