6. Interfaces Lesson

Introduction to Interfaces

Introduction to Interfaces

Note

Activity objective: Show the benefits of interfaces at a high level. We want students to consider when it is appropriate to use an interface.

Interfaces in the Real World

If we swap out one specific device / object with another that shares the same interface, we can still use the new device even though it likely works differently. We are able to do this because we are familiar with the interface:

  • Motor Vehicles and Humans

  • Phone Dial Pads / Screens and Humans

  • Remote Controls and Humans

The same is true when two hardware devices need to interact:

  • ATMs and Debit Cards

  • Computers and USB Devices

  • etc.

This same idea can be applied to a software system (or systems) to let different parts interact with each other.

Interfaces in Software

Scenario: Disparate Classes, Common Action

We have Java code that uses an object of a particular class to perform some action or actions using the object, and we need to update the code to also be able to use an object of some other class to perform the same kind of action or actions using that object. The classes for these objects each perform the action in their own way.

Goal:

Setup our code so that whenever it switches between using these objects to perform the action, the code outside those object’s classes does not need to be updated.

Java Interfaces:

An interface makes classes plug an play (polymorphism). Just like if you swap out your car, you don’t want to have to relearn how to drive.

Interfaces allow us to remove dependencies between classes and make our code easier to update in the future.

When to Use:

Multiple disparate classes that share a common action but are otherwise unrelated (disparate) are good candidates for this technique.

Tip

Think of the common action as a high level action that will work differently on different devices.

For example: A car “moves forward” (high level action) when you press the gas pedal. However, older cars work differently from new cars, electric cars work differently from gas cars, etc.

Group Activity
  1. Come up with an example involving two classes and a common action that meet the following criteria:

    • The classes are disparate (for this activity, let’s have NO overlap).

    • The classes contain a common action (method) that can be performed on either kind of object, even though the classes are disparate.

    • The common action should be a high level action. The action should not be done the same way for each class.

  2. Create an interface to describe the common action.

Deliverables:

  • A UML diagram that shows the two classes, the interface, and the relationships between all three (arrows).

  • A short justification describing how the implemention of the common action (method) is different but still accomplishes the common goal.

The Drawable Interface

The Drawable Interface

Note

Some parts of this activity contain solutions. To show the solutions, you will need to toggle “Show Solutions” above.

Note

Activity objective: Show the benefits of interfaces using an example we can refactor.

Step 1: Write out disparate classes along with the main method that calls a method to draw each (name the draw method differently). The draw methods will need to be in the driver class for the first step.

Step 2: Show how we can refactor the code and put the draw method in each class and then adjust the main method.

Step 3: Add an interface and show the resulting UML diagram. The classes/relationships have changed. However, the main method is so much cleaner and won’t need to be modified later.

Part 1: Exploring the Problem
UML - Version 1

Consider the UML Diagram below and answer the following on your exit tickets:

  1. Would you consider Tree, Airplane, and Person to be disparate classes?

  2. Do they share a common action?

  3. Is the common action implemented the exact same way?

!includesub lesson6.common.puml!STYLE
!includesub lesson6.before.puml!CLASSES_NO_DRAW

UML - Version 2

The common action “Draw” can be added to each class using an identical method signature:

!includesub lesson6.common.puml!STYLE
!includesub lesson6.before.puml!CLASSES_DRAW

UML - Version 3

Now, imagine we want to be able to draw many different objects of each type. We might create methods like this:

!includesub lesson6.common.puml!STYLE
!includesub lesson6.before.puml!CLASSES_DRAW

class Utility {
   + {static} drawTrees(trees: Tree[]): void
   + {static} drawAirplanes(planes: Airplane[]): void
   + {static} drawPeople(people: Person[]): void
}

Utility --> Tree : "dependsOn"
Utility --> Airplane : "dependsOn"
Utility --> Person : "dependsOn"

Discussion

Answer the following on your exit ticket:

Keeping the same structure as the existing code, what steps would we need to take to add another class, Flower, that can be drawn? Assume that the Flower class has two methods: drawStem and drawPetals and that you would need to call both to draw the entire flower. We also want a method that allows us to draw many flowers at once.

You can answer by describing what you would need to do or by drawing a UML diagram.

Post-Discussion UML

What is tedious/error-prone about this process? What if we needed to add hundreds of classes that can be drawn?

