Inheritance¶
After mastering the basics of the Shadow language and learning how classes and interfaces work, we’re ready to move on to some advanced object-oriented concepts. The first we’ll dive into is inheritance.
As you’ve probably noticed, objects on their own are powerful programming tools with innumerable applications. But sometimes we want to take an existing class and add features to it without disrupting code that uses the original class. Inheritance allows us to do exactly this: make specialized or refined versions of existing classes in order to reuse code safely.
To illustrate the value of inheritance, consider the following scenario. You are the owner of Shadow and Light, a restaurant with three Michelin stars, and you want to write a program that represents your employees. First, you start by brainstorming the attributes that all employees at your restaurant share:
Name
Wage
Hours worked
You also want to give your employees the ability to clock in and clock out of work.
You could write a class called Employee
and give it member variables and methods that correspond to the attibutes and abilities outlined above. However, your employees have many things in common, but a chef will not have the same responsibilities as a waiter or manager. So, how will you differentiate the different jobs your employees have?
You could forgo the Employee
class completely and write separate classes: Waiter
, Manager
, and Chef
. Each class would still contain all the abilities of a general employee but with some additions. While this approach is logical, it’s not ideal because a lot of code will be repeated between the classes. What if there was a way for the Waiter
, Manager
, and Chef
classes to inherit from an Employee
class? Any code and data that all classes need could be written in this parent class, and only the details that make the Waiter
, Manager
, and Chef
classes special would be written in those child classes. Using this approach, we would not have to write some of the same methods or member variable declarations repeatedly. Shadow allows us to do exactly that.
Before we go over how to implement this example, let’s define some terms used with inheritance:
Parent class: A parent class, or superclass, is a class that others inherit from. In this example, the parent class is
Employee
.Child class: A child class, or subclass, is one that inherits methods and member variables from another class. In this example, the subclasses are
Waiter
,Manager
, andChef
.Is-a relationship: We say that a child class has an is-a relationship with its parent class. For example, a
Waiter
is anEmployee
, but not everyEmployee
is aWaiter
.
The relationships between parent classes and their children can be viewed as a tree where each child branches off from its parent.
Shadow only allows single inheritance. In other words, a class can only inherit from one other class, meaning that a child class may only have one parent. The Waiter
class could not inherit from both the Employee
and Student
classes. However, this restriction doesn’t prevent the parent class itself from inheriting from another class. In our example, the Employee
class could inherit from another class called Person
. Ultimately, all classes are children of the the root class Object
, the only class that has no parent.
Inheriting from a class¶
In order to understand the syntax needed to inherit from a class, the two example classes Employee
and Waiter
are shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import shadow:io@Console;
class tutorials:inheritance@Employee
{
get String name;
get set int hoursWorked;
get double wage;
public create(String name, double wage)
{
this:name = name;
hoursWorked = 0;
this:wage = wage;
}
public clockIn(int hours) => ()
{
Console.printLine(name # " is clocking in for a " # hours # " hour shift");
hoursWorked += hours;
}
public clockOut() => ()
{
Console.printLine(name # " is clocking out.");
}
}
|
And here is the Waiter
class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | import shadow:io@Console;
class tutorials:inheritance@Waiter is Employee
{
int totalTables;
double tips;
public create(String name, double wage)
{
super(name, wage);
tips = 0.0;
totalTables = 0;
}
public waitTables(int tables) => ()
{
Console.printLine(this->name # " just picked up " # tables # " tables");
numTables += totalTables;
}
public receiveTip(double tip)
{
tips += tip;
}
}
|
Using is
for inheritance¶
By itself, there’s nothing new about the Employee
class. It has three member variables, one constructor, and two methods.
Now, look at the Waiter
class. Notice how the class header says, class Waiter is Employee
. The keyword is
signifies to the compiler that Waiter
inherits from Employee
. Syntactically, this is the only thing you have to do to establish the inheritance relationship. If no class is specified with an is
, the class Object
is assumed to be the parent.
What’s inherited?¶
Now that we’ve established how to inherit from a parent class, it’s important to discuss what exactly is inherited: the members of the parent class. All of its member variables and methods are passed on to the child.
How does this apply to our example? Notice how Waiter
appears to have only two member variables. In reality, it has five – Waiter
inherits the private member variables of its parent class, name
, hoursWorked
, and wage
. Although these private member variables are inherited, they cannot be directly used in the child class. For example, look at Line 17 of the Waiter
class. Instead of writing Console.printLine(name # ... )
, we must use the get
property of the variable name
in the child class.
Calling parent constructors¶
In the constructor for the Waiter
class, you may have noticed this unusual statement on Line 10: super(name, wage);
When super()
is called, it invokes the constructor of the parent class. However, the number and type of parameters must exactly match that of an existing parent constructor or you will get a compiler error. You should pay especially close attention if a parent class has multiple constructors. In our example, name
is a String
, and wage
is a double
, which matches the constructor in the Employee
class. The member variables name
, hoursWorked
, and wage
are subsequently initialized. However, tips
and totalTables
still need to be initialized, and this is done in the last two lines of the Waiter
constructor.
Warning
If a parent class doesn’t have a default constructor, which takes no parameters, you must make a call to super()
to initialize the parent member variables explicitly.
Since the member variables of the parent class are private
automatically, you could not simply say this:name = name;
in the child class constructor.
Note
If you make a call to super()
in a child class constructor, that call must be the first statement in the constructor.
Driver code using inheritance¶
Examine the excerpt from the driver class and console output below in order to see inheritance in action:
1 2 3 4 5 6 7 8 9 10 11 | Employee sarah = Employee:create("Sarah" , 10.50);
Waiter trevor = Waiter:create("Trevor", 20.1, 50.5);
Console.printLine("Testing the Employee object");
sarah.clockIn(7);
Console.printLine();
Console.printLine("Testing the Waiter object");
Console.printLine("Hi, " # trevor->name);
trevor.clockIn(5);
trevor.waitTables(4);
|
The console output:
Testing the Employee object
Sarah is clocking in for a 7 hour shift
Testing the Waiter object
Hi, Trevor
Trevor is clocking in for a 5 hour shift
Trevor just picked up 4 tables
As seen in the first few lines of the driver class, there is nothing syntactically different about creating either an Employee
object or a Waiter
object. In Line 9, notice the way that we access the name
property inherited from the parent class: trevor->name
. Although these member variables cannot be directly accessed in the child class itself, get
and set
properties from the parent class can still be used on child class objects. Similarly, on Line 10 we can call the parent method clockIn()
even though it was not defined in the Waiter
class. Because it’s inherited, we can call it on any Waiter
object.
Although we only showed implementations for Employee
and Waiter
, you can practice inheritance by implementing the Chef
and Manager
classes.
Inheriting from a class and implementing interfaces¶
It’s possible for a class to inherit from a parent class and also implement one or more interfaces. Although a class can implement multiple interfaces, it can only directly inherit from one other class. This can be confusing, since both relationships use the keyword is
.
If a class inherits from another class, the parent class must come first after the is
, followed by the interfaces it implements in any order, separated by and
. For example:
class Beyonce
is Awesome
and CanDance
and CanSing
Here, the class name is Beyonce
, and the class it inherits from is Awesome
, and the two interfaces it implements are CanDance
and CanSing
. When a class implements several interfaces, it’s Shadow convention to put the parent class and each interface on a separate, indented line.
The protected
keyword¶
In the Classes tutorial, we explained the private
and public
modifiers, but there’s also a protected
modifier. If a constant or method is marked protected
, it means that it can only be accessed within the class itself and in any child classes. For example, if a method in the Employee
class had been marked protected
, only code inside its children (such as Waiter
) and itself would be able to call it.
In addition, you can also create protected
get
and set
properties. Applying get
and set
modifiers to member variables creates public
properties by default, so protected
versions must be written by hand. See the three short classes below:
class tutorials:inheritance@Hello
{
get String word = "hello";
protected set word(String word) => ()
{
this:word = word;
}
}
class tutorials:inheritance@Bonjour is Hello
{
public speakFrench() => ()
{
this->word = "bonjour";
}
}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import shadow:io@Console;
class tutorials:inheritance@Language
{
public main(String[] args) => ()
{
Hello hello = Hello:create();
Console.printLine(hello->word);
Bonjour bonjour = Bonjour:create();
bonjour.speakFrench();
Console.printLine(bonjour->word);
}
}
|
Note that class Bonjour
inherits from Hello
. This means that, unless speakFrench()
is called, the member variable word
will contain the value "hello"
in Bonjour
objects. But we do call the speakFrench()
method on Line 11, which causes the bonjour
object to use the protected
set
property, changing its member variable word
to "bonjour"
. We could not have used this set
property in the driver class Language
because it’s marked protected
in the parent class Hello
.
Method overriding¶
In many cases, the methods that a parent class provides work perfectly for a child class. However, there are situations when the child class needs to change the method, adding different functionality. This process, in which the programmer provides a new implementation for a method inherited from a parent class, is called method overriding. In order to properly override a method, the overridden method header must match the header of the original method. The method body may – and should – be different.
If a parent method is marked readonly
, overridden versions in child classes must also be marked readonly
. It’s possible to make the visibility of an overridden method broader, marking the child method public
when the parent method was private
, but it’s illegal to do the reverse, making the visibility of a method narrower.
A commonly overridden method is the toString()
method, which gives a String
representation of the object. Overriding the toString()
method was discussed in an earlier tutorial.
Children inherit the methods of their parents, and most inherited methods can be overridden. In our Employee
and Waiter
class examples above, Waiter
inherits the methods clockIn()
and clockOut()
from Employee
. In order to use these methods (as defined in Employee
) on a Waiter
object named waiter
, all you would need to do is write waiter.clockOut()
. However, the Waiter
class has a tips
member variable. What if we need to reset the tips
variable to 0.0
whenever a waiter clocks in? We could override the clockIn()
method in Waiter
as shown below:
public clockIn(int hours) => ()
{
Console.printLine(this->name " is clocking in for a " # hours # " hour shift");
this->hoursWorked += hours;
tips = 0.0;
}
Note that this method’s header exactly matches the header of the clockIn()
method in the Employee
class. If the method had merely had the wrong return types or had been private
instead of public
, there would have been a compiler error. On the other hand, if we had written a method that took different parameters, we would have created a new overloaded method instead of overriding an existing method.
Note
Despite the similar names, the method overriding described above has nothing to do with method overloading, in which multiple methods have the same name but different parameter types. It is possible for a class to do both method overriding and method overloading. If an overloaded method is overridden, only the the method with matching parameters is overridden.
It’s useful to note that in addition to constructors, the super
keyword can also be used to call the parent class method of a method you have overridden. In the overridden clockIn()
method above, we are mostly just repeating code from the parent class method. We could shorten the code (and reduce the chances that we made an error copying it over) by calling the parent clockIn()
method defined in Employee
before resetting tips
:
public clockIn(int hours) => ()
{
super.clockIn(hours);
tips = 0.0;
}
Note
Although we can refer to parent methods using the super
keyword, it isn’t possible to refer to methods from earlier ancestors.
The locked
keyword¶
Some methods should not be overridden because their behavior needs to be fixed and predictable. By marking a method locked
, children of the class cannot override that method. In other words, the implementation of that method cannot change. Under some circumstances, the compiler can call locked
methods using static dispatch instead of dynamic dispatch, resulting in slightly faster code.
As an example, if we wanted to make it so that the clockOut()
method described above in the Employee
class works exactly the same for all employees, we could define it in this way:
public locked clockOut() => ()
{
Console.printLine(name # " is clocking out.");
}
It’s also possible to mark a class header with the locked
keyword. It’s impossible to inherit from classes that have been marked locked
. String
is an example of such a class. The behavior of String
is too important for it to be replaced by child classes that might do unexpected things.