Lab 13: The Adapter Pattern

The goal of this lab is to practice using the Adapter pattern.

1. Introduction

Evil Professor NenurdNav has prepared his own version of a predator-prey simulation for Lab 31 of his course CSCI 542. It is similar to ours conceptually, but all the code is different. It includes a class Snake, which has some interesting behavior:

You like this idea and want to plug this Snake class into our version of the simulation. However, it uses all different interfaces. You must use the adapter pattern in order to make the Snake class work in our simulation. Specifically, you will need to finish a few classes for which I have given you some skeleton code, all found in the predprey package.

2. Initial setup

Copy the current version of the simulation code into your directory.

cp -r ~tvandrun/Public/cs245/lab13/* .

When you make an Eclipse project for this, make sure that you select the folder containing the folders predprey, simulation, and nenurdnav. If you have things set up right, then the second screen of the new-project wizard should look something like this:

If it doesn't and you don't know why, then ask for help.

3. Inspecting the code

The first step is to get to know the new code. Inspect the the files in the nenurdnav package. (You will not need to modify any code in this package, although you might want to add debugging code there at some point.)

4. Writing adapters

Read all these instructions carefully and do these steps in order. They are fairly complicated. Along the way, we will run into certain specific problems. For each of these, first make sure you have completed the task up to the point when the problem comes up; then make sure that you understand what the problem is; then, read on to find the solution to the problem.

A. SnakeAdapter, the basics

Look the class SnakeAdapter. It adapts a Snake to the Agent interface. It has an internal Snake that is being wrapped/adapted. The class also has a bunch of stuff that we'll need later.

For right now, look at the end of the file where the methods required by the Agent interface are. Complete these methods.

All these methods can be one-liners. You may choose an arbitrary, reasonable value for the snake's weight and color. For other operations, delegate them to the internal snake.

The Agent interface also requires an enterGrid() method, and we would like to complete that method next. However, we have a problem:

The Snake object needs a reference to an Island object, not a SimulationGrid object. Moreover, while an agent in our simulation receives a reference to the grid through the method enterGrid() (invoked sometime after the class is instantiated to make the object), the Snake class needs a reference to that Island passed to it through the constructor.

B. GridAdapter.putOrganism(int, int, char, int, dist, Organism)

We need to adapt the grid so that the snake can use it as an Island. (Make sure you understand all that's going on: With SnakeAdapter, we were taking something from the other simulation and adapting it to fit into ours; now we are going to take something from our simulation and adapt it so that it can fit into the class from the other simulation.)

Look at the GridAdapter class. It implements Island and has an internal grid.

The Island interface has two putOrganism() methods, both of which play the role that setAgentAt() plays in our grid. The second putOrganism() method has an interesting feature: you don't need to give it the absolute new position of the organism; instead you can give a reference point, a direction, and a distance, and it will put the organism in the position which is the given distance away from the given position in the given direction.

Implement this method (the putOrgansim() method that returns an int[]). Read the documentation for the method in the interface carefully to be sure you understand what it should do. Do not put the organism in the grid directly---after all, you can't put an Organism in the grid, because it is a grid of Agents, not Organisms. We'll worry about that problem later. Instead, putOrganism(int, int, char, int, Organism) should call the other method, putOrganism(int, int, Organism)

Once that is done, we should consider how to extract organisms, using getOrganism(). However, this leads us to a problem:

The SimulationGrid object contains only Agents, but this method needs to return an Organism. Put another way, the Snake needs to look at Organisms, but everything else in the simulation is an Agent.

C. AgentAdapter and GridAdapter.getOrgansim()

What we need to do is adapt Agents so that Snake can interact with them. This is actually a pretty easy task, so I've already done it for you. Look at AgentAdapter. It is used to wrap an Agent (such as Rabbit) so that it masquerades as an Organism.

Your job is to use this class. First of all, you will need to do a similar calculation of the exact position based on the relative position, distance, and direction, as in putOrganism().

Then, consider what will be at that position: either it will be a real Agent, in which case it needs to be wrapped in an AgentAdapter and returned; or, it will be a SnakeAdapter, in which case it needs to be unwrapped and just the internal snake returned.

Finish the GridAdapter.getOrganism() method..

With that experience behind us, we can start thinking about the other putOrganism() method. In principle, this method is just like getOrganism(), but the other way around: the input is Organism which means it is either a Snake or an AgentAdapter; all Snakes need to wrapped as SnakeAdapters before being put into the grid, and all AgentAdapters need to be unwrapped. However, this brings us to a new problem:

The most common use of putOrganism() for a snake is when it moves, which means it will be putting itself into another position of the grid. However, we don't want to keep making new SnakeAdapters every time the snake moves. Not only would that create more objects than necessary, it will also make it difficult to give each snake a reference to the grid.

D. SnakeAdapter and GridAdapter.putOrganism(int, int, Organism)

The solution is that we keep a HashMap mapping Snakes to SnakeAdapters. When we want to make a new adapter for a snake, we first check to see if that snake already has an adapter, and use that one instead. (The only time that a snake wouldn't have an adapter is if it were just hatched.)

Instead of making an adapter for a snake by instantiating the class (calling the SnakeAdapter(Snake) constructor), the GridAdapter class should use the static makeAdapter() method, which first checks the hash map for a previously made adapter for that snake.

Notice that the SnakeAdapter(Snake) constructor is private and that it prints an error to the screen if an adapter already exists for that snake.

Also notice that there is another constructor, SnakeAdapter() without a Snake parameter. This is to be called when the simulation starts and we make the initial population of the island. Notice that this constructor does not initialize internal. More on that later.

Your task in this this part is to write the method GridAdapter.putOrganism(int, int, Organism). Note that the organism o is one of three things: null, a Snake, or an AgentAdapter.

E. Finally, PDAdapter

We still need to write SnakeAdapter.enterGrid(), but the Snake needs not only a grid, but also a PredatorDeterminer. So, one more adapter: PDAdapter which wraps the PreyArbitor so that it acts like an object with type PredatorDeterminer.

One complication: PreyArbitor is a class with only static methods, but PredatorDeterminer is to be an object.

Look at the class PDAdapter. Notice that it is a singleton class. Finish it by writing the preysOn() method. Notice that the two parameters a and b are each either null, a Snake (which needs to be wrapped), or a AgentAdapter (which needs to be unwrapped).

(Our PreyArbitor can handle SnakeAdapters. See the file predprey.dat.)

F. Finally finally, SnakeAdapter.enterGrid()

Remember the problem that Snakes need to get their Island when they are instantiated, but in our simulation, agents don't get their grid reference until later, when enterGrid() is called. Here's how we'll solve this:

There are two ways in which a Snake is instantiated. Either it is part of the initial population, or it's an offspring. In Snake.go(), the code for making an offspring gives the child snake a reference to the same island as the parent snake, so we never need to call enterGrid().

If the snake is part of the initial population, then what will happen is the simulation will make a new SnakeAdapter using the public constructor. This doesn't actually make a new Snake, that is, it doesn't initialize internal. We'll defer that to when enterGrid() is called.

Your final task is to write that SnakeAdapter.enterGrid() method. It should

And that's it! Now try it out.


Thomas VanDrunen
Last modified: Tue Dec 2 13:02:27 CST 2014