Skip to main content

Problem Solving with Algorithms and Data Structures using Kotlin The Interactive Edition

Section A.2 Composition and Inheritance

Two important concepts in object-oriented programming are composition and inheritance. Let’s examine them in detail.

Subsection A.2.1 Composition

Composition happens when an object is made up of other objects. For example, in a card game, we might represent a Deck as made up of a list of Card objects:
Listing A.2.1.
class Card(val suit: String, val num: Int) {
}

class Deck {
    val cards = mutableListOf<Card>()
}
Composition is also known as a Has-A relationship. (A Deck has a list of Cards). Put formally, the Deck is the aggregating class, and the Card class is is the aggregated class.
Let’s use composition to build a Kotlin simulation of a toaster. What things is a toaster built from?
The first two of these are built by the toaster company. But the company that makes toasters doesn’t build the power supply or the dial (which has circuitry to control the current flow). Instead, those are parts they order from some other companies and put into the chassis.
That means that our Toaster has-a PowerSupply object and has-a Dial object.
Before we see how composition affects the way we construct and manipulate objects, let’s give a few more instances of composition (has-a) relationships. Using composition reflects the way objects are built out of other objects (parts). Each of the sub-parts has its own attributes and things that it can do (its methods). Notice that we sometimes need multiple instances of a sub-part when constructing the larger object.
  • A printer has a power supply, printer drum, and toner cartridge.
  • A bicycle has a gear assembly, handbrakes (2), and tires (2).
  • A refrigerator has a power supply, an icemaker, and a compressor.
  • A window in a word processor has a text area, a ribbon (icons for manipulating text), and two scroll bars (horizontal and vertical).
Back to the toaster. ListingΒ A.2.2 shows the constructors for the PowerSupply and Dial classes:
Listing A.2.2. Constructors for PowerSupply and Dial
class PowerSupply(var voltage: Int) {
    // new power supplies are always turned off
    var turnedOn = false

    // initializer block
    init {
        if (voltage != 220) {
            voltage = 110
        }
    }
}

class Dial(val minValue: Int, val maxValue: Int) {
    // new dials are always set to lowest value
    var dialValue = minValue
}
Now, look at how the Toaster class starts, with line numbers for reference:
Listing A.2.3.
class Toaster(var numSlots: Int, var voltage: Int) {
    var numSlices = 0
    var power: PowerSupply
    var darkness: Dial

    init {
        numSlots = maxOf(1, minOf(4, numSlots))
        power = PowerSupply(voltage)
        darkness = Dial(1, 10)
    }

    fun insertBread(newSlices : Int) {
        if (numSlices + newSlices <= numSlots) {
            numSlices += newSlices
        } else {
            println("Toaster is too full!")
        }
    }
}
The constructor in has two parameters: the number of slots in the toaster and what voltage it should have. The number of slots and number of slices of bread currently in the toaster are attributes belonging to the Toaster class, and those get set directly.
The power supply is an object, which is why line 8 has to call the PowerSupply constructor to build a power supply with the desired voltage. Think of this as the toaster company calling up the power supply company and telling them β€œsend me a 110-volt power supply” and putting that power supply into the finished toaster.
Similarly, line 9 has to call the Dial constructor to build a dial with a range of 1-10.
Although a real-life toaster is composed of parts, all of the controls are on the toaster’s exterior. We don’t expectβ€”or wantβ€”the customer to have to directly access the power supply to turn the toaster on! Similarly, we want to provide methods in the Toaster class that will give programs that use the class access to the methods and attributes of power and darkness:
Listing A.2.4. Methods for Access to Composed Classes
fun isTurnedOn() : Boolean {
    return power.turnedOn
}

fun setTurnedOn(turnedOn: Boolean) {
    power.turnedOn = turnedOn
}

fun getDialValue() : Int {
    return darkness.dialValue
}

