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, CarnivourousPlant
s inherit all behaviour of regular Plant
s. 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 thatimplements
only the providedPlant
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 }
- Have
Compile and execute your code using only the command line arguments javac
and java
. Verify the code correctly compiles and prints the following:
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 ordinaryPlant
, with the effect that classes implementingCarnivorousPlant
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()
- Pitcher plants, like all other plants do photosynthesis. They must provide an implementation for
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 theCarnivorousPlant
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!
"
- Have the standard plant methods return slightly modified Strings:
- Uncomment the deactivated lines in the provided
Main
class, so themain
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
FruitFly
: Is tiny, makes an annoying high-pitched sound.makeAnnoyingSound()
returns "Bziiiiiiiiiiiiiiiii
".HoverFly
: Is larger, makes bursts of annoying buzzing sound.makeAnnoyingSound()
returns "Bzuuuuuuuuuuuuu Bzz Bzz Bzuuuuuu
".
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 aFly
object to theeatFly(Fly)
method. - Modify the
PitcherPlant
using the if/else statements provided above
- Add the new
- Compile and run the code. Verify the correct output is printed
- Modify the code in
PitcherPlant
'seatFly(Fly)
implementation, so it uses Polymorphism. There must be not if/else statement or call togetName()
.- 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.