8
$\begingroup$

I am finding it very difficult to create a lab for Objects in Java before we've involved Inheritance and Polymorphism. The problem, as I see it, is that Inheritance and Polymorphism are basically the motivators for Objects in the first place. As a result, the labs that I've created have all felt stodgy and artificial.

However, as an instructional reality, waiting until we have all three ideas in place in a high school environment simply means biting off too much material. Are there other ways to think about creating labs for Objects that would support the early understandings required while still being meaningful?

$\endgroup$
  • 1
    $\begingroup$ Have you considered making them use an OO- API before writing their own classes? $\endgroup$ – Michel Billaud Dec 17 '17 at 18:58
  • $\begingroup$ @MichelBillaud That's a good point. And there are many resources out there to help with that. I've never been inclined in that direction, but perhaps you are right that that is the best path forward. $\endgroup$ – Ben I. Dec 17 '17 at 19:13
  • $\begingroup$ @BenI. The interest of encapsulation, polymorphism, inheritance etc. is difficult to demonstrate on small size self-contained examples. Silly examples like animals and vehicles don't help. $\endgroup$ – Michel Billaud Dec 18 '17 at 10:41

11 Answers 11

10
$\begingroup$

Inheritance and Polymorphism are basically the motivators for Objects in the first place

I think this is where you're getting stuck. Neither of those are necessarily first-class motivators for Objects, but they are useful things you can do when you have objects.

Work-in-progress textbook chapter on OOP (aimed at students aged 16-18) and why we use it: https://en.wikibooks.org/wiki/A-level_Computing/AQA/Paper_1/Fundamentals_of_programming/Object-oriented_programming

Some alternatives:

