Skip to content

Lab 02

In this lab session you will practice the principles of interfaces, and train your awareness for unmaintainable code. The code to work with is available on GitLab you only need the "plants" folder and its content.

Interfaces

In the previous lectures and lab, we've discussed the interest of coding against interfaces, rather than classes. In this first exercise block, you will be working with a hierarchy of interfaces: Plants and Carnivorous Plants. You will therefore be working with two interfaces:

  • Plant, as an interface to define classic functions of all plants:
    • Photosynthesis ("consuming" light)
    • Drinking ("consuming" water)
  • CarnivorousPlant, as an interface to add additional functions to some plants:
    • Eating a fly: Carnivorous plants not only do photo synthesis and consume water, the also attract yummy flies and digest their bodies for additional nutrients.

We can accurately capture this plant hierarchy with two interfaces:

---
title: Plant Interfaces
---
classDiagram
    Plant <.. CarnivorousPlant: extends

    class Plant {
        <<interface>>
        +doPhotosynthesis() String
        +drinkWater() String
    }

    class CarnivorousPlant {
        <<interface>>
        +eatFly(): String
    }
How do you read this diagram?

There are two interfaces: Plant and CarnivorousPlant. The interfaces form a hierarchy, that is, CarnivourousPlants inherit all behaviour of regular Plants. The interfaces by themselves do not provide any logic, only define what functions implementing classes must provide. Interface hierarchies are additive, that means any concrete class that only implements Plant needs only the upper two methods, but any class that implements CarnivorousPlant requires all three methods implemented.

Task 1, Potatos

Your first task is to download and extract the provided code. You will also find a class Main, with the following default implementation:

 public static void main(String[] args) {
  // Create a plant and a carnivorous plant instance.
  Plant normalPlant = new Potato(); // TODO: create a new class "Potato" that is a plant, but not a carnivorousPlant.
  // CarnivorousPlant carnivorousPlant = new PitcherPlant();
  // Let both do some photosynthesis:
  System.out.println("Doing photosynthesis:");
  System.out.println(normalPlant.doPhotoSynthesis());
  // System.out.println(carnivorousPlant.doPhotoSynthesis());
  // Let both enjoy a sip of water
  System.out.println("Serving water:");
  System.out.println(normalPlant.drinkWater());
  // ...
}

The provided code will not compile, because the main method makes use of a novel class: Potato.

Caption of potatoes. Delicious, but not a carnivorous plants. Image credits: Wikipedia

Your turn

  • Write a Potato class that implements only the provided Plant interface.
  • Provide implementations for the two required methods, but do not change the interfaces.
    • Have doPhotoSynthesis() return the String: "Oh, sunbeams. I love warm sunbeams!"
    • Have drinkWater() return the String: "Slurp... aah!"
      ---
      title: Plant Interfaces and Potato Class
      ---
      classDiagram
          Plant <.. CarnivorousPlant: extends
          Plant <|-- Potato: implements
      
          class Plant {
              <<interface>>
              +doPhotosynthesis() String
              +drinkWater() String
          }
      
          class CarnivorousPlant {
              <<interface>>
              +eatFly(): String
          }
      
          class Potato {
              <<Class>>
              +doPhotosynthesis() String
              +drinkWater() String
          }

Compile and execute your code using only the command line arguments javac and java. Verify the code correctly compiles and prints the following:

Doing photosynthesis:
Oh, sunbeams. I love warm sunbeams!
Serving water:
Slurp... aah!

Task 2, Pitcher Plants

So far you've been only using the top level interface.

  • In the last lecture we've briefly seen the concept of multiple interfaces.
  • In the above example CarnivorousPlant extends ordinary Plant, with the effect that classes implementing CarnivorousPlant must provide method implementations for both interfaces combined.
  • An example are Pitcher Plants
    • Pitcher plants, like all other plants do photosynthesis. They must provide an implementation for doPhotosynthesis()
    • Pitcher plants, like all other plants need water. They must provide an implementation for drinkWater()
    • Pitcher plants, unlike all other plants attract and eat flies. They must provide an additional interface for eatFly()

Caption of a pitcher plants. Smells delicious, if you're a fly. Image credits: Wikipedia