!includesub lesson6.common.puml!STYLE
!includesub lesson6.before.puml!CLASSES_DRAW

class Flower {
   - petalColor: Color
   - numberOfPetals: int
   + Flower(petalColor: Color, numberOfPetals: int)
   + bloom(): void
   + changeColor(newColor: Color): void
   + getPetalColor(): Color
   + getNumberOfPetals(): int
   + drawStem(): void
   + drawPetals(): void
   + draw(): void
}

class Utility {
   + {static} drawFlowers(flowers: Flower[]): void
   + {static} drawTrees(trees: Tree[]): void
   + {static} drawAirplanes(planes: Airplane[]): void
   + {static} drawPeople(people: Person[]): void
}

Utility --> Tree : "dependsOn"
Utility --> Airplane : "dependsOn"
Utility --> Person : "dependsOn"
Utility --> Flower : "dependsOn"

Redundant Code

Let’s take a minute to consider the methods inside of the Utility class. The drawFlowers method probably looks something like this:

public static void drawFlowers(Flower[] flowers) {
    for (Flower currentFlower: flowers) {
        currentFlower.draw();
    } // for
} // drawFlowers

On your exit tickets, answer the following questions:

  1. How is drawTrees different from drawFlowers?

  2. How is drawAirplanes different from drawFlowers?

  3. Is there redundancy between these methods?

Thinking Bigger

Having to add a method to Utility every time we add a new class (like Flower) is the equivalent of having to learn to drive all over again if you buy a new car. We have to do this because our classes do not have a common interface (even though they all contain the same method).

Two changes doesn’t seem like a big deal at first, but imagine a larger code base with more dependencies. You don’t want to be worried that every change you make may introduce bugs in other classes and you certainly don’t want to be checking all dependent code for potential issues.

Part 2: Incorporating the Interface
How do Interfaces help?

Remember the goals of using interfaces:

  • Implementing classes should be plug and play

  • Reduce dependencies

Question

Answer the following on your exit ticket:

  1. How could we incorporate an interface into this code?

  2. What would you call the new interface?

  3. What method(s) would need to be in the interface?

  4. Which classes would implement the interface?

UML Comparison

What is better about the version of this code that contains the interface? Write your answer on your exit ticket.

!includesub lesson6.common.puml!STYLE
!includesub lesson6.before.puml!CLASSES_DRAW

class Utility {
   + {static} drawTrees(trees: Tree[]): void
   + {static} drawAirplanes(planes: Airplane[]): void
   + {static} drawPeople(people: Person[]): void
}

Utility --> Tree : "dependsOn"
Utility --> Airplane : "dependsOn"
Utility --> Person : "dependsOn"

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"

Utility --> Drawable : "dependsOn"

Solution

There is only one dependency and fewer methods in Utility. The Utility class will now work with all types that implement Drawable.

Discussion

Question

Answer the question below on your exit ticket:

With the new structure that incorporates the Drawable interface, what would we need to do to add another class, Flower, that can be drawn?

Updated UML

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Flower {
   - petalColor: Color
   - numberOfPetals: int
   + Flower(petalColor: Color, numberOfPetals: int)
   + bloom(): void
   + changeColor(newColor: Color): void
   + getPetalColor(): Color
   + getNumberOfPetals(): int
   + drawStem(): void
   + drawPetals(): void
   + <<override>> draw(): void
}

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"
Drawable <|..down.. Flower : "implements"

Utility --> Drawable : "dependsOn"

Part 2.5: Memory Map: Calling the drawAll Method

On your exit tickets, write out the code snippet for main shown below, then draw a memory map that depicts what memory looks like after all the lines in the main method execute:

