Lesson Wednesday

At this point we can create our own classes, integrate them into a Spark application, and dynamically create and display content. Another powerful aspect of object-oriented programming is the ability to use one object inside an instance of another object. Constructing objects in this manner is appropriately referred to as "objects within objects".

In this lesson, we'll create an application that demonstrates the implementation and usage of objects within objects. Please code along and check your work the finished product, but know that we won’t be integrating this new functionality into our ongoing application until next week.

Objects Within Objects

Believe it or not, we've already been saving objects inside of other objects for awhile! For example, pretend we have an ArrayList called names:

List<String> names = new ArrayList();

As you know, we can add content to an ArrayList like this:

names.add("Samantha");

Here, "Samantha" is also an object; it's an instance of the String class. So, by adding "Samantha" to names we're saving a String object within our ArrayList object. There it is, objects within objects! Here’s another example:

String apple = “apple”;
String pear =”pear”;
String cherry = “cherry”;

String[] fruits = [apple, pear, cherry];

Objects within Objects Using Custom Classes

When we create custom classes we're storing objects within objects too! Remember, everything in Java (besides primitives!) is an object. So, when we create a Post object in our Blog, then save a String as its content property, we're storing an object inside another object!

So far, whenever we've saved objects within objects, at least one of two objects has belonged to a built-in Java class like String or ArrayList. However, we can just as easily store objects from our custom classes within an objects of another class we define ourselves. We’ll also integrate this into our Blog next week, so we can use separate classes like Comment along with Post, but let’s explore this topic in a simpler way first.

Objects within Objects Using Rectangles

Get started by forking the following repo: MyRectangles to your GitHub account, then cloning it back down so you can make changes. Begin with the first commit.

Currently, our Rectangles program creates a Rectangle object using user-inputted values. These values represent the height and width of the Rectangle. As you know, we can also write methods to check whether a Rectangle is also a square (ie: If their height and width values are equal).

In this lesson we'll add another feature: If a rectangle is a square, we will create a Cube object using its dimensions, then return the volume and surface area of that particular Cube. This means our app will use two classes we define ourselves: Rectangle and Cube.

Testing Objects within Objects

As always, we'll write the necessary back-end code for this features before implementing it into our user interface. And, we'll use BDD to do so. You may also want to add a .gitignore file to your Rectangles project at this point.

We'll begin by creating a spec file for our new class and writing a test to confirm we can successfully create instances of it.

Make a new Cube.java class, then, generate a new test file by using the shortcut shift + alt + t. We don’t need a setUp() or tearDown() method here, so no need to create those.

Now we should have a Cube.java class in src/main/java and a CubeTest.java file in src/test/java.

As discussed previously, when testing objects, the most simple behavior is usually confirming we can successfully instantiate an object of that class. So, we'll generate a test for our Cube constructor and make it look like this:

rectangles/src/test/java/CubeTest.java
import org.junit.*;
import static org.junit.Assert.*;

public class CubeTest {

  @Test
  public void newCube_instantiatesCorrectly() throws Exception {
    Rectangle testRectangle = new Rectangle(30, 30);
    Cube testCube = new Cube(testRectangle);
    assertEquals(true, testCube instanceof Cube);
  }

}

We have some red squiggly lines, so we need to rewrite things in our Cube file still.

Objects as Arguments

Now, you probably noticed something new in the test above. Instead of passing in two numerical values as arguments, like we did when creating Rectangles, we're providing a single Rectangle to the Cube constructor.

Remember what our application is supposed to do: It will take two user-provided values and create a Rectangle out of them. Then, if the Rectangle is also a square (as determined by the shape property that is the result of a call to our isRectSquare() method), we will create a Cube object using its measurements. Then, we'll return the Cube's volume and surface area to the user.

Geometrically speaking, a cube is simply a three-dimensional square. Since our Rectangles already contain height and width values, we can pass in the entire Rectangle object instead of providing individual height and width measurements.

Remember, Rectangle is just another type of object, much like String or Integerobjects we have been providing to constructors for nearly two weeks already.

Now, we should be able to declare a property to contain the Cube's corresponding Rectangle, and a constructor to confirm our Cube objects instantiate correctly:

rectangle/src/main/java/Cube.java
public class Cube {
private Rectangle face;

public Cube(Rectangle rectangle) {
   face = rectangle;
}

}

Now our test that checks whether the object is instantiated correctly should pass! Cool.

Next, let's make sure each Cube is correctly saving the Rectangle object we provide as a property. We'll write a test to confirm a getter method called getFace() can successfully retrieve the Rectangle we passed into the Cube's constructor:

rectangle/src/test/java/CubeTest.java
...
  @Test
  public void newCube_savesRectangleInformation_Rectangle() throws Exception {
    Rectangle testRectangle = new Rectangle(30, 30);
    Cube testCube = new Cube(testRectangle);
    assertEquals(testRectangle, testCube.getFace());
  }