fun setDialValue(dialValue : Int) {
    darkness.dialValue = dialValue
}
Now we can write a program that creates a Toaster object and makes it do things:
Listing A.2.5.
fun main() {
    val euroFour = Toaster(4, 220)

    euroFour.setTurnedOn(true)
    euroFour.setDialValue(4)
    euroFour.insertBread(1)
}
To summarize this discussion of composition:
  • Use composition when you have objects that are built up from other objects.
  • Provide methods in an object to give users access to the attributes and methods of the sub-objects.

Subsection A.2.2 Inheritance

An Is\-A relationship is known as inheritance:
  • An electric bicycle is a bicycle (with extra attributes and capabilities)
  • A trumpet is a bugle with valves (keys) that give the ability to play more notes
  • An alarm clock is a desk clock with extra attributes and capabilities
  • In a computer application, a β€œcombo box” is a drop-down menu with extra capabilities (you can type the value as well as select it from the list)
Without inheritance, we would have to represent the Bicycle and ElectricBicycle classes separately, but with a lot of duplicated methods between them, such as the code for color, frameSize, changing gears, etc.
That’s a lot of duplicated code when we translate it to Kotlin. We can use a concept called inheritance to say that ElectricBicycle inherits all of Bicycle’s methods and attributes. All we have to include now in ElectricBicycle are the attributes and methods that add extra capabilities to an electric bicycle.
In this example, Bicycle is called the superclass or parent class, and ElectricBicycle is called the subclass or child class. As in the real world, the child inherits things from the parent
 1 
With one exception: as the joke says, β€œParents inherit their gray hair from their children.”
.
Before we go into details about how to use inheritance in Kotlin, let’s take a break for a short exercise:

Checkpoint A.2.6.

In this exercise, we will explore has-a and is-a relationships. First, state whether the relationship of the following classes is composition or inheritance:
For classes that exhibit the inheritance relationship, could you name a few data fields/attributes for the superclass? Could you name a few for the subclass only?
For example, Teacher (subclass) is-a Person (superclass). Data fields for Person are: name, age, address, etc. Data fields for Teacher are: school, hire date, etc.
Let’s use a smaller example for our continued discussion of inheritance. We’ll have an Item class, which represents an item in a store. An Item instance has a name, a SKU (stock keeping unit), and a price. This is the parent class. We also have a SaleItem class, which is a child class. In addition to a name, SKU, and price (which it inherits from the parent class), a SaleItem has a discount percentage (expressed as a decimal).
ListingΒ A.2.7 shows the code for Item. Note that the open keyword has been added before class. This is what allows for Item to be used as a superclass. Without this keyword, it would be considered final and would not be allowed to be a parent/superclass. Similarly the open keyword in front of specialName will allow it to be overridden by a subclass.
Listing A.2.7.
open class Item(val name: String, val sku: String, var price: Double) {
    open val specialName = name + " " + sku

    open fun purchase(quantity: Int) : Double {
        return price * quantity
    }

    override fun toString() : String {
        return String.format("%s (%s): $%.2f", name, sku, price)
    }

