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 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.
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 Creative Commons BY-NC-SA 4.0 License.