"Object = Code + Data"

  • It's easy to show examples where global functions cause problems because they accidentally trample on each other's data (they're using the same data, but making assumptions about it)
  • ...especially easy/fun to show how two unrelated functions can accidentally choose the same name for their variables (they're using different data, but happen to have chosen same name, e.g. "x" for x-co-ordinate, but also "x" for number-of-items-to-multiply)
  • Via encapsulation (and optionally: hiding) objects wipe-out many of these common, painful, frustrating bugs that beginner programmers often discover themselves when using global functions

"Object = aggregation of functions"

  • How do you organize your source code when you have more than a few dozen functions? Objects help enormously

"Object = classification of data" (Object as ADT)

  • How do you organize your data when you have three different meanings of "x-coordinate": Position, Velocity, and Acceleration? Objects make it easy to create ad-hoc, named, data-types that massively simplify this problem
  • i.e. Objects-as-Abstract-Datatype
$\endgroup$
  • $\begingroup$ I think you missed one other important aspect of objects: reuse. A properly built object can be reused in many future projects. Look to the 'libraries' included with many programming languages as a prime example. $\endgroup$ – Gypsy Spellweaver Jun 15 '17 at 3:51
  • 1
    $\begingroup$ @GypsySpellweaver Yes, I wasn't trying to be comprehensive in the list above, just a few of the more common, easier to appreciate, items. But code re-use is mostly achieved without objects - the things that make objects good for code re-use were already common before OOP - so I think it's a more nuanced one. $\endgroup$ – user31 Jun 15 '17 at 8:38
  • $\begingroup$ @GypsySpellweaver, unfortunately, "reuse" has been overhyped. Libraries are reused, of course, but not a lot else. It is incredibly difficult to build objects with the intention of reuse. Some domain frameworks work, but not much else. OTOH, OOP makes it easy to construct purpose-built (bespoke) solutions. But inheritance is NOT the main tool, and is itself overused. Inheritance from classes (rather than interfaces) can lead you quickly into dead ends. $\endgroup$ – Buffy Jul 24 '17 at 13:36
  • $\begingroup$ A fourth possibility is "Object = Bundle of Behavior". The public interface defines the behavior. Any "data" is there only as a consequence of the implementation of the behavior. The data could be directly held or could be deferred to other objects, either held in fields or simply referenced. This is not quite the same as aggregation of functions, since the behaviors have a consistent overall purpose. The methods can also be thought of as services of various kinds. What does the Monkey do? $\endgroup$ – Buffy Jul 24 '17 at 13:44
7
$\begingroup$

A benefit to OOP often overlooked is encapsulation. The object has data and methods (knows things and does things) that elements outside the object neither have access to, nor even know exists. Only the exposed elements (methods) can be used by other elements to get the expected results. (However that happens.)

Build the lab around the "mystery" of an object's contents. Create a class they can implement, without having the source, only the methods. Have them experiment with that interface to see how easy it is to get results without having to create the "inner workings" of the object. Then have them recreate that class. The only requirements being that it look and act the same from outside the class. How they get it to generate the same results is independent work. Lastly, compare different versions, having the students swap their version with other students' versions and see the possibilities of interoperability, and the beginnings of how "libraries" are built and maintained.

$\endgroup$
  • $\begingroup$ I love this idea. I'm going to tinker around with it a bit and see what I can come up with. $\endgroup$ – Ben I. May 30 '17 at 11:14
4
$\begingroup$

After students have explored java's string and math classes I introduce a group project called "Mathey". Each group is to write as many math functions that they can think of and assemble them into their own class. Students seem to like this and are very competitive in coming up with useful math functions and enjoy showing off their work and creativity when demonstrating their class to their peers.

$\endgroup$
  • $\begingroup$ I love this idea. I have a kid right now who is trying to make an algebraic solver for a project. What a creative way to package such projects! $\endgroup$ – Ben I. May 29 '17 at 23:43
  • $\begingroup$ Also, welcome to the community. I hope we hear more from you in the future! $\endgroup$ – Ben I. May 29 '17 at 23:47
  • $\begingroup$ It might be useful to add some more detail about this project and how it relates to object, but otherwise great answer! Welcome to CSEducators.SE! $\endgroup$ – thesecretmaster May 30 '17 at 2:24
2
$\begingroup$

Our first Java class has a prereq of procedural programming, so students already know how to write functions. We begin by giving an overview of the type system and the basics of reading an API page. We study the String and BigInteger classes in some depth. They have seen object-based programming in Python, so the waters here are pretty familiar.

Our first big project is to build a simple API. Example: BigFraction, which is a type capable of doing arbitrary-precision rational arithmetic. Other possibilities: Vector3D, a 3-d vector class, Complex, a complex number class, and Quaternion, a class for handling quaternions.

The students learn Javadoc at this time. They are asked to program with another student's API. It's quite interesting.

$\endgroup$
2
$\begingroup$

My planned approach for AP CS A next year is to have students begin thinking about how to model the world around them using objects. Coming off a year of C programming, the jump to OOP is more a conceptual than a syntactical challenge.

One challenge I plan on assigning is to create a class to model a student in high school. What variables are essential? What are key methods for student behavior? What might be public or private? It is completely open-ended and will force students to begin thinking in an object-oriented manner. It will be less of code and more of computational thinking in the OO-context.

After doing this, inheritance will become much clearer. It's one thing to have a Students class, but it is also valuable to create a class for each grade level and extend the superclass accordingly (or to have subclasses based on elementary, high school, college, grad school). The possibilities for design become endless, and effective program/software design can truly begin here.

While the syntax is important to teach, I see the introduction of objects as a great vehicle for "unplugged" activities that teach the object-oriented mindset.

$\endgroup$
2
$\begingroup$

I'm a big fan of using real-world data in as many class assignments in APCS A as possible. Small example: I was trying to prove to a group of people just how bad the weather had been in Denver in May 2015. Bad meaning an abundance of precipitation. Off I went to NOAA's website to gather weather precipitation data for Denver for all years on record. Found an Excel file. After cleaning it up a bit, I had two columns, one for date and one for units of precipitation. Started to write a program to read that data in from the file. I had a choice to make. Would I want to create two parallel arrays - one for the dates and one for the units of precipitation? Easy enough to do, but what if i accidentally sorted one array and not the other? Then my data would be corrupt. That brought me to my second option: create a Weather object that has two fields, date and precipitation. That way I could create one array of Weather objects with no chance of the dates and units of precipitation going askew. Simple example (no need for inheritance or polymorphism). Powerful result.

$\endgroup$
2
$\begingroup$

I think an easier, more natural motivator for objects is aggregate objects. This means you can approach discussion with encapsulation and reuse, and also decomposition of a problem. A classic is a deck object containing card objects.

This is still very tricky when you have strong programmers. You want to show how decomposition with objects makes the problem simpler, however, if they are very good procedural programmers it can be tough to have them see value.

That said, I have found teaching with an "objects first" approach in Python to be helpful. Generally, one semester of CS leaves them less capable than a functions first approach. But in a second semester they pick up faster and are better off by the end. They are much better object oriented thinkers.

$\endgroup$
  • $\begingroup$ a card -> deck of cards (array of cards) -> blackjack shoe with 6 decks in it -> game and -> player's hand. Then have them implement a blackjack game, and add in things like the current card count, % chance of going bust if another card is drawn, etc. $\endgroup$ – ivanivan Dec 18 '17 at 0:32
  • $\begingroup$ The Deck class can lead to inheritance (ex. A Pile class, a Deck, a Hand) But I think it leads to simple discussions like "who's job is it to turn a card face up?" $\endgroup$ – TooManyCooks Dec 18 '17 at 1:17
  • 1
    $\begingroup$ Welcome to Computer Science Educators! I hope we hear more from you in the future. $\endgroup$ – Ben I. Dec 18 '17 at 2:33
1
$\begingroup$

This example was suggested to me by Ward Cunningham (of wiki fame). He and his son turned their home into a dungeon game. It had three kinds of objects: Rooms, Actors, and Things. The things could be found in a Room and carried by an Actor. Things had some behavior that could be employed by the Actor. Actors could move between rooms.

One "interesting" thing is a Transporter. A transporter can be found somewhere and "pickedUp". In some other room (or maybe the same) it can be "activated". Later on, if the Transporter is again "activated" it immediately transports the Actor (and maybe her party) back to the room in which it was first activated.

I find this object interesting since if you don't care about polymorphism you just set a firstUse flag and then query it later to see which action happens at "activate". However, it has a much more interesting solution. If the action is represented by a Strategy object instead of a flag, the Strategy determines what will happen at activation. The Transporter has-a Strategy. The currentStrategy can be swapped for another, defining a different action.

Note that this teaches both composition as well as polymorphism, but doesn't need inheritance other than from an interface.

You can also modify the behavior of a Strategy using the Decorator Pattern, again without inheritance, but emphasizing composition. A Decorator of Strategies both is-a Strategy and it has-a Strategy that it employs. The concepts here are simple, deep, and applicable in other places. In particular repeated "decoration" is normally stack-like, itself a useful concept.

Of course, students as well as yourself can come up with other kinds of Things that might be fun to work with. Note the similarity to Lego.

Refs Strategy Design Pattern, Decorator Pattern. BTW, Decorators have uses other than strategy-decorators. It is an independent concept.

I would also suggest that you find a way to tailor your Actors via composition, rather than by inheritance. Strategies can be used for this also. Different actors need not be defined by subclasses, but by objects they "carry" and to which their actions are deferred. For example an immovable-actor can have its "move" method deferred to a no-op object. A "flying-actor" to a ... etc. Lego, again.

$\endgroup$
1
$\begingroup$

This answer will be a bit long, but will give a moderately sophisticated view of OO programming and how to think about it. It is unlikely that focusing specifically on inheritance will be very fruitful for your students. Inheritance is often overused and used in poor ways.

The example here is adapted, with permission, from Joseph Bergin's Polymorphism Companion which is, itself a follow up to his Polymorphism: As It Is Played. The program in the book, and here, is a simple simulation of disease spread within a population. The version here has removed a bit of the complexity, but at the expense of some flexibility in exploring the concept of disease spread.

There will be eight separate files, two interfaces, two enumerations, and four classes, one of which just explores the value of using interfaces in the first place. The only inheritance is that of implementing an interface.

The more important lesson (than inheritance) for a beginner is to learn encapsulation and building objects by composition. OO can fruitfully be thought of as language features for enabling composition. The various implementations of Population are the chief place in this example that illustrate it.

The fundamental concept here is that a Population consists of cells of some kind (a generic type parameter in the original, but omitted here). and a Disease spreads from an infected cell to adjacent cells (and otherwise) depending on the characteristics of the disease and the population. In this simplification, the population substrate is a rectangular arrangement of cells (a two dimensional array), though extensions can, for example, use hexagonal cells or other shapes.

Note that interfaces are good for defining concepts in a programming system without reference to any implementation. The Java libraries define Set, for example, as an interface, more or less representing a mathematician's concept of a Set. There are various implementations which have different efficiencies, depending on the use.

The first Interface here is Disease:

package buffy.disease;

/** A disease that may be introduced into a population
 * @author buffy (derived from work of J Bergin, with permission)
 *
 */
public interface Disease {
    /** Set the probability that contact with an infected "location" will result in infection.
     * @param probability the probability of infection in range [0.0, 1.0]
     */
    public void setInfectionProbability(double probability);

    /** The current probability of infection
     * @return the probability that contact with a location will result in infection.
     */
    public double infectionProbability();


    /** Some diseases transform over time. This permits signaling the disease that it 
     * should update itself.
     * @param value value needed to drive the change
     */
    public void update(Integer value);
}

An infected cell can infect adjacent cells in an iteration of the simulation with a probability determined by the disease. Diseases can morph over time (or space, or other dimensions in general), so an update method is provided to allow this. Later we will see two implementation of this: SimpleDisease and VirulentDisease.

The second interface is Population:

package buffy.disease;

import java.util.Set;
import java.awt.Point;


/** A population into which a disease may be introduced to study how it spreads. A population has a "shape" which helps
 * determine how a disease spreads. A population may be mobile or not. If it is mobile, a 
 * disease may jump over distance,
 * otherwise it spreads only locally. In this model the state of a population location is 
 * either immune, infected, or 
 * available for infection (healthy). An extension might include recovered as a state. An 
 * immune barrier is a set of 
 * cells that might separate the population into regions. A disease can't spread across an 
 * immune barrier, but might
 * jump it if the mobility > 0. Normally a disease can spread only to adjacent locations. 
 * @author buffy (derived from work of J Bergin, with permission)
 *
 * @param <P> a "point" or "location" within the population
 * @param <D> a "direction", usually from a location
 */
public interface Population {

    /** A logical name for the population, perhaps its shape
     * @return the given name
     */
    public String name();

    /** The number of cells in the population
     * @return the number of cells
     */
    public int size();

    /** Try to infect this location with the probability associated with the disease
     * @param location the location to be (possibly) infected
     */
    public void infect(Point location);

    /** Introduce a disease into a random location of a population
     * @param <T> a change measure such as time
     * @param disease the disease to be introduced
     */
    public void introduce(Disease disease);

    /** The current number of infected cells
     * @return the number of infected cells
     */
    public int numberInfected();

    /** How many cells are neither infected nor immune
     * @return the number of cells available to be infected
     */
    public int numberHealthy();

    /** How many cells have been immunized
     * @return the number of immunized cells
     */
    public int numberImmune();

    /** Spread the disease for a certain number of cycles. On a cycle an infected cell will attempt
     * to infect its neighbors
     * @param cycles the number of cycles to run the simulation
     */
    public void spread(int cycles);

    /** Immunize a proportion of the population against the disease (assumes only one disease)
     * @param percentage the probability a given cell is immunized. 
     * @return the number actually immunized
     */
    public int immunize(double percentage);

    /** Permits members of a cell that visit, and possibly infect, other random cells 
     * in the population
     * @param howMany the number of cells to "travel" and thus spread to the destination, 
     * or get infected there. 
     */
    public void shuffle(int howMany);

    /** Create an immunization barrier in the population in a given direction starting at a given location. If 
     * the population wraps back on itself the barrier will also, back to the location. 
     * Otherwise it will extend to the
     * "edge" of the population. This can be used to create "islands" in the population. 
     * @param direction The direction to which the barrier extends
     * @param location the end of the barrier
     * @param distance the length of the barrier. If 0 it extends to the edge of the 
     * population (or wraps around)
     */
    public void immunizeFrom(Point location, Direction direction, int distance);

    /** Once each cycle, spread invokes shuffle. The mobility value determines how many 
     * members of the population visit
     * other cells. Set this to 0 to avoid "travel".  
     * @param mobility the number of population cells that visit other cells per cycle in spread. 
     */
    public void setMobility(int mobility);

    /** The currently infected cells
     * @return a set of infected cells
     */
    public Set<Point> infected();

    /** Determine if a location is infected
     * @param location the location to check
     * @return true if the location is infected
     */
    public boolean isInfected(Point location);

    /** Determine if a location is immune to infection
     * @param location the location to check
     * @return true if the location is immune
     */
    public boolean isImmune(Point location);

    /** Determine if the location is available for infection
     * @param location the location to check
     * @return true if the location is available for infection
     */
    public boolean isHealthy(Point location);

    /** A random location within the population
     * @return a random location
     */
    public Point randomLocation();

    /** Spread the disease from a given location for one "time" cycle
     * @param location the lodation from which to spread
     */
    public void spreadFrom(Point location);

    /** Is this location within the population
     * @param location a location to test
     * @return true if the location is within the location
     */
    public boolean includes(Point location);

    /** For an arbitrary location return one within the location
     * This is usually used to "wrap" a physical model into the desired "logical" shape
     * @param trial an arbitrary location, perhaps outside the population
     * @return a location within the population, logically "near" the trial location.
     */
    public Point reflect(Point trial);
}

Note that we can introduce only one disease into a population (extensions can be built, of course) but the particular kind of disease is independent of the population. This is the beauty and usefulness of using interfaces here. To explore how a different disease spreads in a given population you don't need to re-write any existing code, but just build a new implementation of the Disease interface.

It is possible, with this Population concept to introduce barriers to spread of disease, much as an ocean serves as a barrier to the spread of natural diseases. However, infection can also spread at a distance, perhaps across barriers if the "cells" of the population move (shuffle) or visit other locations, much as air travel has reduced the safety of isolation for such diseases as Ebola.

The simulation also has two Enumerations, Direction and State. The Direction is used to help in setting up barriers.

package buffy.disease;

/** The directions in which a disease might spread (for example) or in 
 * which an immune barrier is extended
 * @author buffy (derived from work of J Bergin, with permission)
 *
 */
public enum Direction {
    NORTH,
    EAST,
    SOUTH,
    WEST;
}

In the original the Direction is actually a generic parameter, permitting directions other than the four cardinal directions. That is one of the simplifications here.

The other enumeration gives the state of a cell at a given time:

package buffy.disease;

/** The state of a cell in a population and its visual representation in an ascii display
 * This might be extended with RECOVERED. 
 * @author buffy (derived from work of J Bergin, with permission)
 *
 */
public enum State{
    IMMUNE(" - "), 
    INFECTED(" + "), 
    HEALTHY(" . ");

    private String symbol;

    State(String symbol){
        this.symbol = symbol;
    }

    public String toString(){
        return symbol;
    }

}

Initially in a test run all cells are either healthy or immune. They may become infected as the simulation runs, but we won't show recovery here, though it is an obvious extension.

Next we will see two examples of diseases, neither very sophisticated. A simple disease does not morph over the run of a simulation.

package buffy.disease;

public class SimpleDisease implements Disease {

    private double probability = 0.0d;

    public SimpleDisease(double initialProbability){
        this.probability = initialProbability;
    }

    public SimpleDisease(){
        //nothing
    }

    @Override
    public void setInfectionProbability(double probability) {
        probability =  Math.min(1.0, probability);
        this.probability = Math.max(0, probability);
    }

    @Override
    public double infectionProbability() {
        return probability;
    }

    @Override
    public void update(Integer value) {
        //nothing
    }    

}

Note a couple of things about this class. First it isn't very interesting since it has no methods representing actions of any kind. It is just an information carrier. Note that it is, however completely encapsulated. It has two mutators (setInfectionProbability, and update) and an accessor for the infection probability. This is not completely desirable, since in order to do anything with a disease you have to retrieve information from it and then carry out the action elsewhere in the program. It is preferable, if you can arrange it, to have objects actually carry out actions on a client's behalf rather than giving out information and having the client do the action, hoping that the client does the right thing with the information. This is the "Tell, don't ask" principle, which is important, but is not exhibited here.

Note also, that the probability set in the mutator is "normalized" to assure that it is indeed a valid probability in the range 0.0 .. 1.0.

Also, when a method body is intentionally left empty it is documented with a comment so that a future reader needn't wonder whether the original programmer just forgot something.

A VirulentDisease is similar, but has an interesting update method:

package buffy.disease;

/** A virulent disease is characterized by an increase in the liklihood of infection as
 * the simulation proceeds. 
 * @author buffy (derived from work of J Bergin, with permission)
 *
 */
public class VirulentDisease implements Disease {
    private double probability = 0.0d;


    public VirulentDisease(double initialProbability){
        this.probability = initialProbability;
        normalize();
    }

    public VirulentDisease(){
        //nothing
    }

    private void normalize(){
        probability =  Math.min(1.0, probability);
        this.probability = Math.max(0, probability);
    }

    @Override
    public void setInfectionProbability(double probability) {
        this.probability = probability;
        normalize();
    }

    @Override
    public double infectionProbability() {
        return probability;
    }

    @Override
    public void update(Integer value) {
        value = Math.min(10, value);
        probability += (1.0 - probability) / Math.max(1, 11 - value);
        normalize();
    }    
}

Note here that we need to assure that the normalize method can never be overridden, since it is invoked from a constructor. It is private here so there is no issue, but if it can't be private it must be at least final otherwise incorrect things can occur in the construction of an object.

Very importantly, note that the two disease classes have only public methods defined in the interface. It is improper and leads to gnarly code to have a subclass or an implementing class that actually extends the public interface of its parent. In that case future programmers need to know too much (and keep in mind too often) all of the intermediate classes and the specific types of the variables in a program.

The simulation can have various kinds of populations as well as diseases. We will only show one here: a square bounded world.

package buffy.disease;

import java.awt.Point;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/** Create a square-array based population. It does not wrap around, but 
 * represents a finite square plane of cells. 
 * It represents a finite rectangular planar world. Diseases are stopped at the edges
 * @author buffy (derived from work of J Bergin, with permission)
 *
 */
public class BoundedSquarePopulation implements Population {

    private int width = 0;
    private State [][] population = null;
    private Random random = new Random();
    private Disease disease = null;
    private Set<Point> infected = new HashSet<Point>();
    private int[] range = null;
    private int shuffleNumber = 0;
    private String name = "Planar";

    /** Create a planar world
     * @param width the width (and height) of the world. 
     */
    public BoundedSquarePopulation(int width){
        this.width = width;
        this.population = new State[width][width];
        this.range = new int[width];
        for(int i = 0; i < width; ++i){
            this.range[i] = i;
        }
        for(int i : this.range) {
            for(int j : this.range){
                this.population[i][j] = State.HEALTHY;
            }
        }
    }

    public BoundedSquarePopulation( int width, String name){
        this(width);
        this.name = name;
    }

    @Override
    public String name(){
        return this.name;
    }

    @Override
    public  Point randomLocation(){
        return new Point(this.random.nextInt(this.width) , this.random.nextInt(this.width));
    }

    /** Guarantee a point is within the population
     * @param trial a suggested point
     * @return trial is returned, possibly modified
     */
    @Override
    public  Point reflect(Point trial){
        trial.x = Math.abs(trial.x) % this.width;
        trial.y = Math.abs(trial.y) % this.width;
        return trial;
    }

    @Override
    public  boolean includes(Point location){
        return location.x >= 0 && location.y >= 0 && 
            location.x < this.width && 
            location.y < this.width;
    }

    @Override
    public  void spreadFrom(Point location){
        Point temp = new Point(0, 0);
        for(int i = location.x - 1; i < location.x + 2; ++i){
            for(int j = location.y - 1; j < location.y + 2; ++j){
                temp.move(i, j);
                infect(temp);
            }
        }
    }

    private double normalize(double percentage){
        percentage = Math.max(0, percentage); // normalize
        percentage = Math.min(1, percentage);
        return percentage;
    }

    @Override
    public String toString(){
        String result = "";
        for(int i : this.range){
            for(int j : this.range){
                result += this.population[j][i] + " ";
            }
        result += "\n";
        }
        return result;
    }

    @Override
    public boolean isInfected(Point location){
        return this.population[location.x][location.y] == State.INFECTED;
    }

    @Override
    public boolean isImmune(Point location){
        return this.population[location.x][location.y] == State.IMMUNE;
    }

    @Override
    public boolean isHealthy(Point location){
        return this.population[location.x][location.y] == State.HEALTHY;
    }

    @Override
    public void immunizeFrom(Point location, Direction direction, int distance){
        distance = Math.abs(distance);
        int endpoint;
        switch(direction){
        case NORTH: {
            endpoint = distance == 0? 0 : Math.max(0, location.y - distance + 1);
            for(int i = endpoint; i <= location.y; ++i){
                if(this.population[location.x][i] == State.HEALTHY) {
                    this.population[location.x][i] = State.IMMUNE;
                }
            }
        }
        break;
        case EAST: {
            endpoint = distance == 0? this.width : Math.min(this.width, location.x + distance );
            for(int i = location.x; i < endpoint; ++i){
                if(this.population[i][location.y] == State.HEALTHY) {
                    this.population[i][location.y] = State.IMMUNE;
                }
            }
        }
        break;
        case SOUTH: {
            endpoint = distance == 0? this.width: Math.min(this.width, location.y + distance );
            for(int i = location.y; i < endpoint; ++i){
                if(this.population[location.x][i] == State.HEALTHY) {
                    this.population[location.x][i] = State.IMMUNE;
                }
            }
        }
        break;
        case WEST: {
            endpoint = distance == 0? 0 : Math.max(0, location.x - distance + 1);
            for(int i = endpoint; i <= location.x; ++i){
                if(this.population[i][location.y] == State.HEALTHY) {
                    this.population[i][location.y] = State.IMMUNE;
                }
            }
        }
        break;
        }
    }

    @Override
    public int immunize(double percentage){
        percentage = normalize(percentage); // normalize
        int result = 0;
        for(int i: this.range){
            for(int j: this.range){     
                if(this.population[i][j] == State.HEALTHY && this.random.nextDouble() < percentage){
                    this.population[i][j] = State.IMMUNE;
                    result++;
                }
            }
        }
        return result;
    }

    @Override
    public void infect(Point location){
        if(includes(location) && isHealthy(location) && 
            this.random.nextDouble() < this.disease.infectionProbability()){
            this.population[location.x][location.y] = State.INFECTED;
            this.infected.add((Point)location.clone());
            System.out.println("infecting: " + location);
        }
    }

    @Override
    public int size() {
        return this.width * this.width;
    }

    @Override
    public  void introduce(Disease disease) {
        this.disease =  disease;
        Point p = randomLocation();
        System.out.println("Introducing disease at: " + p);
        infect(p);  
    }

    @Override
    public int numberInfected() {
        return this.infected.size();
    }

    @Override
    public int numberHealthy(){
        int result = 0;
        for(int i : this.range){
            for(int j : this.range){
                if(this.population[i][j] == State.HEALTHY){
                    result++;
                }
            }           
        }
        return result;
    }

    @Override
    public int numberImmune(){
        int result = 0;
        for(int i : this.range){
            for(int j : this.range){
                if(this.population[i][j] == State.IMMUNE){
                    result++;
                }
            }           
        }
        return result;
    }

    @Override
    public void spread(int cycles) {
        for(int i = 0; i < cycles; i++){
            @SuppressWarnings("unchecked")
            HashSet<Point> temp = (HashSet<Point>)((HashSet<Point>) this.infected).clone();
            for(Point p:temp){
                shuffle(this.shuffleNumber);
                spreadFrom(p);
            }
        }
    }

    @Override
    public void shuffle(int howMany) {
        for(int i = 0; i < howMany; ++i){
            Point from = randomLocation();
            Point to = randomLocation();
            if(isImmune(from) || isImmune(to)){
                return;
            }
            if(isInfected(from)){
                infect(to);
            }
            if(isInfected(to)){
                infect(from);
            }
        }
    }

    @Override
    public void setMobility(int mobility) {
        mobility = Math.max(0, mobility);
        this.shuffleNumber = mobility;
    }

    @SuppressWarnings("unchecked")
    @Override
    public Set<Point> infected() {
        return (Set<Point>) ((HashSet<Point>)this.infected).clone();
    }

}

It is a bit long, but note that, again, all of its public methods are defined in the Population interface.

This class is interesting and exhibits better OO characteristics than the disease classes. It is built from composition, both of library types like set and bespoke types like Disease.

This class is also a better example of "Tell, don't Ask" in, for example the immunize and spreadFrom methods. The accessors are mostly used for reporting out the results of the simulation, not for enabling computation to be done elsewhere.

Finally, here is a main that can be used to run a sample of the simulation.

package buffy.disease;

import java.awt.Point;

public class TrialRun {

    public static void  main(String[] args){
        Population population = new BoundedSquarePopulation(15);
        population.setMobility(3);
        Point where = new Point(10, 5);
        population.immunizeFrom(where, Direction.NORTH, 0);
        Disease disease = new SimpleDisease();
        disease.setInfectionProbability(0.6);
        population.introduce(disease);
        population.spread(3);
        System.out.println("Number infected: " + population.numberInfected() + "/" +
             population.size());
        System.out.println(population.toString());
        System.out.println("Number available but not infected: " + 
            population.numberHealthy() + "/" + population.size());
        System.out.println("Number immunized: " + population.numberImmune() + "/" +
             population.size());
    }    
}

This just uses ASCII graphics to show the final result of a test run, but the other classes and interfaces could, instead, serve as a Model in a Model-View-Controller graphical program that shows the results of infection spread as they occur in a graphical application.


Not shown here is the possibility of creating alternative world (population) geometries. For example a linear (single dimension array) is easy to do. You can also create a world as a Möbius band by "connecting" the left and right edges with reversed orientation so that a disease spreading off the left-top will spread into the right-bottom. Other connections are possible, creating such things as a torus, sphere, tube, etc. Combining that with immunity barriers can yield fairly realistic simulations.

$\endgroup$
1
$\begingroup$

I would give them labs that can later be reused for polymorphism. For example, introduce class signatures for Apple,Banana,Cake,Dinner. Then you say that they need to implement a system of a Dinner:

public class Dinner{
    //store the banana objects and apple objects 
    //        (you can say that an array is useful here, as the dinner can't contain 20,000 apples.
    //and also a collection of cake objects

    ...
}

And ask them to implement a system which allows the creation (which means they would have to write a constructor) of a Dinner. Then they should write methods for the Dinner object, so that one can add apples, bananas and cakes to the diner (this can be tricky; they'd need to keep an index for the arrays) and methods for when a customer orders a cake etc.

This assignment covers objects, including composition (no pun intended) and methods etc.

$\endgroup$
0
$\begingroup$

Kevin Wayne at Princeton came up with a "GuitarHeroine" assignment which uses maybe a little too much of these topics. The version that Stuart Reges uses at UW CSE only requires interfaces.

$\endgroup$
  • 1
    $\begingroup$ This answer would be improved if it had more of a synopsys of the linked material. Reading it without following the links (or if the links die), there is not a lot to go in. $\endgroup$ – Sean Houlihane Jun 11 '17 at 9:24
  • $\begingroup$ @adamblan, Could you give any more details about this assignment? $\endgroup$ – Ben I. Jul 31 '17 at 2:53

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.