Your turn

  • Write a new class PitcherPlant, which implement only the CarnivorousPlant interface.
    ---
    title: Plant Interfaces with Potato and Pitcher Plant
    ---
    classDiagram
      Plant <.. CarnivorousPlant: extends
      Plant <|-- Potato: implements
      CarnivorousPlant <|-- PitcherPlant : implements
    
    
      class Plant {
        <<interface>>
        +doPhotosynthesis() String
        +drinkWater() String
      }
    
      class CarnivorousPlant {
        <<interface>>
        +eatFly(): String
      }
    
      class Potato {
        <<Class>>
        +doPhotosynthesis() String
        +drinkWater() String
      }
    
      class PitcherPlant {
        <<Class>>
        +doPhotosynthesis() String
        +drinkWater() String
        +eatFly(): String
      }
  • Make sure the class provides all required methods.
    • Have the standard plant methods return slightly modified Strings:
      • Oh, sunbeams. I like sunbeams, but I like flies even more.
      • Slurp... good water, but could use some extra nutrients.
    • Have the new method return a String: "Yummy, a fly!"
  • Uncomment the deactivated lines in the provided Main class, so the main method reads:
 public static void main(String[] args) {
  // Create a plant and a carnivorous plant instance.
  Plant normalPlant = new Potato();
  CarnivorousPlant carnivorousPlant = new PitcherPlant();
  // Let both do some photosynthesis:
  System.out.println("Doing photosynthesis:");
  System.out.println(normalPlant.doPhotoSynthesis());
  System.out.println(carnivorousPlant.doPhotoSynthesis());
  // Let both enjoy a sip of water
  System.out.println("Serving water:");
  System.out.println(normalPlant.drinkWater());
  System.out.println(carnivorousPlant.drinkWater());
  // On top of that, let the carnivorous plant eat a fly
  System.out.println("Feeding flies:");
  System.out.println(carnivorousPlant.eatFly());
  System.out.println(carnivorousPlant.eatFly());

Verify your code compiles and prints the following:

Doing photosynthesis:
Oh, sunbeams. I love warm sunbeams!
Oh, sunbeams. I like sunbeams, but I like flies even more.
Serving water:
Slurp... aah!
Slurp... good water, but could use some extra nutrients.
Feeding flies:
Yummy, a fly!
Yummy, a fly!

Polymorphism

Notice how the printed outputs changed between Potatoes and Pitcher Plants, although we called the same methods: doPhotosynthesis() and drinkWater() ? This concept is called polymorphism. We delegate a function call to a class, without caring too much about how it is implemented. Different objects implementing the same interface may have different behaviour, and we have them sort out their reaction themselves. This concepts is called Polymorphism.

Use polymorphism to keep your code clean

Polymorphism is a key concept, and lets you conveniently eliminate overly complicated code, notably obsolete if statements.

Task 3, Flies

An example for overly complicated code is about our plants eating flies. Consider the following change:

  • There is an additional interface Fly
    • getName(): String returns the name of the implementing class.
    • makeAnnoyingSound(): String returns a string representation of the sound produced by the insect in flight.
  • There are two classes implementing Fly

The below diagram illustrates these additional interface and classes:

---
title: Polymorphic Flies
---
classDiagram
    Fly <|-- FruitFly: implements
    Fly <|-- HoverFly: implements

    class Fly {
        <<interface>>
        +getName() String
        +makeAnnoyingSound() String
    }

    class FruitFly {
        <<class>>
        +getName() String
        +makeAnnoyingSound() String
    }

    class HoverFly {
        <<class>>
        +getName() String
        +makeAnnoyingSound() String
    }

Additionally, consider a changed CarnivorousPlant interface and implementation, so that our PitcherPlant provides a realistic imitation of catching the Fly in flight and then harvesting its nutrients.

// Modified: now takes a Fly object as argument
@Override
public String eatFly(Fly fly) {
  // Check which fly it is
  if (fly.getName().equals("FruitFly")) {
    return "Bziiiiiiiiiiiiiiiii... CHOMP!!!!   That was delicious! Yummy! Thank you!";
  } else {
    return "Bzuuuuuuuuuuuuu Bzz Bzz Bzuuuuuu... CHOMP!!!!   That was delicious! Yummy! Thank you!";
  }
}

We can now feed flies to our plant (in Main):

    // Let the carnivorous plant eat two types of flies
Fly fly1 = new FruitFly();
Fly fly2 = new HoverFly(); 
    System.out.

println("Feeding flies:");
    System.out.

println(carnivorousPlant.eatFly(fly1));
    System.out.

println(carnivorousPlant.eatFly(fly2));

And we will see:

    Feeding flies:
    Bziiiiiiiiiiiiiiiii... CHOMP!!!!   That was delicious! Yummy! Thank you!
    Bzuuuuuuuuuuuuu Bzz Bzz Bzuuuuuu... CHOMP!!!!   That was delicious! Yummy! Thank you!

Your turn

Why is the code, as currently implemented in PitcherPlant problematic ?

PitcherPlant has an if construct that checks for specific Fly implementations. Every time a new Fly class comes along, we'd have to modify the code. We could easily end up with a length list of if/else checks for various types of flies.

  • Modify the provided code as described above.
    • Add the new Fly interface, add the two implementing classes.
    • Modify the CarnivorousPlant interface to provide a Fly object to the eatFly(Fly) method.
    • Modify the PitcherPlant using the if/else statements provided above
  • Compile and run the code. Verify the correct output is printed
  • Modify the code in PitcherPlant's eatFly(Fly) implementation, so it uses Polymorphism. There must be not if/else statement or call to getName().
    • Verify the same output is printed.

Solution

Solution available on GitLab.

Only look at solution after you've tried your best.

While it is natural to peek into the solution while solving, the learning effect is drastically reduced. It is in your own interest to earnestly try to solve the exercises, before you consult the solution. If you're feeling blocked, do not hesitate to ask for help. The lab assistants will be happy to give you some clues.