    override fun equals(other: Any?) : Boolean {
        if (other !is Item) {
            return false
        }
        return name.equals(other.name) && 
            sku.equals(other.sku) && 
            price.equals(other.price)
    }

}
Let’s start on the code for SaleItem. The constructor of the subclass can pass along all the parameters that the super class needs, as shown in ListingΒ A.2.8:
Listing A.2.8. Code for SaleItem Constructor
class SaleItem(name: String, 
    sku: String, 
    price: Double, 
    var discount: Double
    ) : Item(name, sku, price) {
        init { discount = maxOf(0.0, minOf(discount, 1.00))}
}
Sometimes, you want to be able to add on to some of the open attributes of the parent class. You can access the parent class’ variable with the super keyword, like with specialName:
Listing A.2.9. Using super
override val specialName: String = super.specialName + " Discounted!"
Here’s the rest of the SaleItem code:
Listing A.2.10.
        override fun purchase(quantity: Int) : Double {
            var cost = price * quantity * (1 - discount)
            return cost
        }

        override fun toString() : String {
            return String.format("%s (%s): $%.2f - %.1f%% discount",name, sku, price, discount * 100.0)
        }

        override fun equals(other: Any?) : Boolean {
            if (other !is SaleItem) {
                return false
            }
            return name.equals(other.name) &&
             sku.equals(other.sku) && 
             price.equals(other.price) && discount == other.discount
        }

}
ListingΒ A.2.11 creates an Item and a SaleItem, prints them, and then purchases ten of each:
Listing A.2.11. Testing the Item and SaleItem Classes
fun main() {
    val envelopes = Item("Letter Size Envelopes - 100 count",
            "LSE-0100", 5.75)
    val marker = SaleItem("Erasable marker - black", "EMB-913",
            2.15, 0.10)

    println(envelopes)
    val envelopeTotal = envelopes.purchase(10)
    println(String.format("Ten boxes of envelopes cost $%.2f", envelopeTotal))

    println(marker)
    val markerTotal = marker.purchase(10)
    println(String.format("Ten markers cost $%.2f", markerTotal))
}
When we run the program, we get the correct output:
Letter Size Envelopes - 100 count (LSE-0100): $5.75
Ten boxes of envelopes cost $57.50
Erasable marker - black (EMB-913): $2.15 - 10.0% discount
Ten markers cost $19.35
Congratulations! We’ve used inheritance, and our program works. Now it’s time to do what Joe Armstrong said in his book Erlang and OTP in Action: β€œMake it work, then make it beautiful, then if you really, really have to, make it fast. 90 percent of the time, if you make it beautiful, it will already be fast. So really, just make it beautiful!”

Aside

Part of making a program beautiful is getting rid of unnecessary duplication. In that spirit, let’s take a closer look at the SaleItem class’s purchase, toString and equals methods. The calculation of the base price in purchase is the same as in Item. The part of toString’s format string for the name, sku, and price is the same as in Item. Similarly, the first three lines of equals are the same as in Item.
We can once again use the super keyword to access variables in a parent class. ListingΒ A.2.12 is a rewrite that eliminates unnecessary duplication:
Listing A.2.12.
override fun purchase(quantity: Int) : Double {
    var cost = super.purchase(quantity) * (1 - discount)
    return cost
}

override fun toString() : String {
    return super.toString() + String.format(" - %.1f%% discount", discount * 100.0)
}

override fun equals(other: Any?) : Boolean {
    if (other !is SaleItem) {
        return false
    }
    return super.equals(other) && discount == other.discount
}

Exercises Exercises

1.
Design a class named Person with two subclasses: Employee and Customer. The attributes for these classes are described in italics. A Person has a name, address, phone number, and email address.
An Employee has an employee number, hire date, and salary.
The Employee class, in turn, has three subclasses: Programmer, Tester, and Manager.
  • A Programmer and a Tester have a cubicle number. Both will receive a fixed bonus at the end of the year.
  • A Manager has an office number and has a variable bonus based on the performance of their team. This means that a Manager should have attributes for the target bonus amount and the performance percentage.
Finally, the Customer should have a customer number and company they work for.
Write starter code for all these classes showing the data fields and attributes. Make meaningful names for the attributes and give them an appropriate data type. (You do not need to create constructors or other methods for the classes.)
2.
The XYZZY Corporation wants to retain their most loyal customers. They launch a customer retention program and offer discount to customers who have been purchasing from the company for at least one year.
Write a subclass PreferredCustomer that extends the Customer class from the preceding exercise. The PrefCust class should have two data fields: average annual purchase amount (average dollar amount purchased per year) and years (number of years they have been a customer).
Customers get a discount percentage based on their years as a customer and average purchase amount. There are three levels of Preferred Customers: bronze, silver, and gold.
  • Bronze: \(\geq\) 1 year and average purchase amount \(\geq\) $5000 per yearβ€”5% discount
  • Silver: \(\geq\) 2 years and average purchase amount \(\geq\) $10000 per yearβ€”7.5% discount
  • Gold: \(\geq\) 3 years and average purchase amount \(\geq\) $15000 per yearβ€”10% discount
