patternjavaMinor
Java beginner exercise : Write a class "Air Plane"
Viewed 0 times
exercisewritejavaplanebeginnerairclass
Problem
I had to do this exercise for a further education in which I'm currently enrolled in:
Write a Java class "Air Plane".
Object-property names and types are given and compulsory.
Write the according constructor and getter-, setter-method. Check within the constructor the given values for being valid.
Moreover are the following methods to implement:
-
info
-
load
-
fillUp
-
fly
-
getTotalWeight
-
getMaxReach
Further requirements concerning the implementation of the methods I have written into my code as comments.
Here's my Plane-class
```
package plane;
public class Plane
{
private double maxWeight;
private double emptyWeight;
private double loadWeight;
private double travelSpeed;
private double flyHours;
private double consumption;
private double maxFuel;
private double kerosinStorage;
public Plane( double maxWeight, double emptyWeight, double loadWeight,
double travelSpeed, double flyHours, double consumption,
double maxFuel, double kerosinStorage )
{
this.maxWeight = maxWeight;
this.emptyWeight = emptyWeight;
this.loadWeight = loadWeight;
this.travelSpeed = travelSpeed;
this.flyHours = flyHours;
this.consumption = consumption;
this.maxFuel = maxFuel;
this.kerosinStorage = kerosinStorage maxFuel
? maxFuel : this.kerosinStorage + kerosinStorage;
}
/*
Returns the total weight of the plane. Which is: emptyWeight +
weight of load + weight of kerosin.
Expect 1 liter Kerosin as 0.8 kg.
*/
public double getTotalWeight ()
{
return emptyWeight + loadWeight
+ (kerosinStorage * 0.8);
}
/*
How far can the plane fly with the current kerosin storage?
*/
public double getMaxReach ()
{
return (kerosinStorage / consumption) * travelSpeed;
Write a Java class "Air Plane".
Object-property names and types are given and compulsory.
Write the according constructor and getter-, setter-method. Check within the constructor the given values for being valid.
Moreover are the following methods to implement:
-
info
-
load
-
fillUp
-
fly
-
getTotalWeight
-
getMaxReach
Further requirements concerning the implementation of the methods I have written into my code as comments.
Here's my Plane-class
```
package plane;
public class Plane
{
private double maxWeight;
private double emptyWeight;
private double loadWeight;
private double travelSpeed;
private double flyHours;
private double consumption;
private double maxFuel;
private double kerosinStorage;
public Plane( double maxWeight, double emptyWeight, double loadWeight,
double travelSpeed, double flyHours, double consumption,
double maxFuel, double kerosinStorage )
{
this.maxWeight = maxWeight;
this.emptyWeight = emptyWeight;
this.loadWeight = loadWeight;
this.travelSpeed = travelSpeed;
this.flyHours = flyHours;
this.consumption = consumption;
this.maxFuel = maxFuel;
this.kerosinStorage = kerosinStorage maxFuel
? maxFuel : this.kerosinStorage + kerosinStorage;
}
/*
Returns the total weight of the plane. Which is: emptyWeight +
weight of load + weight of kerosin.
Expect 1 liter Kerosin as 0.8 kg.
*/
public double getTotalWeight ()
{
return emptyWeight + loadWeight
+ (kerosinStorage * 0.8);
}
/*
How far can the plane fly with the current kerosin storage?
*/
public double getMaxReach ()
{
return (kerosinStorage / consumption) * travelSpeed;
Solution
Builder pattern
Consider the "builder pattern". As I started to pass in arguments to the constructor, it was hard to keep the semantics right.
The builder pattern helps the developer to
The builder pattern has only one assertion: It doesn't matter how many arguments you passed in, it will always build a consistent object.
Avoid multiple return-statements
Return-statements are structural identical to goto-statements although they are a formalized version. What all goto-alike-statements (return, continue, break) hav in common: They are not refactoring-stable. They hinder you to apply reforings like "extract method". If you have to insert a new case in an algorithm that uses break, continue and return-statements you may have to overthink the whole algorithm so that your change will not break it.
Avoid inexpressive return values
You may see return values like true/false to indicate something has been processed well or not. These return values may be sufficient for trivial cases in trivial environments where less exceptional cases occur.
In complex environment a method execution may fail due to several reasons. A connection to the server was lost, an inconsistency on the database-side was recognized, the execution failed because of security reasons... to name only the tip of the iceberg. There modern languages introduce a concept for "exceptional" cases: Exceptions.
E.g. you have following signature:
Beside you have mixed two concerns in one method (load/unload) that you treat differently (overload will not be allowed, unload will be corrected) you also try to publish success information via return value.
I suggest to not publish true or false. I suggest to have either no return value or the new value of the loadWeight. Exceptional cases I would handle with the concepts of exceptions. I would expect a signature like this:
The OverloadedException may not be signature relevant (RuntimeException) but it expresses the intention of the method.
Beside that I would split responsibilities and introduce a method:
Avoid comments
If you want to make comments it is an indicator for that your code itself may not be clear enough. I intentionally said "avoid comments" but not "do not comment anything". First think about the things that will be compiled and run to be as clear as possible. Then if you think it's neccessary to comment then comment. Comments have to be maintained separately. They are "uncompiled" code and cannot be be put under test. So they may lie if they diverge from your code semantics.
E.g. you have following signature:
In your comment you mentioned that "liter" may be negative. This is an allowed value but your method signature says "fillUp". So one of them is lying. You now have two possibilities:
The best "comment" for a "procedure", "function", "method" is a set of tests that show the usage of it so other developers can see, how your code will work in different situations. Instead of testing your object in a main-scope I suggest to make ...
Unit Tests
Following the suggestions you can do expressive unit tests:
You should decide which coverage you want to aim. I prefer condition coverage over statement coverage because it forces you to keep your methods small. Methods under condtion coverage have at least 2^condition_elements of test cases. If you have long methods with several conditions your test case count may explode.
As you see in the test cases, I have comments. They describe the business rules you want to enforce.
Consider the "builder pattern". As I started to pass in arguments to the constructor, it was hard to keep the semantics right.
The builder pattern helps the developer to
- abstract from argument input order
- handle a lot of constructor arguments
- abstract from default values that make sense
- make arguments optional and therefore avoid telescope constructors
The builder pattern has only one assertion: It doesn't matter how many arguments you passed in, it will always build a consistent object.
Avoid multiple return-statements
Return-statements are structural identical to goto-statements although they are a formalized version. What all goto-alike-statements (return, continue, break) hav in common: They are not refactoring-stable. They hinder you to apply reforings like "extract method". If you have to insert a new case in an algorithm that uses break, continue and return-statements you may have to overthink the whole algorithm so that your change will not break it.
Avoid inexpressive return values
You may see return values like true/false to indicate something has been processed well or not. These return values may be sufficient for trivial cases in trivial environments where less exceptional cases occur.
In complex environment a method execution may fail due to several reasons. A connection to the server was lost, an inconsistency on the database-side was recognized, the execution failed because of security reasons... to name only the tip of the iceberg. There modern languages introduce a concept for "exceptional" cases: Exceptions.
E.g. you have following signature:
public boolean load (double kg)Beside you have mixed two concerns in one method (load/unload) that you treat differently (overload will not be allowed, unload will be corrected) you also try to publish success information via return value.
I suggest to not publish true or false. I suggest to have either no return value or the new value of the loadWeight. Exceptional cases I would handle with the concepts of exceptions. I would expect a signature like this:
public double load (double kg) throws OverloadedExceptionThe OverloadedException may not be signature relevant (RuntimeException) but it expresses the intention of the method.
Beside that I would split responsibilities and introduce a method:
public double unload (double kg)Avoid comments
If you want to make comments it is an indicator for that your code itself may not be clear enough. I intentionally said "avoid comments" but not "do not comment anything". First think about the things that will be compiled and run to be as clear as possible. Then if you think it's neccessary to comment then comment. Comments have to be maintained separately. They are "uncompiled" code and cannot be be put under test. So they may lie if they diverge from your code semantics.
E.g. you have following signature:
public void fillUp (double liter)In your comment you mentioned that "liter" may be negative. This is an allowed value but your method signature says "fillUp". So one of them is lying. You now have two possibilities:
- Think about a name, that abstracts from draining or filling up fuel (adjust?) so it is clear that you may have a negative argument or ...
- ... separate the concerns (draining, filling up) into separate methods to match SRP.
The best "comment" for a "procedure", "function", "method" is a set of tests that show the usage of it so other developers can see, how your code will work in different situations. Instead of testing your object in a main-scope I suggest to make ...
Unit Tests
Following the suggestions you can do expressive unit tests:
public class TestPlane {
/**
* A plane's fuel can be filled up.
*/
@Test
public void fillUpNormal() {
Plane plane = new PlaneBuilder().setMaxFuel(2000).setInitialKerosinStorage(1700).build();
Assert.assertEquals(1800, plane.fillUp(100));
}
/**
* A plane cannot be filled up beyond max fuel.
*/
@Test
public void fillUpOverfilled() {
Plane plane = new PlaneBuilder().setMaxFuel(2000).setInitialKerosinStorage(1700).build();
try {
plane.fillUp(400);
Assert.fail();
} catch (OverfilledException e) {
Assert.assertEquals(100, e.getOverfilledBy());
}
}
}You should decide which coverage you want to aim. I prefer condition coverage over statement coverage because it forces you to keep your methods small. Methods under condtion coverage have at least 2^condition_elements of test cases. If you have long methods with several conditions your test case count may explode.
As you see in the test cases, I have comments. They describe the business rules you want to enforce.
Code Snippets
public boolean load (double kg)public double load (double kg) throws OverloadedExceptionpublic double unload (double kg)public void fillUp (double liter)public class TestPlane {
/**
* A plane's fuel can be filled up.
*/
@Test
public void fillUpNormal() {
Plane plane = new PlaneBuilder().setMaxFuel(2000).setInitialKerosinStorage(1700).build();
Assert.assertEquals(1800, plane.fillUp(100));
}
/**
* A plane cannot be filled up beyond max fuel.
*/
@Test
public void fillUpOverfilled() {
Plane plane = new PlaneBuilder().setMaxFuel(2000).setInitialKerosinStorage(1700).build();
try {
plane.fillUp(400);
Assert.fail();
} catch (OverfilledException e) {
Assert.assertEquals(100, e.getOverfilledBy());
}
}
}Context
StackExchange Code Review Q#145651, answer score: 6
Revisions (0)
No revisions yet.