1public static void drawAll(Drawable[] objs) {
2    for (Drawable obj: objs) {
3        obj.draw();
4    } // for
5} // for
1public static void main(String[] args) {
2
3    Drawable[] objects = new Drawable[2];
4
5} // main
1public static void drawAll(Drawable[] objs) {
2    for (Drawable obj: objs) {
3        obj.draw();
4    } // for
5} // for
1public static void main(String[] args) {
2
3    Drawable[] objects = new Drawable[2];
4    objects[0] = new Tree(12, Color.GRAY);
5    objects[1] = new Person(Color.GREEN, Color.GRAY);
6
7} // main
1public static void drawAll(Drawable[] objs) {
2    for (Drawable obj: objs) {
3        obj.draw();
4    } // for
5} // for
 1public static void main(String[] args) {
 2
 3    Drawable[] objects = new Drawable[2];
 4    objects[0] = new Tree(12, Color.GRAY);
 5    objects[1] = new Person(Color.GREEN, Color.GRAY);
 6
 7    // What does memory look like when inside drawAll but
 8    // just before it executes its first line of code?
 9    Utility.drawAll(objects);
10
11} // main
1public static void drawAll(Drawable[] objs) {
2    for (Drawable obj: objs) {
3        obj.draw();
4    } // for
5} // for
 1public static void main(String[] args) {
 2
 3    Drawable[] objects = new Drawable[2];
 4    objects[0] = new Tree(12, Color.GRAY);
 5    objects[1] = new Person(Color.GREEN, Color.GRAY);
 6
 7    // What does memory look like when inside drawAll and
 8    // just before the call to draw() exeutes (first iteration)?
 9    Utility.drawAll(objects);
10
11} // main
  • What is the data type of the obj variable?

  • Is the value of obj equal to null?

  • If not null, what is the data type of the object that obj refers to?

  • What class defines the body of the draw() method being called via obj?

Part 3: Compatibility

Imagine the code in each group is found in the main method of a Driver class.

For each numbered block of code, write on your exit tickets whether or not the code will compile. Explain your answer for each.

Group 1

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Flower {
   - petalColor: Color
   - numberOfPetals: int
   + Flower(petalColor: Color, numberOfPetals: int)
   + bloom(): void
   + changeColor(newColor: Color): void
   + getPetalColor(): Color
   + getNumberOfPetals(): int
   + drawStem(): void
   + drawPetals(): void
   + <<override>> draw(): void
}

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"
Drawable <|..down.. Flower : "implements"

Utility --> Drawable : "dependsOn"

  1. Drawable d = new Person(Color.BLUE, Color.BLUE);
    d.draw();
    
  2. Drawable d = new Person(Color.BLUE, Color.BLUE);
    d.getEyeColor();
    
  3. Drawable d = new Person(Color.BLUE, Color.BLUE);
    Utility.drawIt(d);
    
Group 2

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Flower {
   - petalColor: Color
   - numberOfPetals: int
   + Flower(petalColor: Color, numberOfPetals: int)
   + bloom(): void
   + changeColor(newColor: Color): void
   + getPetalColor(): Color
   + getNumberOfPetals(): int
   + drawStem(): void
   + drawPetals(): void
   + <<override>> draw(): void
}

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"
Drawable <|..down.. Flower : "implements"

Utility --> Drawable : "dependsOn"

  1. Utility.drawIt(new Person(Color.BLUE, Color.BLUE));
    
  2. Person bob = new Person(Color.BLUE, Color.BLUE);
    System.out.println(bob.getHaircolor());
    
  3. Airplane plane = new Drawable();
    
Group 3

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Flower {
   - petalColor: Color
   - numberOfPetals: int
   + Flower(petalColor: Color, numberOfPetals: int)
   + bloom(): void
   + changeColor(newColor: Color): void
   + getPetalColor(): Color
   + getNumberOfPetals(): int
   + drawStem(): void
   + drawPetals(): void
   + <<override>> draw(): void
}

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"
Drawable <|..down.. Flower : "implements"

Utility --> Drawable : "dependsOn"

  1. Drawable tree = new Tree(5, Color.GREEN);
    tree.grow(7);
    
  2. Tree tree = new Tree(5, Color.GREEN);
    tree.draw();
    
  3. Drawable device = new Scanner(System.in);
    device.draw();
    
Group 4

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Flower {
   - petalColor: Color
   - numberOfPetals: int
   + Flower(petalColor: Color, numberOfPetals: int)
   + bloom(): void
   + changeColor(newColor: Color): void
   + getPetalColor(): Color
   + getNumberOfPetals(): int
   + drawStem(): void
   + drawPetals(): void
   + <<override>> draw(): void
}

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"
Drawable <|..down.. Flower : "implements"

Utility --> Drawable : "dependsOn"

  1. Utility.drawIt(new Scanner(System.in));
    
  2. Tree tree = new Tree(5, Color.GREEN);
    Drawable d = tree;
    
