Home CPSC 240

Design Patterns Continued

 

The Observer Pattern

The observer pattern is a very common one. In fact we have seen it before. The way that GUI components respond to events in Swing is based on the observer pattern. The idea is that we allow a number of objects (the observers) to be notified when some other object (the subject) has changed in some way.

In the case of Swing buttons, the subject is the button and the observers are the ActionListener objects. We register the observers by calling addActionListener.

As another example, imagine we have an RSS program where users can subscribe to RSS feeds. In this system, users would be sent links when the feed they are on is updated. We can have a Subscriber interface which allows links to be received:

 
public interface Subscriber {
    void sendLink(String link);
}

We can then write a Feed class which contains a list of subscribers. Every time we have a change in state (in this case new material ready), we can notify all the Subscriber objects by calling the method:


public class Feed {
    private ArrayList<Subscriber> subscribers;
    private String url;

    public Feed(String url) {
        this.url = url;
        subscribers = new ArrayList<>();
    }

    public void addSubscriber(Subscriber sub) {
        subscribers.add(sub);
    }

    public void update() {
        // here we could download new articles from the URL if any exist
        String[] newLinks = {"article 1", "article 2"};

        for (String link : newLinks) {
            for (Subscriber s : subscribers) {
                s.sendLink(link);
            }
        }
    }
}

This allows us to call the .update() method on our Feed object, and have all the subscribers be notified of the new material.


 

The Iterator Patten

The iterator pattern is another one which is used by Java libraries. It is what allows us to use algorithms like sort, shuffle, reverse, min, max, etc. on not just arrays and ArrayLists, but also our own classes that we can create. They are also behind the new-style for loop in Java which lets us loop through a variety of sequences.

The idea is that an Iterator is an object which tells us two things: what the next element in the sequence is, and whether we are at the end of it. We can make an iterator by implementing the Iterator interface and overriding methods. We can create a new iterator which lets us loop through the alphabet:


class AlphabetIterator implements Iterator<Character> {
	// keeps track of which one we're on and whether we are uppercase or not
	private char current;
	private boolean upper;
	
	// starts out on a or A
	public AlphabetIterator(boolean uppercase) {
		if (uppercase) {
			current = 'A';
		} else {
			current = 'a';
		}
		upper = uppercase;
	}

	// returns whether we have a next value in the sequence
	@Override
	public boolean hasNext() {
		if (upper) {
			return current <= 'Z';
		} else {
			return current <= 'z';
		}
	}

	// returns the next one in the sequence
    @Override
	public Character next() {
		Character val = Character.valueOf(current);
		current++;
		return val;
	}
}

This will allow us to loop through letters. We provide a constructor to tell us if we want them to be upper or lower case. Then we override the next and hasNext methods.

The next step is to create a class that provides iterators into it. This is what classes like ArrayList do. Any class that implements the Iterable interface can be looped through by providing iterators into it. There is only one method in this interface: iterator which returns an iterator into the sequence. We can make an iterable alphabet class like this:


class Alphabet implements Iterable<Character> {
	private boolean uppercase;
	
	public Alphabet(boolean uppercase) {
		this.uppercase = uppercase;
	}

	public Iterator<Character> iterator() {
		return new AlphabetIterator(uppercase);
	}
}

With this, we can create an Alphabet object, and loop through it with a for loop:


Alphabet letters = new Alphabet(true);
for (Character c : letters) {
    System.out.println(c);
}

The way the new-style for loop works internally is by calling .iterator() on the object in it. Then it calls .hasNext() and .next() on it to go through the items therein.

We can also write our own code to the Iterable interface. For example, we can write a method to count how many items are in a sequence:


public static int count(Iterable sequence) {
    int i = 0;
    // loop through using the iterator interface
    Iterator current = sequence.iterator();
    while (current.hasNext()) {
        i++;
        current.next();
    }
    return i;
}

We can call this method with anything that is iterable: our Alphabet class, an ArrayList, a LinkedList, etc. The complete example is available at IteratorExample.


 

The Strategy Pattern

Another common design pattern is the strategy pattern. In this pattern, we have multiple ways of doing some task. We extract that task into an interface and then create classes which perform the task in each different way. We can then write code to the interface and allow the different strategies to be "plugged in" to it.

We did something very similar to this with our version of the Pig! game which used inheritance. Here, the way we decide to roll or stay is the "strategy". We have two ways of performing this task: either by asking the user or using AI. We used an abstract class, while the Strategy pattern typically uses an interface, but the idea is the same.

As another example, imagine we are creating a program which can work with image files. We might want to support multiple types of image formats (.png, .jpg, etc.) but not want to put code dealing with them all in our main save code. We can create a "Strategy" which represents the way we do the saving:


public interface SaveFormat {
    void save(Pixel[][] image);
}

Then we can write code which does the saving by relying on the strategy:


public class Image {
    
    // ... lots of other stuff ...

    public void save(SaveFormat format) {
        // ask the user where they want to save to
        format.save(data);
    }
}

Then we can write classes to save to different formats:


public class PngFormat implements SaveFormat {
    @Override
    public void save(Pixel[][] image) {
        // ...
    }
}

public class JpgFormat implements SaveFormat {
    @Override
    public void save(Pixel[][] image) {
        // ...
    }
}

And then choose when we call the save method on our image:


Image img = new Image();

// ...

img.save(new PngFormat());

Essentially the purpose of the strategy pattern is to extract out some part of the process into its own classes, so that we can plug different ones in to get different behavior.

Copyright © 2024 Ian Finlayson | Licensed under a Attribution-NonCommercial 4.0 International License.