The discount percentage is a derived attributeβ€”it is never set directly, but instead is computed based on other attributes.
Write the PrefCust class with all its data fields. Write a method named getDiscount that uses the average annual purchase amount and years as a customer to return the discount percent (as a percentage).
3.
In this exercise, you will implement an Account class which represents a bank checking account. You will then create two classes that inherit from Account: SavingsAccount and CreditCardAccount.
You will then use composition to create a Customer class which includes instances of each of these account classes. Finally, you will write a program with a main method that works with these classes.
Part 1: Create a class named Account, which has the following properties:
Here are the constructors and other methods:
  • Create a two-parameter constructor that takes an account number and balance. Make sure that the balance is always greater than zero (Hint: kotlin.math.abs)
  • Implement these methods: void deposit(double amount) and \newline void withdraw(double amount). For both these methods, if the amount is less than zero, the account balance remains untouched. For the withdraw method, if the amount is greater than the balance, it remains untouched. These methods do not print anything.
  • Implement a toString method that returns a string with the account number and balance, properly labeled.
Part 2: Next, implement the SavingsAccount class. It inherits from Account and adds a apr property, which is the annual percentage rate (APR) for interest.
  • Write a three-argument constructor that takes an account number, balance, and interest rate as a decimal (thus, a 3.5% interest rate is given as 0.035). Make sure that the interest rate is never less than zero.
  • Write a calculateInterest instance method that returns the annual interest, calculated as the current balance times the annual interest rate.
  • Modify toString to include the interest rate. IMPORTANT: The value returned by the toString method must not include the calculated annual interest.
Part 3: Next, implement the CreditCardAccount class, which inherits from Account and adds these private properties:
  • apr, a double representing the annual interest rate charged on the balance.
  • creditLimit, a double which gives the credit limit for the card.
Then, implement the following:
  • A four-argument constructor that takes an account number, balance, interest rate as a decimal (thus, a 3.5% interest rate is given as 0.035), and credit limit. Make sure that neither the interest rate nor credit limit can be negative.
  • Modify toString to include the interest rate and credit limit. IMPORTANT: the value returned by the toString method must not include the monthly payment.
  • Override the withdraw method so that you can have a negative balance. If a withdrawal would push you over the credit limit, leave the balance untouched. Examples:
    • If your balance is $300 with a credit limit of $700, you can withdraw $900 (leaving a balance of $-600).
    • If your balance is $-300 with a credit limit of $700, you can withdraw $350 (leaving a balance of $-650).
    • If your balance is $-300 with a credit limit of $700, you can not withdraw $500, because that would then owe $800, which is more than your $700 limit.
    In short, the maximum amount you can withdraw is your current balance plus the credit limit.
  • Implement a calculatePayment method that works as follows: If the balance is positive, the minimum amount you have to pay on your card per month is zero. Otherwise, your monthly payment is the minimum of 20 and $(apr/12) \cdot (βˆ’balance)$
Part 4: Now, write a Customer class that will use composition to include the preceding classes.
FigureΒ A.2.13 shows the relationships of all the classes in this exercise.
Figure A.2.13. Composition and Inheritance Among All Classes
Part 5: Finally, write a program named TestCustomer.kt that creates a Customer named β€œJoe Doakes” with this data:
  • Regular account number 1037825 with a balance of $3,723.00
  • Savings account number 9016632 with a balance of $4,810.25 and an annual interest rate of 2.05%
  • Checking account number 85332162 with a balance of -$2500.00, an interest rate of 7.125%, and a credit limit of $3000.00.
Then, do the following transactions:
  • Deposit $257.18 into the regular account, then withdraw $587.23.
  • Deposit $2,466.12 into the savings account, then withdraw $8,000.00.
  • Withdraw $480.00 from the credit card account.
  • Display the status of the regular account (number and balance).
  • Display the status of the savings account (number, balance, and annual interest amount).
  • Display the status of the credit card account (number, balance, interest rate, and monthly payment due).
You have attempted of activities on this page.