In which we explore the four pillars of object oriented programming by way of a World War 2 tank.
The Sherman tank in British service
In WW2 the allies needed a reliable, mass-produced piece of armour to counter the German panzers. The Americans, with all their industrial prowess, designed the M4 Sherman tank to fulfil this role. It was a good tank, well armed for its time, and proved successful on the battlefield.
The British, beleaguered and war weary, were happy to receive this new weapon under the terms of lend-lease program (whereby the Americans supplied and armed its allies in the war). Of course they had to put their own stamp on it, making a few modifications and calling it the Sherman V.
The tank served them well, but as the war progressed, the axis forces introduced newer and heavier tanks - the Panther and King Tiger. The poor Sherman struggled to penetrate the armour of these new big cats, so the decision was made by the British to up-gun the Sherman, fitting the far more deadly 17pdr.
Modelling the Sherman tank in British service
Let us now look to model the Sherman tank in British service in an object
oriented programming (OOP) language - Java. We begin by declaring a class called Sherman which will define our tank.
public class Sherman
{
// Definition of a Sherman tank to go here.
}
When we talk of defining a class what do we actually mean? In OOP terms we are talking about the behaviour and properties of that class. Let's start with the behaviour. Our tank can move and shoot, and also the crew can bail out and abandon the tank if things get too hairy. In Java we use methods to define behaviour.
public class Sherman
{
public void move(int miles)
{
// Implementation of movement to go here.
}
public void shoot()
{
// Implementation of shooting to go here.
}
public void bailOut()
{
// Implementation of bailing out to go here.
}
}
You'll notice that we haven't yet defined an implementation for these methods. Before we do that, let us think about what properties our tank might have. If we are moving and shooting, it is fair to assume we have some need of fuel and ammunition. We also need to indicate if the tank is currently bailed out, as we cannot perform any actions if the crew have abandoned the vehicle. Let us model this state information as private properties. We will set these properties once via a constructor for the class.
public class Sherman
{
private int milesOfFuel;
private int ammunition;
private boolean bailedOut;
public Sherman(int milesOfFuel, int ammunition)
{
this.milesOfFuel = milesOfFuel;
this.ammunition = ammunition;
bailedOut = false;
}
public void move(int miles)
{
// Implementation of movement to go here.
}
public void shoot()
{
// Implementation of shooting to go here.
}
public void bailOut()
{
// Implementation of bailing out to go here.
}
}
This idea of defining the state alongside the behaviour that acts upon it is known as encapsulation. It is one of the four pillars of OOP. The unit of encapsulation in an OOP language like Java is a class.
We can now start to implement our tank's methods. Let's cover bailing out first, as it is the simplest to do.
public void bailOut()
{
if (bailedOut)
{
System.println("Tank already bailed out!");
}
else
{
bailedOut = true;
System.println("Crew bailing out!");
}
}
Here we use the bailedOut property from the state we defined in this class to see if we can bail the crew out or not.
Next let's look at shooting.
public void shoot()
{
if (bailedOut)
{
return;
}
if (ammunition > 0)
{
ammunition = ammunition - 1;
System.println("BOOM!");
}
else
{
System.println("Click...");
}
}
We are querying two properties from our class state to see if we can fire - the crew are present and we have enough ammunition. If we can shoot, we modify the ammunition variable to reduce it by one.
Finally movement.
public void move(int miles)
{
if (bailedOut)
{
return;
}
if (milesOfFuel >= miles)
{
milesOfFuel = milesOfFuel - miles;
System.println(string.format("Driving %d miles.", miles));
}
else
{
System.println("Not enough fuel!");
}
}
Let's take our tank for a spin!
var tank = new Sherman(10, 3);
tank.move(5); // Output: Driving 5 miles.
tank.shoot(); // Output: BOOM!
tank.move(10); // Output: Not enough fuel!
tank.bailOut(); // Output: Crew bailing out!
Neat. This little demonstration illustrates another of the four pillars of OOP, namely that of abstraction. Notice that we can just send simple messages to our tank like move and shoot without having to know the complexities of how they are implemented. Abstraction combined with encapsulation are powerful tools for modelling complex behaviours in a manageable way.
Time for an upgrade
As mentioned in the introduction to this article, the Sherman soon found its main gun outclassed by the armour of newer axis tanks like the Panther. To solve this problem, the British retrofitted the Sherman with the much more powerful 17pdr gun. How can we model this upgrade in our program?
The Sherman with the 17pdr gun - called a Sherman Firefly - has, for our
purposes, the same behaviour as a regular Sherman. It just made a bigger boom when shooting! Therefore we don't want to have to write a whole new class to define this newer version of our tank. If we did, we would end up duplicating the state and behaviours other than shooting. What we want is a way to say "it's the same as this class, but with this one change". Enter another pillar of OOP, inheritance.
public class ShermanFirefly extends Sherman
{
public ShermanFirefly(int milesOfFuel, int ammunition)
{
super(milesOfFuel, ammunition);
}
@override
public void shoot()
{
if (bailedOut)
{
return;
}
if (ammunition > 0)
{
ammunition = ammunition - 1;
System.println("KABOOM!");
}
else
{
System.println("Click...");
}
}
}
Pretty neat. We have defined a new class ShermanFirefly that we told Java inherits from the existing Sherman class by using the extends keyword. We have then redefined the shoot method to output a bigger boom when firing. Notice however that we had to redefine the whole method, including the bits that didn't need to change. Can we do better than that?
public class Sherman
{
// Other code omitted.
public void shoot()
{
if (bailedOut)
{
return;
}
if (ammunition > 0)
{
ammunition = ammunition - 1;
fire();
}
else
{
System.println("Click...");
}
}
protected void fire
{
System.println("BOOM!");
}
// Other code omitted.
}
Here in the base Sherman class we have changed the shoot method slightly. It now delegates the actual printing of output to another method called fire. Note that this method is marked as protected. That means no one outside the class can see it. So no sneaky calling fire directly and bypassing the checks for bailed out crew and ammunition levels.
var tank = new Sherman(10, 3);
tank.shoot(); // Output: BOOM!
tank.fire(); // Won't compile.
With this new method in place, we can amend our ShermanFirefly class os it redefines the fire method and not the shoot method.
public class ShermanFirefly extends Sherman
{
public ShermanFirefly(int milesOfFuel, int ammunition)
{
super(milesOfFuel, ammunition);
}
@override
protected void fire()
{
System.println("KABOOM!");
}
}
Let's try a few shots with the Firefly.
var tank = new ShermanFirefly(10, 3);
tank.shoot(); // Output: KABOOM!
Working together
Tanks seldom worked in isolation. They would be grouped together in platoons, usually of three or more tanks. The British had a problem though. The Firefly was lethal against the bigger axis tanks, but there weren't enough to equip whole platoon's of them. The solution was to equip platoons with a mix of two or three regular Shermans and one Firefly.
How can we model such a platoon of tanks in our code?
var platoon = new List<Sherman>();
platoon.add(new Sherman(10, 5));
platoon.add(new Sherman(10, 5));
platoon.add(new ShermanFirefly(10, 3));
Because the ShermanFirefly inherits from the Sherman class, we can add it to a list of type Sherman. A Firefly is-a Sherman in our model. What happens if we ask each tank in our platoon to fire its gun?
for(Sherman tank : platoon)
{
tank.shoot();
}
// Outputs:
// BOOM!
// BOOM!
// KABOOM!
Notice how the Firefly shot differently to the other Shermans. Despite the fact we used an instance of type Sherman in our for loop, the Firefly still made its big KABOOM!. This is know as polymorphism, and is the last of the four pillars of OOP. Objects of different types can respond to the same message in different ways.
Home for tea and medals
There you have it. We used the four pillars of object oriented programming - encapsulation, abstraction, inheritance, and polymorphism - to model the World War 2 era British tank the Sherman, and its up-gunned version the Firefly.
Complete listing:
public class Sherman
{
private int milesOfFuel;
private int ammunition;
private boolean bailedOut;
public Sherman(int milesOfFuel, int ammunition)
{
this.milesOfFuel = milesOfFuel;
this.ammunition = ammunition;
bailedOut = false;
}
public void move(int miles)
{
if (bailedOut)
{
return;
}
if (milesOfFuel >= miles)
{
milesOfFuel = milesOfFuel - miles;
System.println(string.format("Driving %d miles.", miles));
}
else
{
System.println("Not enough fuel!");
}
}
public void shoot()
{
if (bailedOut)
{
return;
}
if (ammunition > 0)
{
ammunition = ammunition - 1;
fire();
}
else
{
System.println("Click...");
}
}
public void bailOut()
{
if (bailedOut)
{
System.println("Tank already bailed out!");
}
else
{
bailedOut = true;
System.println("Crew bailing out!");
}
}
protected void fire()
{
System.println("BOOM!");
}
}
public class ShermanFirefly extends Sherman
{
public ShermanFirefly(int milesOfFuel, int ammunition)
{
super(milesOfFuel, ammunition);
}
@override
protected void fire()
{
System.println("KABOOM!");
}
}
Top comments (0)