Solutions
  1. Yes: draw() is defined in the Drawable interface and Person is compatible with Drawable since it implements it.

  2. No: The compiler only looks at the type of the variable (Drawable), which determines which methods can be called. The interface does not contain getEyeColor(), so it is not allowed..

  3. Yes: The drawIt method takes in a reference of type Drawable. Any compatible type can be passed in.

  4. Yes: The drawIt method takes in a reference of type Drawable. Any compatible type can be passed in.

  5. Yes: The variable type is Person, so unique person methods are accessible.

  6. No: Interfaces cannot be instantiated using the new keyword.

  7. No: grow is unique to the Tree class and is not visible when using a Drawable reference since Drawable does not have a grow method.

  8. Yes: Tree implements the interface, so it possesses the draw() method.

  9. No: The Drawable is not compatible with Scanner since Scanner does not implement the interface.

  10. No: The Drawable is not compatible with Scanner since Scanner does not implement the interface.

  11. Yes: Assigning a specific object to a compatible type (class or interface) is always allowed.

Part 4: Bulk Draw
Download Source Code (Instructors)

Note

The starter code contains the same interface and implementing classes. It also comes with a Driver program (containing a main method). While the implementing classes do implement the interface and have a draw method, they may not have all of the methods found in the UML above. Any differences are not substantial and will not affect your ability to complete the last two parts of the lesson.

Execute the command below to download and extract the files:

sh -c "$(curl -fsSL https://cs1302book.com/_bundle/cs1302-interface-lesson.sh)"
- downloading cs1302-interface-lesson bundle...
- verifying integrity of downloaded files using sha256sum...
- extracting downloaded archive...
- removing intermediate files...
subdirectory cs1302-interface-lesson successfully created

Change to the cs1302-interface-lesson directory that was created using cd, then look at the files that were bundled as part of the starter code using tree.

Download Source Code (Students)

Note

The starter code contains the same interface and implementing classes. It also comes with a Driver program (containing a main method). While the implementing classes do implement the interface and have a draw method, they may not have all of the methods found in the UML above. Any differences are not substantial and will not affect your ability to complete the last two parts of the lesson.

Listing 33 Getting the Refactored Code
interface1302
- downloading cs1302-interface-lesson bundle...
- verifying integrity of downloaded files using sha256sum...
- extracting downloaded archive...
- removing intermediate files...
subdirectory cs1302-interface-lesson successfully created

Change to the cs1302-interface-lesson directory that was created using cd, then look at the files that were bundled as part of the starter code using tree.

Interpreter Script

Notice the file called compile_and_run.sh located directly inside of cs1302-interface-lesson. This file contains the appropriate compilation commands for each of the files in our starter code. Instead of typing each command manually, we can run this file to clean out bin, compile the code, and run our driver. You can find more information on compiler scripts in the assigned textbook reading on the topic.

Go ahead and run the file now. You won’t see any output because our Driver doesn’t contain any print statements (yet).

./compile_and_run.sh
Bulk Draw (Activity)

!includesub lesson6.common.puml!STYLE
!include lesson6.after.puml

class Utility {
   + {static} drawIt(obj: Drawable): void
   + {static} drawAll(objs: Drawable[]): void
}

Drawable <|..down.. Tree : "implements"
Drawable <|..down.. Airplane : "implements"
Drawable <|..down.. Person : "implements"

Utility --> Drawable : "dependsOn"

In the main of the Driver class, we have created an array of Drawable references and assigned each index a valid/compatible object.

Implement the drawAll method in Utility.java and then call the method from main using the existing Drawable array.

When you are finished, use the script (compile_and_run.sh) to compile and run your code instead of manually typing out the commands!

Now, you should see some output!

Part 5: Adding an Implementing Class
Planning for a new class (No Laptops)

Discuss and then draw a UML diagram for a new class to add to the code that meets the following criteria:

  • has minimal overlap with the existing classes (disparate);

  • has at least two instance variables;

  • has at least two instance methods (excluding getters/setters);

  • properly implements the Drawable interface.

Implement the new class (Laptops)
  1. Create the .java for your new class and implement it in that file. In addition to everything else, be sure to not forget to include implements and the method override.

  2. Update the compile script to compile the new class.

  3. Make sure that your new class compiles using the script before continuing to the next part.

Use the new class (Laptops)
  1. Add an instance of the new class to the Drawable array in the main method.

  2. Call drawAll on the updated array containing the new type of object.

Write on your exit ticket: How did the code in the Utility class have to change to accommodate this new class?