...

After confirming it fails, we can add code to pass this second test:

rectangle/src/main/java/Cube.java
public class Cube {

   private Rectangle face;

   public Cube(Rectangle rectangle) {
       face = rectangle;
   }

   public Rectangle getFace() {
       return face;
   }
}

  • We initialize the Cube with a Rectangle object that represents one face of the three-dimensional Cube.
  • We set the object equal to the Cube's face property.
  • Then, we define a getter method called getFace() that returns the Rectangle object associated with our Cube.

Calculating Volume

Next, let's determine the volume of our Cubes. As always, we'll begin with a test:

rectangle/src/test/java/CubeTest.java
...
 @Test
  public void volume_determinesTheVolumeOfTheCube_27000() throws Exception {
    Rectangle testRectangle = new Rectangle(30, 30);
    Cube testCube = new Cube(testRectangle);
    assertEquals(27000, testCube.volume());
  }
... 

After confirming it fails, we can add logic for our new volume() method:

rectangle/src/main/java/Cube.java
public class Cube {
  private Rectangle face;

  public Cube(Rectangle rectangle) {
    face = rectangle;
  }

  public Rectangle getFace() {
    return face; 
  }

  public int getVolume() {
    int height = face.getHeight();
    return height * height * height;
  }
}

A Cube can call Rectangle public methods. No squiggly lines! That's what declaring something public means; it's accessible to outside classes.

So, in our Cube's volume() method we are able to call the Rectangle class' getHeight() method on face (Remember, face is just a Cube attribute that stores a Rectangle object). Calling this method retrieves the height of the Rectangle. That is, the height of one face of our Cube.

We can multiply height by itself 3 times to determine the Cube's volume. (We don't call both getHeight() _and _ getWidth() because the height and width of a square are the same!)

This may sound confusing; but don't let it intimidate you! This is no different than being able to call built-in String or ArrayList methods in the method of a custom class. The only difference is that here, both classes are custom.

Calculating Surface Area

Now, let's write a method to determine the surface area of our Cube. Here's the spec:

rectangle/src/test/java/CubeTest.java
…
@Test
public void surfaceArea_determinesTheSurfaceAreaOfACube_5400() throws Exception{
  Rectangle testRectangle = new Rectangle(30, 30);
  Cube testCube = new Cube(testRectangle);
  assertEquals(5400, testCube.getSurfaceArea());
}
...

Once we confirm it fails, we can create the method to make it pass:

rectangle/src/main/java/Cube.java
...
public int getSurfaceArea() {
  int surfaceArea = face.getHeight() * face.getWidth(); 
  return surfaceArea * 6;
}
...

To calculate the surface area of a Cube, multiply the area of one face by 6 (because a three-dimensional Cube is composed of 6 identical faces). After adding this logic, we should be able to run our tests and see them all pass.

If you get stuck or lost, check this repo before you proceed against your own work.

Refactor

However, don't forget that refactoring is a very important step in the Red, Green, Refactor workflow we'll continue to follow closely for the rest of the course. And, this time around, we can definitely refactor.

You see, we could add logic to calculate the area of the face Rectangle in the Cube class, as seen above; but that's not best practice. It makes more sense to determine the area of a Rectangle within the Rectangle class itself. Even though the Cube will utilize this value for its own calculations, the area of a Rectangle is more specific to the Rectangle class than it is the Cube class.

We'll refactor getSurfaceArea() to look like this:

rectangle/src/main/java/Cube.java
...
public int getSurfaceArea() {
  return face.area() * 6;
}
...

Now, if we run our specs again, we receive an error: not a statement message from the Java compiler. This is because we haven't created a Rectangle method named area().

But, before we do, we'll need to write spec for our Rectangle class, too:

rectangle/src/test/java/RectangleTest.java
...
@Test
public void area_returnsTheAreaOfTheRectangle_450() throws Exception{
  Rectangle testRectangle = new Rectangle(15, 30);
  assertEquals(450, testRectangle.area());
}
...

Then, we can write the area() method to pass this test:

rectangle/src/main/java/Rectangle.java
...
public int getArea() {
  return height * width;
}
...

Now we can run our tests to make sure the area() and getSurfaceArea() specs are passing.

Great work! We just created the entire back-end of an application using objects within objects!

Spark User Interface

Now let's edit the front-end user interface for our application.

We already have a basic front-end in place - let’s move to accepting user inputs for our rectangle dimensions instead of hardcoding dummy data in our App.java like we did here:

rectangle/src/main/java/App.java
...
get("/", (req, res) -> {
   //just for testing - make two new objects so we have something to retrieve
   Rectangle rectangle = new Rectangle(3,2);
   Rectangle squareRectangle = new Rectangle(12, 12);
...

Let’s begin by adding a form to the index.hbs like we did with our blog.

rectangle/src/main/resources/templates/index.hbs
<form action="/rectangles/new" method="post">
   <p> Calc My Rec </p>
   <label for="height">Height</label>
   <input id="height" name="height" type="text">

   <label for="width">Width</label>
   <input id="width" name="width" type="text">

   <button type="submit" class="btn btn-default">Go!</button>
</form>

{{#each myRectangles }}
 <p><h3>Your rectangle's height is {{ height }}</h3></p>
 <p><h3>Your rectangle's width is {{ width }}</h3></p>

   {{#if shape }}
     <p><h3>Your rectangle is a square!</h3></p>
  {{else}}
     <p><h3>Your rectangle isn't a square!</h3></p>
  {{/if}}
{{/each }}

We should be able to launch the application and see our new form in the browser. As you can see, we set the form's action attribute to /rectangles/new and the method to POST. So when this form is submitted Spark will automatically navigate to the /rectangles/new route. Let's create this route next:

rectangle/src/main/java/App.java
...
post("/rectangles/new", (req, res) -> {
   Map<String, ArrayList<Rectangle>> model = new HashMap<>();
   int height = Integer.parseInt(req.queryParams("height"));
   int width = Integer.parseInt(req.queryParams("width"));
   Rectangle myRectangle = new Rectangle(height, width); //as we know this will automatically add itself to a list of all rectangles.
   res.redirect("/"); //send user to root route. Cool eh!
   return null; //gotta send back something otherwise we get an error.
});
...

Here, we save the information provided by the user into int height and int width variables.

We instantiate a Rectangle object with the height and width variables we just created. When it saves, it automatically adds itself to a list of all rectangles.

Let’s send the user to a new page where they can see all of the information they’ve been excited to see about their awesome rectangle, instead of seeing everything on the main page. There, we can show them Rectangle and Cube information.

Let’s make a new rectangle-detail.hbs file and use it when we change our route one more time.

If the user's Rectangle is a square we want to create a Cube object out of it, and return the surface area and volume of said Cube. We'll include additional logic in our /rectangles/new route to handle this:

rectangle/src/main/java/App.java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import spark.ModelAndView;
import spark.template.handlebars.HandlebarsTemplateEngine;

import static spark.Spark.get;
import static spark.Spark.post;

public class App {
   public static void main(String[] args) {

       get("/", (req, res) -> {
           Map<String, ArrayList<Rectangle>> model = new HashMap<>();
           ArrayList myRectangleArrayList = Rectangle.getAll();
           model.put("myRectangles", myRectangleArrayList);
           return new ModelAndView(model, "index.hbs");
       }, new HandlebarsTemplateEngine());

       post("/rectangles/new", (req, res) -> {
           Map<String, Object> model = new HashMap<>(); // I may be different from your code to account for generic “Objects”
           int height = Integer.parseInt(req.queryParams("height"));
           int width = Integer.parseInt(req.queryParams("width"));
           Rectangle myRectangle = new Rectangle(height, width); //as we know this will automatically add itself to a list of all rectangles.
           model.put("myRectangle", myRectangle); //don’t forget me!

           if (myRectangle.getShape()) {
               Cube myCube = new Cube(myRectangle);
               model.put("myCube", myCube);
           }
           return new ModelAndView(model, "rectangle-detail.hbs"); //render a detail page instead of redirecting to home.
       }, new HandlebarsTemplateEngine());
   }
}

Let’s add code from our index page to rectangle-detail.hbs and change a few things:

Here, we add an if statement to the "/rectangles/new" route. It checks if the new instance of Rectangle is a square. If so, we create a Cube, initialize it with the Rectangle object, and add our new Cube to model.

Let's update our rectangle-detail.hbs template to display information about the new Cube:

rectangle/src/main/resources/templates/rectangle-detail.hbs
<h2>Exciting stats on your rectangle! </h2>

{{#if myRectangle }}
   <p><h3>Your rectangle's height is {{ myRectangle.height }}.</h3></p>
   <p><h3>Your rectangle's width is {{ myRectangle.width }}.</h3></p>

   {{#if myRectangle.shape}}
       <p><h3>Your rectangle is a square.</h3></p>
       {{myCube.surfaceArea}} units surface area and <br>
       {{myCube.volume}} units volume.
   {{else}}
       <p><h3>Your rectangle isn't a square, sorry.</h3></p>
   {{/if}}

{{else}}
   <p><h3>No details found. </h3></p>
{{/if }}

We have added in two lines after <h3> Your rectangle is a square!</h3>. They call the surfaceArea() and volume() methods on our Cube. They aren’t getters in a traditional sense, but remember that Handlebars can only execute methods in templates if they are prefixed with the word “get”.

We can ignite our Spark and see it in action!


Example GitHub Repo for Rectangles at this point in time